1st
This commit is contained in:
174
cmd/main.go
Executable file
174
cmd/main.go
Executable file
@@ -0,0 +1,174 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gordenko.dev/dima/flashscore"
|
||||||
|
fsmodel "gordenko.dev/dima/flashscore/model"
|
||||||
|
"gordenko.dev/dima/qx"
|
||||||
|
"gordenko.dev/dima/tipper/daemon"
|
||||||
|
"gordenko.dev/dima/tipper/model"
|
||||||
|
"gordenko.dev/dima/web"
|
||||||
|
"gordenko.dev/dima/web/api"
|
||||||
|
"gordenko.dev/dima/web/router"
|
||||||
|
"gordenko.dev/dima/ws"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
port = 33465
|
||||||
|
baseDir = "/home/ubuntu/ht"
|
||||||
|
dsn = "user:password@/tipper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger := log.New(os.Stdout, "", log.LstdFlags)
|
||||||
|
|
||||||
|
db, err := qx.Open("mysql", dsn)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("qx.Open: %s\n", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
tipperModel := model.New(db)
|
||||||
|
flashscoreModel := fsmodel.New(db)
|
||||||
|
|
||||||
|
wsServer, err := ws.NewPublicServer(ws.PublicServerOptions{
|
||||||
|
Logger: log.New(os.Stdout, "ws: ", log.LstdFlags),
|
||||||
|
LostConnectionTimeout: time.Duration(60*60*24) * time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("ws.NewPublicServer: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := daemon.New(daemon.Options{
|
||||||
|
Logger: logger,
|
||||||
|
FlashscoreAPIAddr: "http://localhost:7767/api",
|
||||||
|
OnexbetWsURL: "ws://localhost:7772/ws",
|
||||||
|
OnexbetAPIURL: "http://localhost:7772/api",
|
||||||
|
FlashscoreModel: flashscoreModel,
|
||||||
|
TipperModel: tipperModel,
|
||||||
|
SportIDs: []int{
|
||||||
|
flashscore.Football,
|
||||||
|
flashscore.Handball,
|
||||||
|
flashscore.Tennis,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("daemon.New: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicAPI, err := api.NewAPI(api.Options{
|
||||||
|
Logger: log.New(os.Stdout, "api: ", log.LstdFlags),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("api.NewAPI: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicAPI.PublicFunc("setTipResult", d.SetTipResultManual)
|
||||||
|
publicAPI.PublicFunc("listSportTips", d.ListSportTips)
|
||||||
|
publicAPI.PublicFunc("listTeamZones", flashscoreModel.ListTeamZones)
|
||||||
|
publicAPI.PublicFunc("listTeamsByZone", flashscoreModel.ListTeamsByZone)
|
||||||
|
publicAPI.PublicFunc("listTeamAliases", flashscoreModel.ListTeamAliases)
|
||||||
|
publicAPI.PublicFunc("addTeamAlias", flashscoreModel.AddTeamAlias)
|
||||||
|
publicAPI.PublicFunc("removeTeamAlias", flashscoreModel.RemoveTeamAlias)
|
||||||
|
|
||||||
|
publicAPI.PublicFunc("listStrategies", tipperModel.ListStrategies)
|
||||||
|
publicAPI.PublicFunc("listStrategyRatedChampStats", tipperModel.ListStrategyRatedChampStats)
|
||||||
|
publicAPI.PublicFunc("getStrategyRatedHistory", tipperModel.GetStrategyRatedHistory)
|
||||||
|
|
||||||
|
publicAPI.PublicFunc("listStrategiesReports", tipperModel.ListStrategiesReports)
|
||||||
|
publicAPI.PublicFunc("listStrategies", tipperModel.ListStrategies)
|
||||||
|
publicAPI.PublicFunc("listStrategyChampStats", tipperModel.ListStrategyChampStats)
|
||||||
|
publicAPI.PublicFunc("getStrategyHistory", tipperModel.GetStrategyHistory)
|
||||||
|
publicAPI.PublicFunc("getSportRatedReport", tipperModel.GetSportRatedReport)
|
||||||
|
|
||||||
|
publicAPI.PublicFunc("searchMatchCandidates", d.SearchMatchCandidates)
|
||||||
|
publicAPI.PublicFunc("searchMatchCandidatesByTeamName", d.SearchMatchCandidatesByTeamName)
|
||||||
|
publicAPI.PublicFunc("addTeamsAliases", d.AddTeamsAliases)
|
||||||
|
|
||||||
|
r := router.New()
|
||||||
|
r.HandleFunc("/football", pageFootball)
|
||||||
|
r.HandleFunc("/handball", pageHandball)
|
||||||
|
r.HandleFunc("/tennis", pageTennis)
|
||||||
|
r.HandleFunc("/football-by-days", d.PageFootballByDays)
|
||||||
|
r.HandleFunc("/handball-by-days", d.PageHandballByDays)
|
||||||
|
r.HandleFunc("/tennis-by-days", d.PageTennisByDays)
|
||||||
|
r.HandleFunc("/aliases", pageAliases)
|
||||||
|
r.HandleFunc("/stats", pageStats)
|
||||||
|
r.HandleFunc("/matches", d.PageMatches)
|
||||||
|
//r.HandleFunc("/watch", d.PageWatch)
|
||||||
|
//r.HandleFunc("/unwatch", d.PageUnwatch)
|
||||||
|
r.HandleFunc("/listRatedChamps", d.ListRatedChamps)
|
||||||
|
r.HandleFunc("/", pageIndex)
|
||||||
|
r.HandleFunc("/best-strategies", pageStrategies)
|
||||||
|
//
|
||||||
|
r.Handle("/api", publicAPI)
|
||||||
|
r.Handle("/ws", wsServer)
|
||||||
|
|
||||||
|
app, err := web.NewApp(web.AppOptions{
|
||||||
|
Port: port,
|
||||||
|
Router: r,
|
||||||
|
BaseDir: baseDir,
|
||||||
|
TemplateDir: "templates",
|
||||||
|
StaticDirs: map[string]string{
|
||||||
|
"static": "static",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("web.NewApp: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go app.Run()
|
||||||
|
go d.Run()
|
||||||
|
|
||||||
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
<-sigs
|
||||||
|
|
||||||
|
logger.Println("daemon stopped.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
var topChamps = map[string]bool{
|
||||||
|
"/england/premier-league": true,
|
||||||
|
"/germany/bundesliga": true,
|
||||||
|
"/italy/serie-a": true,
|
||||||
|
"/spain/laliga": true,
|
||||||
|
"/portugal/primeira-liga": true,
|
||||||
|
"/switzerland/super-league": true,
|
||||||
|
"/turkey/super-lig": true,
|
||||||
|
"/faroe-islands/premier-league": true,
|
||||||
|
"/usa/mls": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Топ чемпы
|
||||||
|
var myIgnoreChamps = map[string]bool{
|
||||||
|
"/england/premier-league": true,
|
||||||
|
"/germany/bundesliga": true,
|
||||||
|
"/spain/laliga": true,
|
||||||
|
"/portugal/primeira-liga": true,
|
||||||
|
"/switzerland/super-league": true,
|
||||||
|
"/turkey/super-lig": true,
|
||||||
|
//"/faroe-islands/premier-league": true,
|
||||||
|
"/usa/mls": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptedChamps(champs map[string]bool) func(string) bool {
|
||||||
|
return func(champID string) bool {
|
||||||
|
return champs[champID]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nonAcceptedChamps(champs map[string]bool) func(string) bool {
|
||||||
|
return func(champID string) bool {
|
||||||
|
return !champs[champID]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
40
cmd/pages.go
Normal file
40
cmd/pages.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gordenko.dev/dima/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pageFootball(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
reply.Render("football", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageHandball(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
reply.Render("handball", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageTennis(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
reply.Render("tennis", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageAliases(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
reply.Render("aliases", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageStats(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
reply.Render("stats", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageStrategies(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
reply.Render("temp_stats", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageIndex(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
reply.GoTo("/handball")
|
||||||
|
return
|
||||||
|
}
|
||||||
1262
daemon/daemon.go
Normal file
1262
daemon/daemon.go
Normal file
File diff suppressed because it is too large
Load Diff
77
daemon/daemon_test.go
Normal file
77
daemon/daemon_test.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
func TestCorrectTotal(t *testing.T) {
|
||||||
|
//total := 58.5
|
||||||
|
//total = total - 8
|
||||||
|
|
||||||
|
total, _ := strconv.ParseFloat("68.000", 64)
|
||||||
|
|
||||||
|
//total := 68.0
|
||||||
|
|
||||||
|
total = correctTotal(total, roundUp)
|
||||||
|
|
||||||
|
fmt.Printf("%.2f\n", total)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
func TestOver(t *testing.T) {
|
||||||
|
s := NewHandballFTOver(StrategyOptions{
|
||||||
|
ID: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
match := &TeamMatch{
|
||||||
|
TipTotalOver: 50.5,
|
||||||
|
BasicUnderGoalsPerMinute: 1, // 1 гол в минуту
|
||||||
|
}
|
||||||
|
|
||||||
|
upd := onexbet.TeamLiveMatchData{
|
||||||
|
CurrentTime: 20 * 60, // 30 min
|
||||||
|
HomeGoals: 12,
|
||||||
|
AwayGoals: 10,
|
||||||
|
Total: onexbet.Total{
|
||||||
|
Over: []onexbet.ParamOffer{
|
||||||
|
{
|
||||||
|
Price: 1.41,
|
||||||
|
Param: "48.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Price: 1.5,
|
||||||
|
Param: "49.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Price: 1.51,
|
||||||
|
Param: "50.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Price: 1.65,
|
||||||
|
Param: "51.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Price: 1.85,
|
||||||
|
Param: "52.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
comment, bet, code := s.GetTip(match, upd)
|
||||||
|
|
||||||
|
fmt.Printf("Code: %d\n", code)
|
||||||
|
fmt.Printf("Comment: %s\n", comment)
|
||||||
|
|
||||||
|
buf, _ := json.MarshalIndent(bet, "", " ")
|
||||||
|
fmt.Printf("Bet: %s\n", buf)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func TestTelegram(t *testing.T) {
|
||||||
|
err := sendMessageToTelegramGroup("754217958", "Персональное сообщение Максиму")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
799
daemon/football.go
Normal file
799
daemon/football.go
Normal file
@@ -0,0 +1,799 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gordenko.dev/dima/flashscore"
|
||||||
|
"gordenko.dev/dima/onexbet"
|
||||||
|
"gordenko.dev/dima/tipper/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FootballHistoryProcessor(match *TeamMatch) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Алгоритм 2
|
||||||
|
ТБ 0.5 в Матче
|
||||||
|
|
||||||
|
До матча:
|
||||||
|
ТБ 2.5 (в матче) <=1.7
|
||||||
|
В 75% игр был гол
|
||||||
|
|
||||||
|
В Лайве:
|
||||||
|
Сумма атак обычных и опасных >= 135
|
||||||
|
6 ударов в сторону ворот OFF TARGET
|
||||||
|
4 удара в створ ON TARGET
|
||||||
|
|
||||||
|
До 70 минуты
|
||||||
|
*/
|
||||||
|
|
||||||
|
type FootballOver05 struct {
|
||||||
|
id int64
|
||||||
|
notifyInTelegram bool
|
||||||
|
isChampAccepted func(string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFootballOver05(opt StrategyOptions) FootballOver05 {
|
||||||
|
if opt.ID == 0 {
|
||||||
|
panic("StrategyID not defined")
|
||||||
|
}
|
||||||
|
s := FootballOver05{}
|
||||||
|
s.id = opt.ID
|
||||||
|
s.notifyInTelegram = opt.NotifyInTelegram
|
||||||
|
s.isChampAccepted = opt.IsChampAccepted
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver05) ID() int64 {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver05) ShortName() string {
|
||||||
|
return fmt.Sprintf("#%d FT Over 0.5 M", s.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver05) IsNotifyInTelegram() bool {
|
||||||
|
return s.notifyInTelegram
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver05) GetTelegramTipPattern() string {
|
||||||
|
return `Сигнал # %d.
|
||||||
|
Будет ГОЛ!
|
||||||
|
Алгоритм %d
|
||||||
|
Тотал Больше 0.5
|
||||||
|
Футбол. %s. %s
|
||||||
|
%s - %s
|
||||||
|
Коэф. %.3f`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver05) IsNeedToWatch(liveMatch onexbet.LiveMatch) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type FootballOver05Notes struct {
|
||||||
|
PriceOver25 float64 `json:"Over 2.5 price"`
|
||||||
|
MatchesAnalysed int `json:"matchesAnalysed"`
|
||||||
|
HasGoalInMatches int `json:"hasGoalInMatches"`
|
||||||
|
GoalsPercent float64 `json:"goalsPercent"`
|
||||||
|
ZeroDrawsInH2H int `json:"zeroDrawsInH2H"`
|
||||||
|
ZeroDrawsPercentInH2H float64 `json:"zeroDrawsPercentInH2H"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver05) IsInteresting(report *TeamMatch) (notes interface{}, _ bool) {
|
||||||
|
var (
|
||||||
|
priceOver25 float64
|
||||||
|
foundPriceOver25 bool
|
||||||
|
//f flashscore.TotalOffer
|
||||||
|
)
|
||||||
|
for _, total := range report.Odds.FullTimeTotal {
|
||||||
|
if total.Total != "2.5" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 2.5
|
||||||
|
for _, offer := range total.Offers {
|
||||||
|
if offer.Bookmaker == flashscore.Bookmaker1xBet {
|
||||||
|
priceOver25 = offer.Over
|
||||||
|
foundPriceOver25 = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundPriceOver25 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// 1xBet не нашли
|
||||||
|
// Берем первый коэф.
|
||||||
|
if len(total.Offers) > 0 {
|
||||||
|
priceOver25 = total.Offers[0].Over
|
||||||
|
foundPriceOver25 = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if priceOver25 > 1.7 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
matchCount int
|
||||||
|
hasGoalsInMatchCount int
|
||||||
|
zeroDrawsInH2HCount int
|
||||||
|
zeroDrawsPercent float64
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, match := range report.HomeTeamMatches {
|
||||||
|
if match.HomeGoals > 0 || match.AwayGoals > 0 {
|
||||||
|
hasGoalsInMatchCount++
|
||||||
|
}
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range report.AwayTeamMatches {
|
||||||
|
if match.HomeGoals > 0 || match.AwayGoals > 0 {
|
||||||
|
hasGoalsInMatchCount++
|
||||||
|
}
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range report.H2H {
|
||||||
|
if match.HomeGoals > 0 || match.AwayGoals > 0 {
|
||||||
|
hasGoalsInMatchCount++
|
||||||
|
} else {
|
||||||
|
zeroDrawsInH2HCount++
|
||||||
|
}
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchCount == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// h2hMatchCount - 100%
|
||||||
|
// zeroDrawsInH2HCount - x%
|
||||||
|
if zeroDrawsInH2HCount > 0 {
|
||||||
|
// Не более 2 сухих ничьи
|
||||||
|
if zeroDrawsInH2HCount > 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zeroDrawsPercent = float64(len(report.H2H)*100) / float64(zeroDrawsInH2HCount)
|
||||||
|
|
||||||
|
// Не более 20% сухих ничьих
|
||||||
|
if zeroDrawsPercent > 20 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchCount - 100%
|
||||||
|
// goalsInMatchCount - x%
|
||||||
|
|
||||||
|
goalsPercent := float64(hasGoalsInMatchCount*100) / float64(matchCount)
|
||||||
|
|
||||||
|
if goalsPercent < 80 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := FootballOver05Notes{
|
||||||
|
PriceOver25: priceOver25,
|
||||||
|
MatchesAnalysed: matchCount,
|
||||||
|
HasGoalInMatches: hasGoalsInMatchCount,
|
||||||
|
GoalsPercent: goalsPercent,
|
||||||
|
ZeroDrawsInH2H: zeroDrawsInH2HCount,
|
||||||
|
ZeroDrawsPercentInH2H: zeroDrawsPercent,
|
||||||
|
}
|
||||||
|
|
||||||
|
//buf, _ := json.MarshalIndent(obj, "", " ")
|
||||||
|
|
||||||
|
return obj, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver05) GetTip(match *TeamMatch, upd onexbet.TeamLiveMatchData) (_ string, _ *BetDetails, _ TipCode) {
|
||||||
|
if upd.CurrentTime == 0 {
|
||||||
|
// ВАЖНО!
|
||||||
|
// Матч еще не начался. Избегаем деления на 0 при вычислении attacksRatio
|
||||||
|
return "матч не начался", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
// Прогнозы даем до 70 минуты включительно.
|
||||||
|
maxTime := 70 * 60
|
||||||
|
|
||||||
|
if upd.CurrentTime > maxTime {
|
||||||
|
return "70 минут уже отыграли", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if upd.HomeGoals > 0 || upd.AwayGoals > 0 {
|
||||||
|
// Если гол уже забили - выходим
|
||||||
|
return "гол уже забили", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем тотал Больше 0.5
|
||||||
|
// Минимальный курс
|
||||||
|
minPrice := 1.5
|
||||||
|
var (
|
||||||
|
minPriceCheckPassed bool
|
||||||
|
currentPrice float64
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, over := range upd.Total.Over {
|
||||||
|
if over.Param == "0.5" {
|
||||||
|
if over.Price < minPrice {
|
||||||
|
return fmt.Sprintf("коэф. %.2f < %.2f", over.Price, minPrice), nil, Waiting
|
||||||
|
}
|
||||||
|
currentPrice = over.Price
|
||||||
|
minPriceCheckPassed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !minPriceCheckPassed {
|
||||||
|
return "тотал 0.5 не найден", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
attacks := upd.HomeAttacks + upd.AwayAttacks + upd.HomeDangerousAttacks + upd.AwayDangerousAttacks
|
||||||
|
shotsOffTarget := upd.HomeShotsOffTarget + upd.AwayShotsOffTarget
|
||||||
|
shotsOnTarget := upd.HomeShotsOnTarget + upd.AwayShotsOnTarget
|
||||||
|
|
||||||
|
if attacks < 135 {
|
||||||
|
return fmt.Sprintf("%d атак < 135", attacks), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if shotsOffTarget < 6 {
|
||||||
|
return fmt.Sprintf("%d shotsOffTarget < 6", shotsOffTarget), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if shotsOnTarget < 4 {
|
||||||
|
return fmt.Sprintf("%d shotsOnTarget < 4", shotsOnTarget), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", &BetDetails{
|
||||||
|
Market: MarketTotal,
|
||||||
|
Side: SideOver,
|
||||||
|
Param: "0.5",
|
||||||
|
Price: currentPrice,
|
||||||
|
}, Bet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver05) GetResult(tip model.Tip, stats onexbet.TeamMatchResult) (_ *TipResult) {
|
||||||
|
//buf, _ := json.MarshalIndent(stats, "", " ")
|
||||||
|
//fmt.Printf("%s\n", buf)
|
||||||
|
|
||||||
|
if stats.Status != onexbet.StatusMatchCompleted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := new(TipResult)
|
||||||
|
goals := stats.HomePoints + stats.AwayPoints
|
||||||
|
if goals > 0 {
|
||||||
|
res.Status = model.Won
|
||||||
|
} else {
|
||||||
|
res.Status = model.Lost
|
||||||
|
}
|
||||||
|
res.Result = fmt.Sprintf("счет: %d-%d", stats.HomePoints, stats.AwayPoints)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
////////// P1 OVER 0.5
|
||||||
|
|
||||||
|
type FootballP1Over05 struct {
|
||||||
|
id int64
|
||||||
|
notifyInTelegram bool
|
||||||
|
isChampAccepted func(string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFootballP1Over05(opt StrategyOptions) FootballP1Over05 {
|
||||||
|
if opt.ID == 0 {
|
||||||
|
panic("StrategyID not defined")
|
||||||
|
}
|
||||||
|
s := FootballP1Over05{}
|
||||||
|
s.id = opt.ID
|
||||||
|
s.notifyInTelegram = opt.NotifyInTelegram
|
||||||
|
s.isChampAccepted = opt.IsChampAccepted
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballP1Over05) ID() int64 {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballP1Over05) ShortName() string {
|
||||||
|
return fmt.Sprintf("#%d H1 Over 0.5 M", s.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballP1Over05) IsNotifyInTelegram() bool {
|
||||||
|
return s.notifyInTelegram
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballP1Over05) GetTelegramTipPattern() string {
|
||||||
|
return `Сигнал # %d.
|
||||||
|
Будет ГОЛ!
|
||||||
|
Алгоритм %d
|
||||||
|
Первый тайм, Тотал Больше 0.5
|
||||||
|
Футбол. %s. %s
|
||||||
|
%s - %s
|
||||||
|
Коэф. %.3f`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballP1Over05) IsNeedToWatch(liveMatch onexbet.LiveMatch) bool {
|
||||||
|
if liveMatch.CurrentPeriod == 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type isInterestingNotesFootballP1Over05 struct {
|
||||||
|
PriceOver25 float64 `json:"Over 2.5 price"`
|
||||||
|
MatchesAnalysed int `json:"matchesAnalysed"`
|
||||||
|
HasGoalInP1Matches int `json:"hasGoalInP1Matches"`
|
||||||
|
P1GoalsPercent float64 `json:"p1GoalsPercent"`
|
||||||
|
HasGoalInP1H2HMatches int `json:"hasGoalInP1H2HMatches"`
|
||||||
|
P1H2HGoalsPercent float64 `json:"p1H2HGoalsPercent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballP1Over05) IsInteresting(report *TeamMatch) (notes interface{}, _ bool) {
|
||||||
|
var (
|
||||||
|
priceOver25 float64
|
||||||
|
foundPriceOver25 bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, total := range report.Odds.FullTimeTotal {
|
||||||
|
if total.Total != "2.5" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 2.5
|
||||||
|
for _, offer := range total.Offers {
|
||||||
|
if offer.Bookmaker == flashscore.Bookmaker1xBet {
|
||||||
|
priceOver25 = offer.Over
|
||||||
|
foundPriceOver25 = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundPriceOver25 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// 1xBet не нашли
|
||||||
|
// Берем первый коэф.
|
||||||
|
if len(total.Offers) > 0 {
|
||||||
|
priceOver25 = total.Offers[0].Over
|
||||||
|
foundPriceOver25 = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if priceOver25 > 1.58 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
hasStatsMatchCount int
|
||||||
|
hasGoalsInP1MatchCount int
|
||||||
|
|
||||||
|
hasH2HStatsMatchCount int
|
||||||
|
hasGoalsInP1H2HMatchCount int
|
||||||
|
p1H2HGoalsPercent float64
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, match := range report.HomeTeamMatches {
|
||||||
|
if match.HasScoreByPeriods {
|
||||||
|
hasStatsMatchCount++
|
||||||
|
|
||||||
|
if match.P1HomeGoals > 0 || match.P1AwayGoals > 0 {
|
||||||
|
hasGoalsInP1MatchCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range report.AwayTeamMatches {
|
||||||
|
if match.HasScoreByPeriods {
|
||||||
|
hasStatsMatchCount++
|
||||||
|
|
||||||
|
if match.P1HomeGoals > 0 || match.P1AwayGoals > 0 {
|
||||||
|
hasGoalsInP1MatchCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range report.H2H {
|
||||||
|
if match.HasScoreByPeriods {
|
||||||
|
hasH2HStatsMatchCount++
|
||||||
|
|
||||||
|
if match.P1HomeGoals > 0 || match.P1AwayGoals > 0 {
|
||||||
|
hasGoalsInP1H2HMatchCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasStatsMatchCount == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasStatsMatchCount - 100%
|
||||||
|
// hasGoalsInP1MatchCount - x%
|
||||||
|
|
||||||
|
p1GoalsPercent := float64(hasGoalsInP1MatchCount*100) / float64(hasStatsMatchCount)
|
||||||
|
|
||||||
|
if p1GoalsPercent < 75 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasH2HStatsMatchCount > 0 {
|
||||||
|
// hasH2HStatsMatchCount - 100%
|
||||||
|
// hasGoalsInP1H2HMatchCount - x%
|
||||||
|
|
||||||
|
p1H2HGoalsPercent = float64(hasGoalsInP1H2HMatchCount*100) / float64(hasH2HStatsMatchCount)
|
||||||
|
|
||||||
|
if p1H2HGoalsPercent < 80 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := isInterestingNotesFootballP1Over05{
|
||||||
|
PriceOver25: priceOver25,
|
||||||
|
MatchesAnalysed: hasStatsMatchCount,
|
||||||
|
HasGoalInP1Matches: hasGoalsInP1MatchCount,
|
||||||
|
P1GoalsPercent: p1GoalsPercent,
|
||||||
|
HasGoalInP1H2HMatches: hasGoalsInP1H2HMatchCount,
|
||||||
|
P1H2HGoalsPercent: p1H2HGoalsPercent,
|
||||||
|
}
|
||||||
|
|
||||||
|
//buf, _ := json.MarshalIndent(obj, "", " ")
|
||||||
|
|
||||||
|
return obj, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballP1Over05) GetTip(match *TeamMatch, upd onexbet.TeamLiveMatchData) (_ string, _ *BetDetails, _ TipCode) {
|
||||||
|
if upd.CurrentTime == 0 {
|
||||||
|
// ВАЖНО!
|
||||||
|
// Матч еще не начался. Избегаем деления на 0 при вычислении attacksRatio
|
||||||
|
return "матч не начался", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
// Прогнозы даем до 20 минуты включительно.
|
||||||
|
maxTime := 20 * 60
|
||||||
|
|
||||||
|
if upd.CurrentTime > maxTime {
|
||||||
|
return "20 минут уже отыграли", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if upd.HomeGoals > 0 || upd.AwayGoals > 0 {
|
||||||
|
// Если гол уже забили - выходим
|
||||||
|
return "гол уже забили", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем тотал Больше 0.5
|
||||||
|
// Минимальный курс
|
||||||
|
minPrice := 1.5
|
||||||
|
var (
|
||||||
|
minPriceCheckPassed bool
|
||||||
|
currentPrice float64
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, over := range upd.H1.Total.Over {
|
||||||
|
if over.Param == "0.5" {
|
||||||
|
if over.Price < minPrice {
|
||||||
|
return fmt.Sprintf("коэф. %.2f < %.2f", over.Price, minPrice), nil, Waiting
|
||||||
|
}
|
||||||
|
currentPrice = over.Price
|
||||||
|
minPriceCheckPassed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !minPriceCheckPassed {
|
||||||
|
return "тотал 0.5 не найден", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
// соотношение атак по времени >= 2.1 (за 10 минут от 21 атаки)
|
||||||
|
attacks := upd.HomeAttacks + upd.AwayAttacks
|
||||||
|
shotsOffTarget := upd.HomeShotsOffTarget + upd.AwayShotsOffTarget
|
||||||
|
|
||||||
|
attacksRatio := float64(attacks*60) / float64(upd.CurrentTime)
|
||||||
|
|
||||||
|
if attacksRatio < 2.1 {
|
||||||
|
return fmt.Sprintf("отношение атак ко времени %.2f < 2.1", attacksRatio), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if shotsOffTarget < 3 {
|
||||||
|
return fmt.Sprintf("%d shotsOffTarget < 6", shotsOffTarget), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", &BetDetails{
|
||||||
|
Market: MarketTotalH1,
|
||||||
|
Side: SideOver,
|
||||||
|
Param: "0.5",
|
||||||
|
Price: currentPrice,
|
||||||
|
}, Bet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballP1Over05) GetResult(tip model.Tip, stats onexbet.TeamMatchResult) (_ *TipResult) {
|
||||||
|
//buf, _ := json.MarshalIndent(stats, "", " ")
|
||||||
|
//fmt.Printf("%s\n", buf)
|
||||||
|
|
||||||
|
h1, ok := stats.ScoreByPeriods[1]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = stats.ScoreByPeriods[2]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := new(TipResult)
|
||||||
|
h1TotalGoals := h1.HomePoints + h1.AwayPoints
|
||||||
|
if h1TotalGoals > 0 {
|
||||||
|
res.Status = model.Won
|
||||||
|
} else {
|
||||||
|
res.Status = model.Lost
|
||||||
|
}
|
||||||
|
res.Result = fmt.Sprintf("1й тайм: %d-%d", h1.HomePoints, h1.AwayPoints)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////// OVER 1.5
|
||||||
|
|
||||||
|
type FootballOver15 struct {
|
||||||
|
id int64
|
||||||
|
notifyInTelegram bool
|
||||||
|
isChampAccepted func(string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFootballOver15(opt StrategyOptions) FootballOver15 {
|
||||||
|
if opt.ID == 0 {
|
||||||
|
panic("StrategyID not defined")
|
||||||
|
}
|
||||||
|
s := FootballOver15{}
|
||||||
|
s.id = opt.ID
|
||||||
|
s.notifyInTelegram = opt.NotifyInTelegram
|
||||||
|
s.isChampAccepted = opt.IsChampAccepted
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver15) ID() int64 {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver15) ShortName() string {
|
||||||
|
return fmt.Sprintf("#%d FT Over 1.5 M", s.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver15) IsNotifyInTelegram() bool {
|
||||||
|
return s.notifyInTelegram
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver15) GetTelegramTipPattern() string {
|
||||||
|
return `Сигнал # %d.
|
||||||
|
Будет 2й ГОЛ!
|
||||||
|
Алгоритм %d
|
||||||
|
Тотал Больше 1.5
|
||||||
|
Футбол. %s. %s
|
||||||
|
%s - %s
|
||||||
|
Коэф. %.3f`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver15) IsNeedToWatch(liveMatch onexbet.LiveMatch) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type isInterestingNotesFootballOver15 struct {
|
||||||
|
PriceOver25 float64 `json:"Over 2.5 price"`
|
||||||
|
MatchesAnalysed int `json:"matchesAnalysed"`
|
||||||
|
HasGoalInMatches int `json:"hasGoalInMatches"`
|
||||||
|
GoalsPercent float64 `json:"goalsPercent"`
|
||||||
|
FavoritePrice float64 `json:"favoritePrice"`
|
||||||
|
Favorite string `json:"favorite"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver15) IsInteresting(report *TeamMatch) (notes interface{}, _ bool) {
|
||||||
|
var favoritePrice float64 = 2
|
||||||
|
var favorite string
|
||||||
|
|
||||||
|
// Проходим по всем ценам, каждый раз обновляя минимальную
|
||||||
|
for _, offer := range report.Odds.Winner {
|
||||||
|
if offer.Home < favoritePrice && offer.Home > 1.01 {
|
||||||
|
favoritePrice = offer.Home
|
||||||
|
favorite = "home"
|
||||||
|
}
|
||||||
|
|
||||||
|
if offer.Away < favoritePrice && offer.Away > 1.01 {
|
||||||
|
favoritePrice = offer.Away
|
||||||
|
favorite = "away"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if favoritePrice > 1.4 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
priceOver25 float64
|
||||||
|
foundPriceOver25 bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, total := range report.Odds.FullTimeTotal {
|
||||||
|
if total.Total != "2.5" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 2.5
|
||||||
|
for _, offer := range total.Offers {
|
||||||
|
if offer.Bookmaker == flashscore.Bookmaker1xBet {
|
||||||
|
priceOver25 = offer.Over
|
||||||
|
foundPriceOver25 = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundPriceOver25 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// 1xBet не нашли
|
||||||
|
// Берем первый коэф.
|
||||||
|
if len(total.Offers) > 0 {
|
||||||
|
priceOver25 = total.Offers[0].Over
|
||||||
|
foundPriceOver25 = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if priceOver25 > 1.7 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
matchCount int
|
||||||
|
hasGoalsInMatchCount int
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, match := range report.HomeTeamMatches {
|
||||||
|
if match.HomeGoals > 0 || match.AwayGoals > 0 {
|
||||||
|
hasGoalsInMatchCount++
|
||||||
|
}
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range report.AwayTeamMatches {
|
||||||
|
if match.HomeGoals > 0 || match.AwayGoals > 0 {
|
||||||
|
hasGoalsInMatchCount++
|
||||||
|
}
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range report.H2H {
|
||||||
|
if match.HomeGoals > 0 || match.AwayGoals > 0 {
|
||||||
|
hasGoalsInMatchCount++
|
||||||
|
}
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchCount == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchCount - 100%
|
||||||
|
// goalsInMatchCount - x%
|
||||||
|
|
||||||
|
goalsPercent := float64(hasGoalsInMatchCount*100) / float64(matchCount)
|
||||||
|
|
||||||
|
if goalsPercent < 75 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := isInterestingNotesFootballOver15{
|
||||||
|
PriceOver25: priceOver25,
|
||||||
|
MatchesAnalysed: matchCount,
|
||||||
|
HasGoalInMatches: hasGoalsInMatchCount,
|
||||||
|
GoalsPercent: goalsPercent,
|
||||||
|
FavoritePrice: favoritePrice,
|
||||||
|
Favorite: favorite,
|
||||||
|
}
|
||||||
|
|
||||||
|
report.Favorite = favorite
|
||||||
|
|
||||||
|
//buf, _ := json.MarshalIndent(obj, "", " ")
|
||||||
|
|
||||||
|
return obj, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver15) GetTip(match *TeamMatch, upd onexbet.TeamLiveMatchData) (_ string, _ *BetDetails, _ TipCode) {
|
||||||
|
if upd.CurrentTime == 0 {
|
||||||
|
// ВАЖНО!
|
||||||
|
// Матч еще не начался. Избегаем деления на 0 при вычислении attacksRatio
|
||||||
|
return "матч не начался", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
// Прогнозы даем до 70 минуты включительно.
|
||||||
|
maxTime := 70 * 60
|
||||||
|
|
||||||
|
if upd.CurrentTime > maxTime {
|
||||||
|
return "70 минут уже отыграли", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
goals := upd.HomeGoals + upd.AwayGoals
|
||||||
|
|
||||||
|
if goals > 1 {
|
||||||
|
// Если 2 гола уже забили - выходим
|
||||||
|
return "забили более 1 гола", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if goals == 0 {
|
||||||
|
return "счет 0-0", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upd.HomeGoals == 1 && match.Favorite == "away") || (upd.AwayGoals == 1 && match.Favorite == "home") {
|
||||||
|
// все ок, фаворит уступает - анализируем дальше
|
||||||
|
} else {
|
||||||
|
// забил фаворит - далее неинтересно
|
||||||
|
return "забил фаворит", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем тотал Больше 1.5
|
||||||
|
// Минимальный курс
|
||||||
|
minPrice := 1.5
|
||||||
|
var (
|
||||||
|
minPriceCheckPassed bool
|
||||||
|
currentPrice float64
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, over := range upd.Total.Over {
|
||||||
|
if over.Param == "1.5" {
|
||||||
|
if over.Price < minPrice {
|
||||||
|
comment := fmt.Sprintf("коэф. %.2f < %.2f", over.Price, minPrice)
|
||||||
|
return comment, nil, Waiting
|
||||||
|
}
|
||||||
|
currentPrice = over.Price
|
||||||
|
minPriceCheckPassed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !minPriceCheckPassed {
|
||||||
|
return "тотал 1.5 не найден", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
attacks := upd.HomeAttacks + upd.AwayAttacks + upd.HomeDangerousAttacks + upd.AwayDangerousAttacks
|
||||||
|
shotsOffTarget := upd.HomeShotsOffTarget + upd.AwayShotsOffTarget
|
||||||
|
shotsOnTarget := upd.HomeShotsOnTarget + upd.AwayShotsOnTarget
|
||||||
|
|
||||||
|
if attacks < 125 {
|
||||||
|
return fmt.Sprintf("%d атак < 125", attacks), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if shotsOffTarget < 6 {
|
||||||
|
return fmt.Sprintf("%d shotsOffTarget < 6", shotsOffTarget), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if shotsOnTarget < 4 {
|
||||||
|
return fmt.Sprintf("%d shotsOnTarget < 4", shotsOnTarget), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", &BetDetails{
|
||||||
|
Market: MarketTotal,
|
||||||
|
Side: SideOver,
|
||||||
|
Param: "1.5",
|
||||||
|
Price: currentPrice,
|
||||||
|
}, Bet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s FootballOver15) GetResult(tip model.Tip, stats onexbet.TeamMatchResult) (_ *TipResult) {
|
||||||
|
//buf, _ := json.MarshalIndent(stats, "", " ")
|
||||||
|
//fmt.Printf("%s\n", buf)
|
||||||
|
|
||||||
|
if stats.Status != onexbet.StatusMatchCompleted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := new(TipResult)
|
||||||
|
goals := stats.HomePoints + stats.AwayPoints
|
||||||
|
if goals > 1 {
|
||||||
|
res.Status = model.Won
|
||||||
|
} else {
|
||||||
|
res.Status = model.Lost
|
||||||
|
}
|
||||||
|
res.Result = fmt.Sprintf("счет: %d-%d", stats.HomePoints, stats.AwayPoints)
|
||||||
|
return res
|
||||||
|
}
|
||||||
1165
daemon/handball.go
Normal file
1165
daemon/handball.go
Normal file
File diff suppressed because it is too large
Load Diff
41
daemon/helpers.go
Normal file
41
daemon/helpers.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"gordenko.dev/dima/onexbet"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isFloatEqual(a, b, eps float64) bool {
|
||||||
|
if math.Abs(a-b) < eps {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func SortOffersByParamMaxMin(offers []onexbet.ParamOffer) {
|
||||||
|
sort.Slice(offers, func(i, j int) bool {
|
||||||
|
if offers[i].ParamFloat > offers[j].ParamFloat {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func SortOffersByParamMinMax(offers []onexbet.ParamOffer) {
|
||||||
|
sort.Slice(offers, func(i, j int) bool {
|
||||||
|
if offers[i].ParamFloat < offers[j].ParamFloat {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTennisSetCompleted(score onexbet.PeriodScore) (_ bool) {
|
||||||
|
if (score.HomePoints == 6 && score.AwayPoints < 5) || score.HomePoints == 7 ||
|
||||||
|
(score.AwayPoints == 6 && score.HomePoints < 5) || score.AwayPoints == 7 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
875
daemon/onexbet.go
Executable file
875
daemon/onexbet.go
Executable file
@@ -0,0 +1,875 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gordenko.dev/dima/flashscore"
|
||||||
|
"gordenko.dev/dima/onexbet"
|
||||||
|
"gordenko.dev/dima/tipper/model"
|
||||||
|
"gordenko.dev/dima/web/api"
|
||||||
|
"gordenko.dev/dima/ws"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Daemon) tryLinkToLiveMatch(sportID int, liveMatch onexbet.LiveMatch) {
|
||||||
|
matchName := fmt.Sprintf("%s - %s", liveMatch.Home, liveMatch.Away)
|
||||||
|
|
||||||
|
s.logger.Printf("try link: %s\n", matchName)
|
||||||
|
// Если уже распознан - пропускаем
|
||||||
|
|
||||||
|
// На всякий пожарный проверка
|
||||||
|
//_, ok := s.onexbetMatchToTeamMatch[liveMatch.MatchID]
|
||||||
|
//if ok {
|
||||||
|
// s.logger.Printf("try link: %s\n", matchName)
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
|
||||||
|
// Если этот матч уже проверяли - игнорируем
|
||||||
|
s.mutex.Lock()
|
||||||
|
|
||||||
|
unrecognizedMatches, ok := s.unrecognizedOnexbetMatches[sportID]
|
||||||
|
if !ok {
|
||||||
|
s.logger.Printf("unrecognized matches for the sport %d not found\n", sportID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if unrecognizedMatches[liveMatch.MatchID] {
|
||||||
|
//s.logger.Printf("Match (%s) is unrecognized. Don't repeat identification\n", matchName)
|
||||||
|
s.mutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
homeTeamCandidates, err := s.fsmodel.ListTeamCandidates(sportID, liveMatch.Home)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("fsmodel.ListFootballTeamCandidates: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//s.logger.Printf("homeTeamCandidates (for %s): %q", liveMatch.Home, homeTeamCandidates)
|
||||||
|
|
||||||
|
awayTeamCandidates, err := s.fsmodel.ListTeamCandidates(sportID, liveMatch.Away)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("fsmodel.ListFootballTeamCandidates: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//s.logger.Printf("awayTeamCandidates (for %s): %q", liveMatch.Away, awayTeamCandidates)
|
||||||
|
//
|
||||||
|
|
||||||
|
teamMatches, ok := s.teamMatches[sportID]
|
||||||
|
if !ok {
|
||||||
|
err = fmt.Errorf("Not found matches for the sportID %d", sportID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range teamMatches {
|
||||||
|
var isTeamsSwapped bool
|
||||||
|
|
||||||
|
isIdentified := s.tryIdentifyMatch(match.Home.TeamID, match.Away.TeamID,
|
||||||
|
homeTeamCandidates, awayTeamCandidates)
|
||||||
|
|
||||||
|
if !isIdentified {
|
||||||
|
// Пробуем опознать если home и away поменять местами
|
||||||
|
isIdentified = s.tryIdentifyMatch(match.Home.TeamID, match.Away.TeamID,
|
||||||
|
awayTeamCandidates, homeTeamCandidates)
|
||||||
|
|
||||||
|
isTeamsSwapped = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if isIdentified {
|
||||||
|
|
||||||
|
if !match.Home.WasLinked {
|
||||||
|
err = s.fsmodel.SetTeamWasLinked(match.Home.TeamID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("fsmodel.SetTeamWasLinked: %s; teamID=%s\n",
|
||||||
|
err, match.Home.TeamID)
|
||||||
|
}
|
||||||
|
s.mutex.Lock()
|
||||||
|
match.Home.WasLinked = true
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !match.Away.WasLinked {
|
||||||
|
err = s.fsmodel.SetTeamWasLinked(match.Away.TeamID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("fsmodel.SetTeamWasLinked: %s; teamID=%s\n",
|
||||||
|
err, match.Away.TeamID)
|
||||||
|
}
|
||||||
|
s.mutex.Lock()
|
||||||
|
match.Away.WasLinked = true
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
match.IsLinkedToOnexbet = true
|
||||||
|
match.OnexbetMatchID = liveMatch.MatchID
|
||||||
|
match.OnexbetStatsMatchID = liveMatch.StatsMatchID
|
||||||
|
match.Score = liveMatch.Score
|
||||||
|
match.ScoreByPeriods = liveMatch.ScoreByPeriods
|
||||||
|
match.CurrentPeriod = liveMatch.CurrentPeriod
|
||||||
|
match.IsTeamsSwapped = isTeamsSwapped
|
||||||
|
|
||||||
|
s.onexbetMatchToTeamMatch[liveMatch.MatchID] = match
|
||||||
|
s.identifiedMatches[liveMatch.MatchID] = true
|
||||||
|
|
||||||
|
s.logger.Printf("Identified: %s; %s; %s\n", matchName, liveMatch.MatchID, match.MatchID)
|
||||||
|
|
||||||
|
// Матч опознан и перешел в inplay
|
||||||
|
s.publisher.Publish(
|
||||||
|
getChannel(match.SportID),
|
||||||
|
WSMessageInplayStatusChanged,
|
||||||
|
InplayStatusMessage{
|
||||||
|
MatchID: match.MatchID,
|
||||||
|
IsInplay: match.IsInplay,
|
||||||
|
IsLinkedToOnexbet: match.IsLinkedToOnexbet,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
s.mutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
// Кешируем негативный результат, чтобы не искать в базе ежеминутно
|
||||||
|
// Если пользователь распознает новые матчи, - кэш будет очищен
|
||||||
|
unrecognizedMatches, ok = s.unrecognizedOnexbetMatches[sportID]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unrecognizedMatches[liveMatch.MatchID] = true
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.logger.Printf("Match (%s; %s) is not identified. Added to unrecognized\n", matchName, liveMatch.MatchID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Daemon) tryIdentifyMatch(homeTeamID, awayTeamID string, homeTeamCandidates, awayTeamCandidates []string) (_ bool) {
|
||||||
|
for _, candidateHomeTeamID := range homeTeamCandidates {
|
||||||
|
if homeTeamID == candidateHomeTeamID {
|
||||||
|
// Если нашли совпадение для home - ищем совпадение для away
|
||||||
|
for _, candidateAwayTeamID := range awayTeamCandidates {
|
||||||
|
if awayTeamID == candidateAwayTeamID {
|
||||||
|
// Mатч идентифицирован
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Daemon) onOnexbetWsClientConnected(conn *ws.Conn) {
|
||||||
|
s.logger.Printf("Onexbet Ws Client Connected\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func (s *Daemon) onOnexbetMatchFinished(conn *ws.Conn, in onexbet.WatchReq) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
match, ok := s.onexbetMatchToTeamMatch[in.MatchID]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
match.IsInplay = false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (s *Daemon) onOnexbetTeamLiveMatches(conn *ws.Conn, in onexbet.LiveMatchesMessage) {
|
||||||
|
//s.logger.Println("onOnexbetTeamLiveMatches")
|
||||||
|
//s.logger.Printf("onexbetMatchToTeamMatch size: %d\n", len(s.onexbetMatchToTeamMatch))
|
||||||
|
|
||||||
|
sportID, ok := onexbetSportToFlashscoreSport[in.SportID]
|
||||||
|
if !ok {
|
||||||
|
s.logger.Printf("Bug: unknown OnexbetSport %d\n", in.SportID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Printf("Onexbet live matches: %d, watching: %d; Inplay: %d\n",
|
||||||
|
len(in.Matches), len(in.Watching), len(s.onexbetMatchToTeamMatch))
|
||||||
|
|
||||||
|
liveMap := make(map[string]onexbet.LiveMatch)
|
||||||
|
|
||||||
|
// В цикле рашем 3 задачи:
|
||||||
|
// - формируем словарь из live матчей на сайте 1xBet, для удобства дальнейшей обработки
|
||||||
|
// - для матчей в inplay проверяем не появился ли liveMatch.StatsMatchID, по которому
|
||||||
|
// можно будет узнать результат матча на сайте 1xBet
|
||||||
|
// - пытаемся распознать новые матчи
|
||||||
|
for _, liveMatch := range in.Matches {
|
||||||
|
liveMap[liveMatch.MatchID] = liveMatch
|
||||||
|
|
||||||
|
match, ok := s.onexbetMatchToTeamMatch[liveMatch.MatchID]
|
||||||
|
if ok {
|
||||||
|
s.logger.Printf("Match (%s - %s) already identified\n", match.Home.CanonicalName, match.Away.CanonicalName)
|
||||||
|
// ВАЖНО!
|
||||||
|
// liveMatch.StatsMatchID появится когда матч перейдет в лайв.
|
||||||
|
if match.OnexbetStatsMatchID == "" && liveMatch.StatsMatchID != "" {
|
||||||
|
match.OnexbetStatsMatchID = liveMatch.StatsMatchID
|
||||||
|
match.Score = liveMatch.Score
|
||||||
|
match.ScoreByPeriods = liveMatch.ScoreByPeriods
|
||||||
|
match.CurrentPeriod = liveMatch.CurrentPeriod
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !s.identifiedMatches[liveMatch.MatchID] {
|
||||||
|
// Матча нет среди распознанных - пытаемся опознать
|
||||||
|
s.tryLinkToLiveMatch(sportID, liveMatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var watchingMap = make(map[string]bool)
|
||||||
|
|
||||||
|
for _, onexbetMatchID := range in.Watching {
|
||||||
|
watchingMap[onexbetMatchID] = true
|
||||||
|
|
||||||
|
_, isMatchInInplay := s.onexbetMatchToTeamMatch[onexbetMatchID]
|
||||||
|
if !isMatchInInplay {
|
||||||
|
// Матча больше нет в inplay. Если 1xbet присылает его в Watching -
|
||||||
|
// значит API-запрос Unwatch где-то потерялся. Поэтому отправляем снова.
|
||||||
|
s.onexbetAPIClient.Call(api.CallReq{
|
||||||
|
FuncName: "unwatch",
|
||||||
|
In: onexbet.WatchReq{
|
||||||
|
SportID: in.SportID,
|
||||||
|
MatchID: onexbetMatchID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Цикл по inplay матчам
|
||||||
|
//if sportID == flashscore.Tennis {
|
||||||
|
//
|
||||||
|
//s.checkInplayTennisMatches(liveMap, watchingMap)
|
||||||
|
//} else {
|
||||||
|
s.checkInplayTeamMatches(sportID, in.SportID, liveMap, watchingMap)
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
//func (s *Daemon) checkInplayTennisMatches(liveMap map[string]bool, watchingMap map[string]bool) {
|
||||||
|
//for onexbetMatchID, match := range s.onexbetMatchToTeamMatch {
|
||||||
|
//if match.
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
func (s *Daemon) checkInplayTeamMatches(sportID int, onexbetSportID onexbet.OnexbetSport, liveMap map[string]onexbet.LiveMatch, watchingMap map[string]bool) {
|
||||||
|
for onexbetMatchID, match := range s.onexbetMatchToTeamMatch {
|
||||||
|
if match.SportID != sportID {
|
||||||
|
// Если другой спорт - пропускаем матч
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
liveMatch, isIn1xbetLive := liveMap[onexbetMatchID]
|
||||||
|
|
||||||
|
if !isIn1xbetLive {
|
||||||
|
// МАТЧИ могут просто исчезать из ЛАЙВ списка, поэтому просто удалять
|
||||||
|
// нельзя! Сперва запросим результат матча у 1xbet и решим что делать.
|
||||||
|
//err := s.processMatchDisappearedFromLive(match, onexbetMatchID)
|
||||||
|
//if err != nil {
|
||||||
|
// s.logger.Printf("processMatchDisappearedFromLive: %s\n", err)
|
||||||
|
//}
|
||||||
|
|
||||||
|
s.logger.Printf("Inplay match (%s - %s) not found in 1xbet live matches. Remove from inplay\n",
|
||||||
|
match.Home.CanonicalName, match.Away.CanonicalName)
|
||||||
|
|
||||||
|
s.removeMatchFromInplay(match, onexbetMatchID)
|
||||||
|
} else {
|
||||||
|
if !watchingMap[onexbetMatchID] {
|
||||||
|
// Матч есть в лайве 1xbet (liveMap) - но нет в наблюдаемых. Например,
|
||||||
|
// перезагрузился 1xbet демон, или отписались.
|
||||||
|
if len(match.Strategies) > 0 {
|
||||||
|
// Если еще есть не сработавшие стратегии - начинаем наблюдать.
|
||||||
|
//s.logger.Printf("Watch: %d, %s\n", in.SportID, onexbetMatchID)
|
||||||
|
|
||||||
|
// Подписываемся на матчи, которые только что идентифицировали + на те, которые
|
||||||
|
// пропали из watch списка (например из-за перезагрузки 1xBET демона)
|
||||||
|
|
||||||
|
for _, strategy := range match.Strategies {
|
||||||
|
if strategy.IsNeedToWatch(liveMatch) {
|
||||||
|
// Если хотя бы одной стратегии нужно наблюдать за матчем
|
||||||
|
// - подписываемся и прерываем цикл
|
||||||
|
match.IsInplay = true
|
||||||
|
|
||||||
|
s.onexbetAPIClient.Call(api.CallReq{
|
||||||
|
FuncName: "watch",
|
||||||
|
In: onexbet.WatchReq{
|
||||||
|
SportID: onexbetSportID,
|
||||||
|
MatchID: onexbetMatchID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
// Матч дабавили в inplay
|
||||||
|
s.publisher.Publish(
|
||||||
|
getChannel(match.SportID),
|
||||||
|
WSMessageInplayStatusChanged,
|
||||||
|
InplayStatusMessage{
|
||||||
|
MatchID: match.MatchID,
|
||||||
|
IsInplay: match.IsInplay,
|
||||||
|
IsLinkedToOnexbet: match.IsLinkedToOnexbet,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
s.mutex.Unlock()
|
||||||
|
// ВАЖНО!
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.mutex.Lock()
|
||||||
|
//s.logger.Printf("Unwatch: %d, %s\n", in.SportID, onexbetMatchID)
|
||||||
|
// Матч больше не интересен
|
||||||
|
// Все стратегии сработали - отписываемся от матча
|
||||||
|
delete(s.onexbetMatchToTeamMatch, onexbetMatchID)
|
||||||
|
match.IsInplay = false
|
||||||
|
|
||||||
|
s.onexbetAPIClient.Call(api.CallReq{
|
||||||
|
FuncName: "unwatch",
|
||||||
|
In: onexbet.WatchReq{
|
||||||
|
SportID: onexbetSportID,
|
||||||
|
MatchID: onexbetMatchID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Матч убрали из inplay
|
||||||
|
s.publisher.Publish(
|
||||||
|
getChannel(match.SportID),
|
||||||
|
WSMessageInplayStatusChanged,
|
||||||
|
InplayStatusMessage{
|
||||||
|
MatchID: match.MatchID,
|
||||||
|
IsInplay: match.IsInplay,
|
||||||
|
IsLinkedToOnexbet: match.IsLinkedToOnexbet,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Матч есть в лайве 1xbet (liveMap) и в наблюдаемых - все OK.
|
||||||
|
// Ничего не делаем.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func (s *Daemon) processMatchDisappearedFromLive(match *TeamMatch, onexbetMatchID string) (err error) {
|
||||||
|
if match.OnexbetStatsMatchID == "" {
|
||||||
|
// Ситуация возвожна, когда матч появился на короткий срок, OnexbetStatsMatchID еще
|
||||||
|
// не получил и затем исчез.
|
||||||
|
err = fmt.Errorf("1xBet match %s has empty OnexbetStatsMatchID", onexbetMatchID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
matchResult, err := onexbet.GetTeamMatchResult(match.OnexbetStatsMatchID)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("onexbet.GetTeamMatchResult: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchResult.Status != onexbet.StatusMatchCompleted {
|
||||||
|
// Если матч не завершен - пропускаем. Предполагаем что матч просто
|
||||||
|
// временно исчез из лайва
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Матч завершен - загружаем прогнозы, чтобы выставить результаты
|
||||||
|
tips, err := s.model.ListMatchTips(match.MatchID)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("model.ListMatchTips: %s; matchID=%s", err, match.MatchID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tips) > 0 {
|
||||||
|
// На один матч может быть несколько прогнозов, поэтому важно знать что результаты
|
||||||
|
// всех прогнозов удалось записать в БД. Пока все результаты записать не удастся -
|
||||||
|
// это метод будет вызыватся снова и снова.
|
||||||
|
for _, tip := range tips {
|
||||||
|
err = s.trySetTipResult(tip, matchResult)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("trySetTipResult: %s; tipID=%d", err, tip.TipID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Результат всех прогнозов по матчу успешно записан либо прогнозов не было.
|
||||||
|
// В любом случае - удаляем матч из лайва.
|
||||||
|
s.removeMatchFromInplay(match, onexbetMatchID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (s *Daemon) removeMatchFromInplay(match *TeamMatch, onexbetMatchID string) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
delete(s.onexbetMatchToTeamMatch, onexbetMatchID)
|
||||||
|
match.IsInplay = false
|
||||||
|
|
||||||
|
// Матч убрали из inplay
|
||||||
|
s.publisher.Publish(
|
||||||
|
getChannel(match.SportID),
|
||||||
|
WSMessageInplayStatusChanged,
|
||||||
|
InplayStatusMessage{
|
||||||
|
MatchID: match.MatchID,
|
||||||
|
IsInplay: match.IsInplay,
|
||||||
|
IsLinkedToOnexbet: match.IsLinkedToOnexbet,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WhyNot struct {
|
||||||
|
MatchID string `json:"matchID"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
HomeGoals int `json:"homeGoals"`
|
||||||
|
AwayGoals int `json:"awayGoals"`
|
||||||
|
GamePace float64 `json:"gamePace"`
|
||||||
|
GamePaceFrame int `json:"gamePaceFrame"` // count from Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
//func (s *Daemon) onOnexbetTennisLiveMatchData(conn *ws.Conn, upd onexbet.TeamLiveMatchData) {
|
||||||
|
// s.mutex.Lock()
|
||||||
|
// defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
// s.logger.Printf("tennis MatchData: %s\n", upd.MatchID)
|
||||||
|
|
||||||
|
//}
|
||||||
|
/*
|
||||||
|
type TeamLiveMatchData struct {
|
||||||
|
MatchID string `json:"matchID"`
|
||||||
|
HomeAttacks int `json:"homeAttacks"`
|
||||||
|
HomeDangerousAttacks int `json:"homeDangerousAttacks"`
|
||||||
|
HomeShotsOnTarget int `json:"homeShotsOnTarget"`
|
||||||
|
HomeShotsOffTarget int `json:"homeShotsOffTarget"`
|
||||||
|
AwayAttacks int `json:"awayAttacks"`
|
||||||
|
AwayDangerousAttacks int `json:"awayDangerousAttacks"`
|
||||||
|
AwayShotsOnTarget int `json:"awayShotsOnTarget"`
|
||||||
|
AwayShotsOffTarget int `json:"awayShotsOffTarget"`
|
||||||
|
CurrentTime int `json:"currentTime"`
|
||||||
|
HomeGoals int `json:"homeGoals"`
|
||||||
|
AwayGoals int `json:"awayGoals"`
|
||||||
|
Winner *Winner3Way `json:"winner"`
|
||||||
|
Total Total `json:"total"`
|
||||||
|
Handicap Handicap `json:"handicap"`
|
||||||
|
IndividualTotalHome Total `json:"individualTotalHome"`
|
||||||
|
IndividualTotalAway Total `json:"individualTotalAway"`
|
||||||
|
H1 Half `json:"h1"` // 1st half
|
||||||
|
// tennis
|
||||||
|
Winner2Way *Winner2Way `json:"winner2way"`
|
||||||
|
|
||||||
|
// score
|
||||||
|
Score Score `json:"score"`
|
||||||
|
ScoreByPeriods map[int]Score `json:"scoreByPeriods"`
|
||||||
|
CurrentPeriod int `json:"currentPeriod"`
|
||||||
|
Periods map[int]Period `json:"periods"`
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func swapTeamLiveMatchData(d onexbet.TeamLiveMatchData) onexbet.TeamLiveMatchData {
|
||||||
|
d.AwayAttacks, d.HomeAttacks = d.HomeAttacks, d.AwayAttacks
|
||||||
|
d.AwayDangerousAttacks, d.HomeDangerousAttacks = d.HomeDangerousAttacks, d.AwayDangerousAttacks
|
||||||
|
d.AwayShotsOnTarget, d.HomeShotsOnTarget = d.HomeShotsOnTarget, d.AwayShotsOnTarget
|
||||||
|
d.AwayShotsOffTarget, d.HomeShotsOffTarget = d.HomeShotsOffTarget, d.AwayShotsOffTarget
|
||||||
|
d.AwayGoals, d.HomeGoals = d.HomeGoals, d.AwayGoals
|
||||||
|
|
||||||
|
if d.Winner != nil {
|
||||||
|
d.Winner.Away, d.Winner.Home = d.Winner.Home, d.Winner.Away
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Winner2Way != nil {
|
||||||
|
d.Winner2Way.Away, d.Winner2Way.Home = d.Winner2Way.Home, d.Winner2Way.Away
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Handicap.Away, d.Handicap.Home = d.Handicap.Home, d.Handicap.Away
|
||||||
|
d.IndividualTotalAway, d.IndividualTotalHome = d.IndividualTotalHome, d.IndividualTotalAway
|
||||||
|
|
||||||
|
d.H1.IndividualTotalAway, d.H1.IndividualTotalHome = d.H1.IndividualTotalHome, d.H1.IndividualTotalAway
|
||||||
|
|
||||||
|
d.Score.Away, d.Score.Home = d.Score.Home, d.Score.Away
|
||||||
|
|
||||||
|
for periodNumber, score := range d.ScoreByPeriods {
|
||||||
|
score.Away, score.Home = score.Home, score.Away
|
||||||
|
|
||||||
|
d.ScoreByPeriods[periodNumber] = score
|
||||||
|
}
|
||||||
|
|
||||||
|
for periodNumber, period := range d.Periods {
|
||||||
|
period.Handicap.Away, period.Handicap.Home = period.Handicap.Home, period.Handicap.Away
|
||||||
|
|
||||||
|
d.Periods[periodNumber] = period
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Daemon) onOnexbetTeamLiveMatchData(conn *ws.Conn, upd onexbet.TeamLiveMatchData) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.logger.Printf("MatchData: %s\n", upd.MatchID)
|
||||||
|
|
||||||
|
// маппинг -
|
||||||
|
match, ok := s.onexbetMatchToTeamMatch[upd.MatchID]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if match.IsTeamsSwapped {
|
||||||
|
upd = swapTeamLiveMatchData(upd)
|
||||||
|
}
|
||||||
|
|
||||||
|
//if match.SportID == flashscore.Handball {
|
||||||
|
//x, _ := json.MarshalIndent(upd, "", " ")
|
||||||
|
//fmt.Printf("MatchID: %s (1xbet MatchID: %s), Time: %d, HomeGoals: %d, AwayGoals: %d\n",
|
||||||
|
// match.MatchID, upd.MatchID, upd.CurrentTime, upd.HomeGoals, upd.AwayGoals)
|
||||||
|
//}
|
||||||
|
|
||||||
|
channel := getChannel(match.SportID)
|
||||||
|
|
||||||
|
//s.logger.Printf("Onexbet TeamLiveMatchData: sportID %d, channel %s\n", match.SportID, channel)
|
||||||
|
|
||||||
|
var (
|
||||||
|
triggeredStrategyIDs []int64
|
||||||
|
comments []string
|
||||||
|
strategyIDs []int
|
||||||
|
)
|
||||||
|
|
||||||
|
for strategyID := range match.Strategies {
|
||||||
|
strategyIDs = append(strategyIDs, int(strategyID))
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Ints(strategyIDs)
|
||||||
|
|
||||||
|
for _, strategyID := range strategyIDs {
|
||||||
|
|
||||||
|
strategy := match.Strategies[int64(strategyID)]
|
||||||
|
// Если по стратегии уже дали прогноз
|
||||||
|
//if match.EndedStrategies[strategy.ID()] {
|
||||||
|
// continue
|
||||||
|
//}
|
||||||
|
|
||||||
|
whyNot, bet, code := strategy.GetTip(match, upd)
|
||||||
|
|
||||||
|
switch code {
|
||||||
|
case Waiting:
|
||||||
|
// pass
|
||||||
|
//s.logger.Printf("Waiting: %s\n", whyNot)
|
||||||
|
|
||||||
|
// Отправить в websocket
|
||||||
|
comments = append(comments, fmt.Sprintf("%s:<br>%s", strategy.ShortName(), whyNot))
|
||||||
|
|
||||||
|
case Unwatch:
|
||||||
|
s.logger.Printf("Unwatch: %s\n", whyNot)
|
||||||
|
|
||||||
|
triggeredStrategyIDs = append(triggeredStrategyIDs, strategy.ID())
|
||||||
|
|
||||||
|
comments = append(comments, fmt.Sprintf("%s:<br>%s", strategy.ShortName(), whyNot))
|
||||||
|
|
||||||
|
case Bet:
|
||||||
|
|
||||||
|
triggeredStrategyIDs = append(triggeredStrategyIDs, strategy.ID())
|
||||||
|
|
||||||
|
var err error
|
||||||
|
//var tip model.Tip
|
||||||
|
//var tipPrice float64
|
||||||
|
|
||||||
|
tip := model.Tip{
|
||||||
|
SportID: int64(match.SportID),
|
||||||
|
StrategyID: strategy.ID(),
|
||||||
|
MatchID: match.MatchID,
|
||||||
|
TipTime: time.Now().Unix(),
|
||||||
|
ChampID: match.Champ.ChampID,
|
||||||
|
ChampName: match.Champ.Name,
|
||||||
|
MatchName: fmt.Sprintf("%s - %s", match.Home.CanonicalName, match.Away.CanonicalName),
|
||||||
|
Market: bet.Market,
|
||||||
|
Side: bet.Side,
|
||||||
|
Param: bet.Param,
|
||||||
|
Price: bet.Price,
|
||||||
|
CurrentMatchTime: upd.CurrentTime,
|
||||||
|
OnexbetStatsMatchID: match.OnexbetStatsMatchID,
|
||||||
|
IsTeamsSwapped: match.IsTeamsSwapped,
|
||||||
|
}
|
||||||
|
|
||||||
|
champRatings, ok := s.champRatings[match.SportID]
|
||||||
|
if ok {
|
||||||
|
// обновляем рейтинги чемпионатов
|
||||||
|
key := ChampRatingKey{
|
||||||
|
ChampID: tip.ChampID,
|
||||||
|
StrategyID: tip.StrategyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
rating, ok := champRatings[key]
|
||||||
|
if ok {
|
||||||
|
//if (rating.Won+rating.Lost) >= 6 && rating.GetAccuracy() >= 75 {
|
||||||
|
if rating.GetAccuracy() >= 70 {
|
||||||
|
tip.IsRated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Printf("Bet: %#v\n", tip)
|
||||||
|
|
||||||
|
tip.TipID, err = s.model.AddTip(tip)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("model.AddTip: %s\n", err)
|
||||||
|
} else {
|
||||||
|
// Отправить в websocket
|
||||||
|
// Добавили новый Tip
|
||||||
|
s.publisher.Publish(channel, WSMessageTipsChanged, nil)
|
||||||
|
|
||||||
|
if match.SportID == flashscore.Football {
|
||||||
|
// Для футбола только рейтинговые чемпионаты
|
||||||
|
// Отправить в телеграм
|
||||||
|
if strategy.IsNotifyInTelegram() && tip.IsRated {
|
||||||
|
// Отправлять ли в телеграм
|
||||||
|
msg := fmt.Sprintf(strategy.GetTelegramTipPattern(), tip.TipID, strategy.ID(),
|
||||||
|
match.Zone.Name, match.Champ.Name,
|
||||||
|
match.Home.CanonicalName, match.Away.CanonicalName,
|
||||||
|
bet.Price)
|
||||||
|
|
||||||
|
telegramChatID, hasChat := sportToTelegramChatID[match.SportID]
|
||||||
|
if hasChat {
|
||||||
|
err = sendMessageToTelegramGroup(telegramChatID, msg)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("send tip to Telegram: %s\n", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.logger.Printf("Sport %d hasn't telegram chat\n", match.SportID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if match.SportID == flashscore.Handball {
|
||||||
|
if strategy.IsNotifyInTelegram() && tip.IsRated {
|
||||||
|
param, err := strconv.ParseFloat(bet.Param, 64)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("Wrong bet param: %s; param=%s\n", err, bet.Param)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
var betterParam1, betterParam2 float64
|
||||||
|
|
||||||
|
switch strategy.(type) {
|
||||||
|
case HandballFTOver, HandballP1Over:
|
||||||
|
betterParam1 = param - 1
|
||||||
|
betterParam2 = param - 2
|
||||||
|
|
||||||
|
case HandballFTUnder, HandballP1Under:
|
||||||
|
betterParam1 = param + 1
|
||||||
|
betterParam2 = param + 2
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("Unknown strategy %#v\n", strategy))
|
||||||
|
}
|
||||||
|
// Для гандбола отправляем все прогнозы
|
||||||
|
|
||||||
|
msg := fmt.Sprintf(strategy.GetTelegramTipPattern(), tip.TipID, strategy.ID(),
|
||||||
|
match.Zone.Name, match.Champ.Name,
|
||||||
|
match.Home.CanonicalName, match.Away.CanonicalName,
|
||||||
|
param, bet.Price, param, betterParam1, betterParam2)
|
||||||
|
|
||||||
|
telegramChatID, hasChat := sportToTelegramChatID[match.SportID]
|
||||||
|
if hasChat {
|
||||||
|
err = sendMessageToTelegramGroup(telegramChatID, msg)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("send tip to Telegram: %s\n", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.logger.Printf("Sport %d hasn't telegram chat\n", match.SportID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if match.SportID == flashscore.Tennis {
|
||||||
|
if strategy.IsNotifyInTelegram() {
|
||||||
|
msg := fmt.Sprintf(bet.TelegramMessage, tip.TipID)
|
||||||
|
|
||||||
|
telegramChatID, hasChat := sportToTelegramChatID[match.SportID]
|
||||||
|
if hasChat {
|
||||||
|
err = sendMessageToTelegramGroup(telegramChatID, msg)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("send tip to Telegram: %s\n", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.logger.Printf("Sport %d hasn't telegram chat\n", match.SportID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, strategyID := range triggeredStrategyIDs {
|
||||||
|
delete(match.Strategies, strategyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(match.Strategies) == 0 {
|
||||||
|
// Все стратегии сработали - отписываемся от матча
|
||||||
|
|
||||||
|
s.logger.Printf("Unwatch: %d, %s\n", match.SportID, upd.MatchID)
|
||||||
|
|
||||||
|
// Матч больше не интересен
|
||||||
|
delete(s.onexbetMatchToTeamMatch, upd.MatchID)
|
||||||
|
match.IsInplay = false
|
||||||
|
|
||||||
|
s.onexbetAPIClient.Call(api.CallReq{
|
||||||
|
FuncName: "unwatch",
|
||||||
|
In: onexbet.WatchReq{
|
||||||
|
SportID: flashscoreSportToOnexbetSport[match.SportID],
|
||||||
|
MatchID: upd.MatchID, // onexbetMatchID
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if match.SportID == flashscore.Tennis {
|
||||||
|
if len(comments) > 0 {
|
||||||
|
s.publisher.Publish(
|
||||||
|
channel,
|
||||||
|
WSMessageWhyNot,
|
||||||
|
WhyNot{
|
||||||
|
MatchID: match.MatchID,
|
||||||
|
Comment: strings.Join(comments, "<br><br>"),
|
||||||
|
Time: "",
|
||||||
|
HomeGoals: upd.HomeGoals,
|
||||||
|
AwayGoals: upd.AwayGoals,
|
||||||
|
GamePace: match.GamePace,
|
||||||
|
GamePaceFrame: match.GamePaceFrame,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(comments) > 0 {
|
||||||
|
s.publisher.Publish(
|
||||||
|
channel,
|
||||||
|
WSMessageWhyNot,
|
||||||
|
WhyNot{
|
||||||
|
MatchID: match.MatchID,
|
||||||
|
Comment: strings.Join(comments, "<br><br>"),
|
||||||
|
Time: fmt.Sprintf("%s", time.Duration(upd.CurrentTime)*time.Second),
|
||||||
|
HomeGoals: upd.HomeGoals,
|
||||||
|
AwayGoals: upd.AwayGoals,
|
||||||
|
GamePace: match.GamePace,
|
||||||
|
GamePaceFrame: match.GamePaceFrame,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
var tipTextPattern string
|
||||||
|
|
||||||
|
switch strategyID {
|
||||||
|
case 1:
|
||||||
|
tipTextPattern = tipTextAlgo1Pattern
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
tipTextPattern = tipTextAlgo2Pattern
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
tipTextPattern = tipTextAlgo3Pattern
|
||||||
|
|
||||||
|
case 100:
|
||||||
|
tipTextPattern = tipTextAlgo100Pattern
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
switch strategyID {
|
||||||
|
case 1:
|
||||||
|
var price float64
|
||||||
|
for _, over := range upd.H1.Total.Over {
|
||||||
|
if over.Param == "0.5" {
|
||||||
|
price = over.Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tipPrice = price
|
||||||
|
|
||||||
|
tip = model.Tip{
|
||||||
|
SportID: SportFootball,
|
||||||
|
StrategyID: int64(strategyID),
|
||||||
|
MatchID: match.MatchID,
|
||||||
|
TipTime: time.Now().Unix(),
|
||||||
|
ChampName: match.Champ.Name,
|
||||||
|
MatchName: fmt.Sprintf("%s - %s", match.Home.CanonicalName, match.Away.CanonicalName),
|
||||||
|
Market: MarketTotal1stHalf,
|
||||||
|
Side: SideOver,
|
||||||
|
Param: "0.5",
|
||||||
|
Price: price,
|
||||||
|
CurrentMatchTime: upd.CurrentTime,
|
||||||
|
OnexbetStatsMatchID: match.OnexbetStatsMatchID,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
var price float64
|
||||||
|
for _, over := range upd.Total.Over {
|
||||||
|
if over.Param == "0.5" {
|
||||||
|
price = over.Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tipPrice = price
|
||||||
|
|
||||||
|
tip = model.Tip{
|
||||||
|
SportID: SportFootball,
|
||||||
|
StrategyID: int64(strategyID),
|
||||||
|
MatchID: match.MatchID,
|
||||||
|
TipTime: time.Now().Unix(),
|
||||||
|
ChampName: match.Champ.Name,
|
||||||
|
MatchName: fmt.Sprintf("%s - %s", match.Home.CanonicalName, match.Away.CanonicalName),
|
||||||
|
Market: MarketTotal,
|
||||||
|
Side: SideOver,
|
||||||
|
Param: "0.5",
|
||||||
|
Price: price,
|
||||||
|
CurrentMatchTime: upd.CurrentTime,
|
||||||
|
OnexbetStatsMatchID: match.OnexbetStatsMatchID,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
var price float64
|
||||||
|
for _, over := range upd.Total.Over {
|
||||||
|
if over.Param == "1.5" {
|
||||||
|
price = over.Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tipPrice = price
|
||||||
|
|
||||||
|
tip = model.Tip{
|
||||||
|
SportID: SportFootball,
|
||||||
|
StrategyID: int64(strategyID),
|
||||||
|
MatchID: match.MatchID,
|
||||||
|
TipTime: time.Now().Unix(),
|
||||||
|
ChampName: match.Champ.Name,
|
||||||
|
MatchName: fmt.Sprintf("%s - %s", match.Home.CanonicalName, match.Away.CanonicalName),
|
||||||
|
Market: MarketTotal,
|
||||||
|
Side: SideOver,
|
||||||
|
Param: "1.5",
|
||||||
|
Price: price,
|
||||||
|
CurrentMatchTime: upd.CurrentTime,
|
||||||
|
OnexbetStatsMatchID: match.OnexbetStatsMatchID,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 100:
|
||||||
|
var price float64
|
||||||
|
for _, over := range upd.H1.Total.Over {
|
||||||
|
if over.Param == "0.5" {
|
||||||
|
price = over.Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tipPrice = price
|
||||||
|
|
||||||
|
tip = model.Tip{
|
||||||
|
SportID: SportFootball,
|
||||||
|
StrategyID: int64(strategyID),
|
||||||
|
MatchID: match.MatchID,
|
||||||
|
TipTime: time.Now().Unix(),
|
||||||
|
ChampName: match.Champ.Name,
|
||||||
|
MatchName: fmt.Sprintf("%s - %s", match.Home.CanonicalName, match.Away.CanonicalName),
|
||||||
|
Market: MarketTotal1stHalf,
|
||||||
|
Side: SideOver,
|
||||||
|
Param: "0.5",
|
||||||
|
Price: price,
|
||||||
|
CurrentMatchTime: upd.CurrentTime,
|
||||||
|
OnexbetStatsMatchID: match.OnexbetStatsMatchID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
250
daemon/over05.go
Executable file
250
daemon/over05.go
Executable file
@@ -0,0 +1,250 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
/*
|
||||||
|
Футбольная стратегия.
|
||||||
|
ТБ 0.5 в 1м тайме
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gordenko.dev/dima/flashscore"
|
||||||
|
"gordenko.dev/dima/onexbet"
|
||||||
|
"gordenko.dev/dima/tipper/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Алгоритм 2
|
||||||
|
ТБ 0.5 в Матче
|
||||||
|
|
||||||
|
До матча:
|
||||||
|
ТБ 2.5 (в матче) <=1.7
|
||||||
|
В 75% игр был гол
|
||||||
|
|
||||||
|
В Лайве:
|
||||||
|
Сумма атак обычных и опасных >= 135
|
||||||
|
6 ударов в сторону ворот OFF TARGET
|
||||||
|
4 удара в створ ON TARGET
|
||||||
|
|
||||||
|
До 70 минуты
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Over05 struct {
|
||||||
|
id int64
|
||||||
|
notifyInTelegram bool
|
||||||
|
isChampAccepted func(string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOver05(opt StrategyOptions) Over05 {
|
||||||
|
if opt.ID == 0 {
|
||||||
|
panic("StrategyID not defined")
|
||||||
|
}
|
||||||
|
s := Over05{}
|
||||||
|
s.id = opt.ID
|
||||||
|
s.notifyInTelegram = opt.NotifyInTelegram
|
||||||
|
s.isChampAccepted = opt.IsChampAccepted
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over05) ID() int64 {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over05) ShortName() string {
|
||||||
|
return fmt.Sprintf("#%d FT Over 0.5", s.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over05) IsNotifyInTelegram() bool {
|
||||||
|
return s.notifyInTelegram
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over05) GetTelegramTipPattern() string {
|
||||||
|
return `Сигнал # %d.
|
||||||
|
Будет ГОЛ!
|
||||||
|
Алгоритм %d
|
||||||
|
Тотал Больше 0.5
|
||||||
|
Футбол. %s. %s
|
||||||
|
%s - %s
|
||||||
|
Коэф. %.3f`
|
||||||
|
}
|
||||||
|
|
||||||
|
type isInterestingNotesOver05 struct {
|
||||||
|
PriceOver25 float64 `json:"Over 2.5 price"`
|
||||||
|
MatchesAnalysed int `json:"matchesAnalysed"`
|
||||||
|
HasGoalInMatches int `json:"hasGoalInMatches"`
|
||||||
|
GoalsPercent float64 `json:"goalsPercent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over05) IsInteresting(report *TeamMatch) (notes interface{}, _ bool) {
|
||||||
|
var (
|
||||||
|
priceOver25 float64
|
||||||
|
foundPriceOver25 bool
|
||||||
|
//f flashscore.TotalOffer
|
||||||
|
)
|
||||||
|
for _, total := range report.Odds.FullTimeTotal {
|
||||||
|
if total.Total != "2.5" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 2.5
|
||||||
|
for _, offer := range total.Offers {
|
||||||
|
if offer.Bookmaker == flashscore.Bookmaker1xBet {
|
||||||
|
priceOver25 = offer.Over
|
||||||
|
foundPriceOver25 = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundPriceOver25 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// 1xBet не нашли
|
||||||
|
// Берем первый коэф.
|
||||||
|
if len(total.Offers) > 0 {
|
||||||
|
priceOver25 = total.Offers[0].Over
|
||||||
|
foundPriceOver25 = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if priceOver25 > 1.7 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
matchCount int
|
||||||
|
hasGoalsInMatchCount int
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, match := range report.HomeTeamMatches {
|
||||||
|
if match.HomeGoals > 0 || match.AwayGoals > 0 {
|
||||||
|
hasGoalsInMatchCount++
|
||||||
|
}
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range report.AwayTeamMatches {
|
||||||
|
if match.HomeGoals > 0 || match.AwayGoals > 0 {
|
||||||
|
hasGoalsInMatchCount++
|
||||||
|
}
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range report.H2H {
|
||||||
|
if match.HomeGoals > 0 || match.AwayGoals > 0 {
|
||||||
|
hasGoalsInMatchCount++
|
||||||
|
}
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchCount == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchCount - 100%
|
||||||
|
// goalsInMatchCount - x%
|
||||||
|
|
||||||
|
goalsPercent := float64(hasGoalsInMatchCount*100) / float64(matchCount)
|
||||||
|
|
||||||
|
if goalsPercent < 75 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := isInterestingNotesOver05{
|
||||||
|
PriceOver25: priceOver25,
|
||||||
|
MatchesAnalysed: matchCount,
|
||||||
|
HasGoalInMatches: hasGoalsInMatchCount,
|
||||||
|
GoalsPercent: goalsPercent,
|
||||||
|
}
|
||||||
|
|
||||||
|
//buf, _ := json.MarshalIndent(obj, "", " ")
|
||||||
|
|
||||||
|
return obj, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over05) GetTip(match *TeamMatch, upd onexbet.TeamLiveMatchData) (_ string, _ *BetDetails, _ TipCode) {
|
||||||
|
if upd.CurrentTime == 0 {
|
||||||
|
// ВАЖНО!
|
||||||
|
// Матч еще не начался. Избегаем деления на 0 при вычислении attacksRatio
|
||||||
|
return "матч не начался", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
// Прогнозы даем до 70 минуты включительно.
|
||||||
|
maxTime := 70 * 60
|
||||||
|
|
||||||
|
if upd.CurrentTime > maxTime {
|
||||||
|
return "70 минут уже отыграли", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if upd.HomeGoals > 0 || upd.AwayGoals > 0 {
|
||||||
|
// Если гол уже забили - выходим
|
||||||
|
return "гол уже забит", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем тотал Больше 0.5
|
||||||
|
// Минимальный курс
|
||||||
|
minPrice := 1.5
|
||||||
|
var (
|
||||||
|
minPriceCheckPassed bool
|
||||||
|
currentPrice float64
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, over := range upd.Total.Over {
|
||||||
|
if over.Param == "0.5" {
|
||||||
|
if over.Price < minPrice {
|
||||||
|
return fmt.Sprintf("коэф. %.2f < %.2f", over.Price, minPrice), nil, Waiting
|
||||||
|
}
|
||||||
|
currentPrice = over.Price
|
||||||
|
minPriceCheckPassed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !minPriceCheckPassed {
|
||||||
|
return "тотал 0.5 не найден", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
attacks := upd.HomeAttacks + upd.AwayAttacks + upd.HomeDangerousAttacks + upd.AwayDangerousAttacks
|
||||||
|
shotsOffTarget := upd.HomeShotsOffTarget + upd.AwayShotsOffTarget
|
||||||
|
shotsOnTarget := upd.HomeShotsOnTarget + upd.AwayShotsOnTarget
|
||||||
|
|
||||||
|
if attacks < 135 {
|
||||||
|
return fmt.Sprintf("%d атак < 135", attacks), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if shotsOffTarget < 6 {
|
||||||
|
return fmt.Sprintf("%d shotsOffTarget < 6", shotsOffTarget), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if shotsOnTarget < 4 {
|
||||||
|
return fmt.Sprintf("%d shotsOnTarget < 4", shotsOnTarget), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", &BetDetails{
|
||||||
|
Market: MarketTotal,
|
||||||
|
Side: SideOver,
|
||||||
|
Param: "0.5",
|
||||||
|
Price: currentPrice,
|
||||||
|
}, Bet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over05) GetResult(tip model.Tip, stats onexbet.TeamMatchResult) (_ *TipResult) {
|
||||||
|
//buf, _ := json.MarshalIndent(stats, "", " ")
|
||||||
|
//fmt.Printf("%s\n", buf)
|
||||||
|
|
||||||
|
if stats.Status != onexbet.StatusMatchCompleted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := new(TipResult)
|
||||||
|
goals := stats.HomePoints + stats.AwayPoints
|
||||||
|
if goals > 0 {
|
||||||
|
res.Status = model.Won
|
||||||
|
} else {
|
||||||
|
res.Status = model.Lost
|
||||||
|
}
|
||||||
|
res.Result = fmt.Sprintf("счет: %d-%d", stats.HomePoints, stats.AwayPoints)
|
||||||
|
return res
|
||||||
|
}
|
||||||
275
daemon/over15.go
Executable file
275
daemon/over15.go
Executable file
@@ -0,0 +1,275 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
/*
|
||||||
|
Футбольная стратегия.
|
||||||
|
ТБ 0.5 в 1м тайме
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gordenko.dev/dima/flashscore"
|
||||||
|
"gordenko.dev/dima/onexbet"
|
||||||
|
"gordenko.dev/dima/tipper/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Алгоритм 3
|
||||||
|
ТБ 1.5 в Матче
|
||||||
|
|
||||||
|
До матча:
|
||||||
|
ТБ 2.5 (в матче) <=1.7
|
||||||
|
В 75% игр был гол
|
||||||
|
КФ фаворита <=1.4
|
||||||
|
|
||||||
|
В Лайве:
|
||||||
|
Сумма атак обычных и опасных >= 125
|
||||||
|
6 ударов в сторону ворот
|
||||||
|
4 удара в створ
|
||||||
|
|
||||||
|
До 70 минуты
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Over15 struct {
|
||||||
|
id int64
|
||||||
|
notifyInTelegram bool
|
||||||
|
isChampAccepted func(string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOver15(opt StrategyOptions) Over15 {
|
||||||
|
if opt.ID == 0 {
|
||||||
|
panic("StrategyID not defined")
|
||||||
|
}
|
||||||
|
s := Over15{}
|
||||||
|
s.id = opt.ID
|
||||||
|
s.notifyInTelegram = opt.NotifyInTelegram
|
||||||
|
s.isChampAccepted = opt.IsChampAccepted
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over15) ID() int64 {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over15) ShortName() string {
|
||||||
|
return fmt.Sprintf("#%d FT Over 1.5", s.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over15) IsNotifyInTelegram() bool {
|
||||||
|
return s.notifyInTelegram
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over15) GetTelegramTipPattern() string {
|
||||||
|
return `Сигнал # %d.
|
||||||
|
Будет 2й ГОЛ!
|
||||||
|
Алгоритм %d
|
||||||
|
Тотал Больше 1.5
|
||||||
|
Футбол. %s. %s
|
||||||
|
%s - %s
|
||||||
|
Коэф. %.3f`
|
||||||
|
}
|
||||||
|
|
||||||
|
type isInterestingNotesOver15 struct {
|
||||||
|
PriceOver25 float64 `json:"Over 2.5 price"`
|
||||||
|
MatchesAnalysed int `json:"matchesAnalysed"`
|
||||||
|
HasGoalInMatches int `json:"hasGoalInMatches"`
|
||||||
|
GoalsPercent float64 `json:"goalsPercent"`
|
||||||
|
FavoritePrice float64 `json:"favoritePrice"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over15) IsInteresting(report *TeamMatch) (notes interface{}, _ bool) {
|
||||||
|
var favoritePrice float64 = 2
|
||||||
|
|
||||||
|
for _, offer := range report.Odds.Winner {
|
||||||
|
if offer.Home < favoritePrice && offer.Home > 1.01 {
|
||||||
|
favoritePrice = offer.Home
|
||||||
|
}
|
||||||
|
|
||||||
|
if offer.Away < favoritePrice && offer.Away > 1.01 {
|
||||||
|
favoritePrice = offer.Away
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if favoritePrice > 1.4 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
priceOver25 float64
|
||||||
|
foundPriceOver25 bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, total := range report.Odds.FullTimeTotal {
|
||||||
|
if total.Total != "2.5" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 2.5
|
||||||
|
for _, offer := range total.Offers {
|
||||||
|
if offer.Bookmaker == flashscore.Bookmaker1xBet {
|
||||||
|
priceOver25 = offer.Over
|
||||||
|
foundPriceOver25 = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundPriceOver25 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// 1xBet не нашли
|
||||||
|
// Берем первый коэф.
|
||||||
|
if len(total.Offers) > 0 {
|
||||||
|
priceOver25 = total.Offers[0].Over
|
||||||
|
foundPriceOver25 = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if priceOver25 > 1.7 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
matchCount int
|
||||||
|
hasGoalsInMatchCount int
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, match := range report.HomeTeamMatches {
|
||||||
|
if match.HomeGoals > 0 || match.AwayGoals > 0 {
|
||||||
|
hasGoalsInMatchCount++
|
||||||
|
}
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range report.AwayTeamMatches {
|
||||||
|
if match.HomeGoals > 0 || match.AwayGoals > 0 {
|
||||||
|
hasGoalsInMatchCount++
|
||||||
|
}
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range report.H2H {
|
||||||
|
if match.HomeGoals > 0 || match.AwayGoals > 0 {
|
||||||
|
hasGoalsInMatchCount++
|
||||||
|
}
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchCount == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchCount - 100%
|
||||||
|
// goalsInMatchCount - x%
|
||||||
|
|
||||||
|
goalsPercent := float64(hasGoalsInMatchCount*100) / float64(matchCount)
|
||||||
|
|
||||||
|
if goalsPercent < 75 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := isInterestingNotesOver15{
|
||||||
|
PriceOver25: priceOver25,
|
||||||
|
MatchesAnalysed: matchCount,
|
||||||
|
HasGoalInMatches: hasGoalsInMatchCount,
|
||||||
|
GoalsPercent: goalsPercent,
|
||||||
|
FavoritePrice: favoritePrice,
|
||||||
|
}
|
||||||
|
|
||||||
|
//buf, _ := json.MarshalIndent(obj, "", " ")
|
||||||
|
|
||||||
|
return obj, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over15) GetTip(match *TeamMatch, upd onexbet.TeamLiveMatchData) (_ string, _ *BetDetails, _ TipCode) {
|
||||||
|
if upd.CurrentTime == 0 {
|
||||||
|
// ВАЖНО!
|
||||||
|
// Матч еще не начался. Избегаем деления на 0 при вычислении attacksRatio
|
||||||
|
return "матч не начался", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
// Прогнозы даем до 70 минуты включительно.
|
||||||
|
maxTime := 70 * 60
|
||||||
|
|
||||||
|
if upd.CurrentTime > maxTime {
|
||||||
|
return "70 минут уже отыграли", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
goals := upd.HomeGoals + upd.AwayGoals
|
||||||
|
|
||||||
|
if goals > 1 {
|
||||||
|
// Если 2 гола уже забили - выходим
|
||||||
|
return "уже забили более 1 гола", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if goals == 0 {
|
||||||
|
return "счет 0:0", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем тотал Больше 1.5
|
||||||
|
// Минимальный курс
|
||||||
|
minPrice := 1.5
|
||||||
|
var (
|
||||||
|
minPriceCheckPassed bool
|
||||||
|
currentPrice float64
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, over := range upd.Total.Over {
|
||||||
|
if over.Param == "1.5" {
|
||||||
|
if over.Price < minPrice {
|
||||||
|
return fmt.Sprintf("коэф. %.2f < %.2f", over.Price, minPrice), nil, Waiting
|
||||||
|
}
|
||||||
|
currentPrice = over.Price
|
||||||
|
minPriceCheckPassed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !minPriceCheckPassed {
|
||||||
|
return "тотал 1.5 не найден", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
attacks := upd.HomeAttacks + upd.AwayAttacks + upd.HomeDangerousAttacks + upd.AwayDangerousAttacks
|
||||||
|
shotsOffTarget := upd.HomeShotsOffTarget + upd.AwayShotsOffTarget
|
||||||
|
shotsOnTarget := upd.HomeShotsOnTarget + upd.AwayShotsOnTarget
|
||||||
|
|
||||||
|
if attacks < 125 {
|
||||||
|
return fmt.Sprintf("%d атак < 125", attacks), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if shotsOffTarget < 6 {
|
||||||
|
return fmt.Sprintf("%d shotsOffTarget < 6", shotsOffTarget), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if shotsOnTarget < 4 {
|
||||||
|
return fmt.Sprintf("%d shotsOnTarget < 4", shotsOnTarget), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", &BetDetails{
|
||||||
|
Market: MarketTotal,
|
||||||
|
Side: SideOver,
|
||||||
|
Param: "1.5",
|
||||||
|
Price: currentPrice,
|
||||||
|
}, Bet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Over15) GetResult(tip model.Tip, stats onexbet.TeamMatchResult) (_ *TipResult) {
|
||||||
|
//buf, _ := json.MarshalIndent(stats, "", " ")
|
||||||
|
//fmt.Printf("%s\n", buf)
|
||||||
|
|
||||||
|
if stats.Status != onexbet.StatusMatchCompleted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := new(TipResult)
|
||||||
|
goals := stats.HomePoints + stats.AwayPoints
|
||||||
|
if goals > 1 {
|
||||||
|
res.Status = model.Won
|
||||||
|
} else {
|
||||||
|
res.Status = model.Lost
|
||||||
|
}
|
||||||
|
res.Result = fmt.Sprintf("счет: %d-%d", stats.HomePoints, stats.AwayPoints)
|
||||||
|
return res
|
||||||
|
}
|
||||||
249
daemon/p1over05.go
Executable file
249
daemon/p1over05.go
Executable file
@@ -0,0 +1,249 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
/*
|
||||||
|
Футбольная стратегия.
|
||||||
|
ТБ 0.5 в 1м тайме
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gordenko.dev/dima/flashscore"
|
||||||
|
"gordenko.dev/dima/onexbet"
|
||||||
|
"gordenko.dev/dima/tipper/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type P1Over05 struct {
|
||||||
|
id int64
|
||||||
|
notifyInTelegram bool
|
||||||
|
isChampAccepted func(string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewP1Over05(opt StrategyOptions) P1Over05 {
|
||||||
|
if opt.ID == 0 {
|
||||||
|
panic("StrategyID not defined")
|
||||||
|
}
|
||||||
|
s := P1Over05{}
|
||||||
|
s.id = opt.ID
|
||||||
|
s.notifyInTelegram = opt.NotifyInTelegram
|
||||||
|
s.isChampAccepted = opt.IsChampAccepted
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s P1Over05) ID() int64 {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s P1Over05) ShortName() string {
|
||||||
|
return fmt.Sprintf("#%d H1 Over 0.5", s.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s P1Over05) IsNotifyInTelegram() bool {
|
||||||
|
return s.notifyInTelegram
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s P1Over05) GetTelegramTipPattern() string {
|
||||||
|
return `Сигнал # %d.
|
||||||
|
Будет ГОЛ!
|
||||||
|
Алгоритм %d
|
||||||
|
Первый тайм, Тотал Больше 0.5
|
||||||
|
Футбол. %s. %s
|
||||||
|
%s - %s
|
||||||
|
Коэф. %.3f`
|
||||||
|
}
|
||||||
|
|
||||||
|
type isInterestingNotesP1Over05 struct {
|
||||||
|
PriceOver25 float64 `json:"Over 2.5 price"`
|
||||||
|
MatchesAnalysed int `json:"matchesAnalysed"`
|
||||||
|
HasGoalInP1Matches int `json:"hasGoalInP1Matches"`
|
||||||
|
P1GoalsPercent float64 `json:"p1GoalsPercent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s P1Over05) IsInteresting(report *TeamMatch) (notes interface{}, _ bool) {
|
||||||
|
var (
|
||||||
|
priceOver25 float64
|
||||||
|
foundPriceOver25 bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, total := range report.Odds.FullTimeTotal {
|
||||||
|
if total.Total != "2.5" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 2.5
|
||||||
|
for _, offer := range total.Offers {
|
||||||
|
if offer.Bookmaker == flashscore.Bookmaker1xBet {
|
||||||
|
priceOver25 = offer.Over
|
||||||
|
foundPriceOver25 = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundPriceOver25 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// 1xBet не нашли
|
||||||
|
// Берем первый коэф.
|
||||||
|
if len(total.Offers) > 0 {
|
||||||
|
priceOver25 = total.Offers[0].Over
|
||||||
|
foundPriceOver25 = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if priceOver25 > 1.58 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
hasStatsMatchCount int
|
||||||
|
hasGoalsInP1MatchCount int
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, match := range report.HomeTeamMatches {
|
||||||
|
if match.HasScoreByPeriods {
|
||||||
|
hasStatsMatchCount++
|
||||||
|
|
||||||
|
if match.P1HomeGoals > 0 || match.P1AwayGoals > 0 {
|
||||||
|
hasGoalsInP1MatchCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range report.AwayTeamMatches {
|
||||||
|
if match.HasScoreByPeriods {
|
||||||
|
hasStatsMatchCount++
|
||||||
|
|
||||||
|
if match.P1HomeGoals > 0 || match.P1AwayGoals > 0 {
|
||||||
|
hasGoalsInP1MatchCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range report.H2H {
|
||||||
|
if match.HasScoreByPeriods {
|
||||||
|
hasStatsMatchCount++
|
||||||
|
|
||||||
|
if match.P1HomeGoals > 0 || match.P1AwayGoals > 0 {
|
||||||
|
hasGoalsInP1MatchCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasStatsMatchCount == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasStatsMatchCount - 100%
|
||||||
|
// hasGoalsInP1MatchCount - x%
|
||||||
|
|
||||||
|
p1GoalsPercent := float64(hasGoalsInP1MatchCount*100) / float64(hasStatsMatchCount)
|
||||||
|
|
||||||
|
if p1GoalsPercent < 75 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := isInterestingNotesP1Over05{
|
||||||
|
PriceOver25: priceOver25,
|
||||||
|
MatchesAnalysed: hasStatsMatchCount,
|
||||||
|
HasGoalInP1Matches: hasGoalsInP1MatchCount,
|
||||||
|
P1GoalsPercent: p1GoalsPercent,
|
||||||
|
}
|
||||||
|
|
||||||
|
//buf, _ := json.MarshalIndent(obj, "", " ")
|
||||||
|
|
||||||
|
return obj, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s P1Over05) GetTip(match *TeamMatch, upd onexbet.TeamLiveMatchData) (_ string, _ *BetDetails, _ TipCode) {
|
||||||
|
if upd.CurrentTime == 0 {
|
||||||
|
// ВАЖНО!
|
||||||
|
// Матч еще не начался. Избегаем деления на 0 при вычислении attacksRatio
|
||||||
|
return "матч не начался", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
// Прогнозы даем до 20 минуты включительно.
|
||||||
|
maxTime := 20 * 60
|
||||||
|
|
||||||
|
if upd.CurrentTime > maxTime {
|
||||||
|
return "20 минут уже отыграли", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if upd.HomeGoals > 0 || upd.AwayGoals > 0 {
|
||||||
|
// Если гол уже забили - выходим
|
||||||
|
return "гол уже забили", nil, Unwatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем тотал Больше 0.5
|
||||||
|
// Минимальный курс
|
||||||
|
minPrice := 1.5
|
||||||
|
var (
|
||||||
|
minPriceCheckPassed bool
|
||||||
|
currentPrice float64
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, over := range upd.H1.Total.Over {
|
||||||
|
if over.Param == "0.5" {
|
||||||
|
if over.Price < minPrice {
|
||||||
|
return fmt.Sprintf("коэф. %.2f < %.2f", over.Price, minPrice), nil, Waiting
|
||||||
|
}
|
||||||
|
currentPrice = over.Price
|
||||||
|
minPriceCheckPassed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !minPriceCheckPassed {
|
||||||
|
return "тотал 0.5 не найден", nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
// соотношение атак по времени >= 2.1 (за 10 минут от 21 атаки)
|
||||||
|
attacks := upd.HomeAttacks + upd.AwayAttacks
|
||||||
|
shotsOffTarget := upd.HomeShotsOffTarget + upd.AwayShotsOffTarget
|
||||||
|
|
||||||
|
attacksRatio := float64(attacks*60) / float64(upd.CurrentTime)
|
||||||
|
|
||||||
|
if attacksRatio < 2.1 {
|
||||||
|
return fmt.Sprintf("отношение атак ко времени %.2f < 2.1", attacksRatio), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if shotsOffTarget < 3 {
|
||||||
|
return fmt.Sprintf("%d shotsOffTarget < 6", shotsOffTarget), nil, Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", &BetDetails{
|
||||||
|
Market: MarketTotalH1,
|
||||||
|
Side: SideOver,
|
||||||
|
Param: "0.5",
|
||||||
|
Price: currentPrice,
|
||||||
|
}, Bet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s P1Over05) GetResult(tip model.Tip, stats onexbet.TeamMatchResult) (_ *TipResult) {
|
||||||
|
//buf, _ := json.MarshalIndent(stats, "", " ")
|
||||||
|
//fmt.Printf("%s\n", buf)
|
||||||
|
|
||||||
|
if stats.Status != onexbet.StatusMatchCompleted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, ok := stats.ScoreByPeriods[1]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//if stats.H1 == nil || stats.H2 == nil {
|
||||||
|
//return
|
||||||
|
//}
|
||||||
|
|
||||||
|
res := new(TipResult)
|
||||||
|
h1Goals := h1.HomePoints + h1.AwayPoints
|
||||||
|
if h1Goals > 0 {
|
||||||
|
res.Status = model.Won
|
||||||
|
} else {
|
||||||
|
res.Status = model.Lost
|
||||||
|
}
|
||||||
|
res.Result = fmt.Sprintf("1й тайм: %d-%d", h1.HomePoints, h1.AwayPoints)
|
||||||
|
return res
|
||||||
|
}
|
||||||
165
daemon/pages.go
Normal file
165
daemon/pages.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gordenko.dev/dima/flashscore"
|
||||||
|
"gordenko.dev/dima/timeutil"
|
||||||
|
"gordenko.dev/dima/tipper/model"
|
||||||
|
"gordenko.dev/dima/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Daemon) PageMatches(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
buf, _ := json.MarshalIndent(s.teamMatches, "", " ")
|
||||||
|
//reply.Render("matches", buf)
|
||||||
|
reply.Write(buf)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type TipView struct {
|
||||||
|
Time string
|
||||||
|
Name string
|
||||||
|
Champ string
|
||||||
|
Link string
|
||||||
|
Result string
|
||||||
|
Status model.TipStatus
|
||||||
|
Strategy string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DayTips struct {
|
||||||
|
Date string
|
||||||
|
Tips []TipView
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageTipsData struct {
|
||||||
|
Days []DayTips
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Daemon) PageHandballByDays(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
data, err := s.getPageTipsData(flashscore.Handball)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.Render("temp_tips", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Daemon) PageFootballByDays(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
data, err := s.getPageTipsData(flashscore.Football)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.Render("temp_tips", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Daemon) PageTennisByDays(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
data, err := s.getPageTipsData(flashscore.Tennis)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.Render("temp_tips", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Daemon) getPageTipsData(sportID int) (_ PageTipsData, err error) {
|
||||||
|
tips, err := s.ListSportTips(sportID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var days []DayTips
|
||||||
|
|
||||||
|
var currentDay int64
|
||||||
|
var dayTips DayTips
|
||||||
|
|
||||||
|
for _, tip := range tips {
|
||||||
|
if tip.Status != model.Won && tip.Status != model.Lost {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tm := time.Unix(tip.TipTime, 0)
|
||||||
|
|
||||||
|
tm = timeutil.FirstSecondInPeriod(tm, "d")
|
||||||
|
//if err != nil {
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
|
||||||
|
day := tm.Unix()
|
||||||
|
|
||||||
|
if day != currentDay {
|
||||||
|
if currentDay != 0 {
|
||||||
|
days = append(days, dayTips)
|
||||||
|
}
|
||||||
|
|
||||||
|
dayTips = DayTips{
|
||||||
|
Date: tm.Format("Monday Jan 2, 2006"),
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDay = day
|
||||||
|
}
|
||||||
|
|
||||||
|
dayTips.Tips = append(dayTips.Tips, TipView{
|
||||||
|
Time: tm.Format("15:04"),
|
||||||
|
Name: tip.MatchName,
|
||||||
|
Champ: tip.ChampName,
|
||||||
|
Link: fmt.Sprintf("https://www.flashscore.com/match/%s/#h2h;overall", tip.MatchID),
|
||||||
|
Result: tip.Result,
|
||||||
|
Status: tip.Status,
|
||||||
|
Strategy: tip.Strategy,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentDay != 0 {
|
||||||
|
days = append(days, dayTips)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PageTipsData{
|
||||||
|
Days: days,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func (s *Tipper) pageLinked(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
//reply.Render("aliases", nil)
|
||||||
|
var list []*interestingFootballMatch
|
||||||
|
|
||||||
|
for _, x := range s.onexbetMatchToInterestingFootballMatch {
|
||||||
|
list = append(list, x)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, _ := json.MarshalIndent(list, "", " ")
|
||||||
|
|
||||||
|
reply.WriteString(string(buf))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (s *Tipper) pageWatch(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
//reply.Render("aliases", nil)
|
||||||
|
var matchID string
|
||||||
|
state.Val("id", &matchID)
|
||||||
|
|
||||||
|
s.onexbetAPIClient.Call("watch", matchID)
|
||||||
|
|
||||||
|
reply.WriteString(matchID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Tipper) pageUnwatch(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
//reply.Render("aliases", nil)
|
||||||
|
var matchID string
|
||||||
|
state.Val("id", &matchID)
|
||||||
|
|
||||||
|
s.onexbetAPIClient.Call("unwatch", matchID)
|
||||||
|
|
||||||
|
reply.WriteString(matchID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*/
|
||||||
55
daemon/telegram.go
Executable file
55
daemon/telegram.go
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gordenko.dev/dima/httpreq"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @getidsbot - показывает ID чата
|
||||||
|
|
||||||
|
const (
|
||||||
|
// tipper, sport_oracle_bot
|
||||||
|
//telegramBotToken = "1059580991:AAFHO4RrQCIF-JWYQI2em1D5aLJ4KbjDPZU"
|
||||||
|
telegramBotToken = "5637682609:AAH2MJG-r0k9EIUQMagdZOXFUjY4nid514I"
|
||||||
|
//telegramChannel = "@Bet.One.Group"
|
||||||
|
tennisTelegramChatID = "-1001189544230"
|
||||||
|
footballTelegramChatID = "-1001480163278"
|
||||||
|
|
||||||
|
handballTelegramChatID = "-1001372665077"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
telegramSendMessageURL = fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", telegramBotToken)
|
||||||
|
)
|
||||||
|
|
||||||
|
//chat_id=[MY_CHANNEL_NAME]&text=[MY_MESSAGE_TEXT]
|
||||||
|
|
||||||
|
func sendMessageToTelegramGroup(telegramChatID string, msg string) (err error) {
|
||||||
|
data := make(url.Values)
|
||||||
|
data.Set("chat_id", telegramChatID)
|
||||||
|
data.Set("text", msg)
|
||||||
|
data.Set("parse_mode", "HTML")
|
||||||
|
|
||||||
|
u := telegramSendMessageURL + "?" + data.Encode()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := httpreq.Send(req, 10*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
err = fmt.Errorf("StatusCode: %d, Body: %s", resp.StatusCode, resp.Body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
1125
daemon/tennis.go
Normal file
1125
daemon/tennis.go
Normal file
File diff suppressed because it is too large
Load Diff
28
go.mod
Normal file
28
go.mod
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
module gordenko.dev/dima/tipper
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-sql-driver/mysql v1.10.0
|
||||||
|
gordenko.dev/dima/fixme v0.0.0-20230801160335-c5c6b3b00ea2
|
||||||
|
gordenko.dev/dima/flashscore v0.0.0-20260721004947-e8a0b2ba4291
|
||||||
|
gordenko.dev/dima/httpreq v1.0.1-0.20230801160925-0916b3afaf24
|
||||||
|
gordenko.dev/dima/onexbet v0.0.0-20260721005557-15b428efec8d
|
||||||
|
gordenko.dev/dima/pretty v0.0.0-20221225212746-0c27d8c0ac69
|
||||||
|
gordenko.dev/dima/qx v0.0.0-20260720012756-9323ec898f91
|
||||||
|
gordenko.dev/dima/timeutil v0.0.0-20231120085404-2b1633b28a5e
|
||||||
|
gordenko.dev/dima/web v0.0.0-20260719063304-9b080f685b42
|
||||||
|
gordenko.dev/dima/ws v0.0.0-20260720220933-ee2de7671d35
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
|
github.com/PuerkitoBio/goquery v1.12.0 // indirect
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
golang.org/x/crypto v0.54.0 // indirect
|
||||||
|
golang.org/x/net v0.57.0 // indirect
|
||||||
|
golang.org/x/text v0.40.0 // indirect
|
||||||
|
gordenko.dev/dima/spider v0.0.0-20260721003948-776f3225b709 // indirect
|
||||||
|
gordenko.dev/dima/textutil v0.0.0-20260718203502-62db7f60f8f7 // indirect
|
||||||
|
)
|
||||||
103
go.sum
Normal file
103
go.sum
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
|
github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
|
||||||
|
github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
|
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||||
|
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||||
|
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gordenko.dev/dima/fixme v0.0.0-20230801160335-c5c6b3b00ea2 h1:gKOjARkSrD8ARC29Wp0IOjnmCujtL4ju8andsNqH2BQ=
|
||||||
|
gordenko.dev/dima/fixme v0.0.0-20230801160335-c5c6b3b00ea2/go.mod h1:7tMDvA2ej8e7VwXIlcIZNtUe/Tk5jFAK498WqC+aMYE=
|
||||||
|
gordenko.dev/dima/flashscore v0.0.0-20260721004947-e8a0b2ba4291 h1:U+ogwnU9eCG8RMwkSaoIwoYeNKO76f2mb7nrEC9GFp4=
|
||||||
|
gordenko.dev/dima/flashscore v0.0.0-20260721004947-e8a0b2ba4291/go.mod h1:QeZWECcy5wpLsnVBuS9mKeNFHI6ShH5spIE41ojKY9U=
|
||||||
|
gordenko.dev/dima/httpreq v1.0.1-0.20230801160925-0916b3afaf24 h1:InEx24mURTm8oJstSxUPcBgbPyr/KtxBUaJzSGrOe1I=
|
||||||
|
gordenko.dev/dima/httpreq v1.0.1-0.20230801160925-0916b3afaf24/go.mod h1:N7UdxORFT8FsC6laerBR3Zf+ur0Zgpv/zuLD2n7SNTQ=
|
||||||
|
gordenko.dev/dima/onexbet v0.0.0-20260721005557-15b428efec8d h1:3DmKztY4on5QOZEmdsCQUcgvREbRuDFHWHZQ3KFi1vg=
|
||||||
|
gordenko.dev/dima/onexbet v0.0.0-20260721005557-15b428efec8d/go.mod h1:8AhK6vCjmKnIGHbGrOcRKWSSgeJVcqzs0LmNHup5MOI=
|
||||||
|
gordenko.dev/dima/pretty v0.0.0-20221225212746-0c27d8c0ac69 h1:nyJ3mzTQ46yUeMZCdLyYcs7B5JCS54c67v84miyhq2E=
|
||||||
|
gordenko.dev/dima/pretty v0.0.0-20221225212746-0c27d8c0ac69/go.mod h1:AxgKDktpqBVyIOhIcP+nlCpK+EsJyjN5kPdqyd8euVU=
|
||||||
|
gordenko.dev/dima/qx v0.0.0-20260720012756-9323ec898f91 h1:J+C9mqwH5XR1idAi/PcdTMFPRrySyo3EVpDWd4m0FFs=
|
||||||
|
gordenko.dev/dima/qx v0.0.0-20260720012756-9323ec898f91/go.mod h1:CR5GISpjHFHZ9HeSIo3fYRyp1+kVx0R9exzBYsIuB3c=
|
||||||
|
gordenko.dev/dima/spider v0.0.0-20260721003948-776f3225b709 h1:zhooBnyXJUCrViDncwUN7Kh6JMobo6vaiAtobHitxG4=
|
||||||
|
gordenko.dev/dima/spider v0.0.0-20260721003948-776f3225b709/go.mod h1:ZVNbQYItuH9vADWOc9d1Alam646Y29q6JIA6UNFj9N8=
|
||||||
|
gordenko.dev/dima/textutil v0.0.0-20260718203502-62db7f60f8f7 h1:KzwamqlIEgAjQdTVNVfjGnS1f3bC+PzkTdZfuQhbVfk=
|
||||||
|
gordenko.dev/dima/textutil v0.0.0-20260718203502-62db7f60f8f7/go.mod h1:M3e8S3N1USCMYmL6Q5uDQJaxfzBlWHnTX7rxCJQ6iJs=
|
||||||
|
gordenko.dev/dima/timeutil v0.0.0-20231120085404-2b1633b28a5e h1:a4YJB6UiGyunNs6ILy4LjXHrOTNMTE1AM1TEi8/1/78=
|
||||||
|
gordenko.dev/dima/timeutil v0.0.0-20231120085404-2b1633b28a5e/go.mod h1:M57Nz/AfzJluvHl7Spc7YsqUsWwmzhT6NhGeRoWxu1s=
|
||||||
|
gordenko.dev/dima/web v0.0.0-20260719063304-9b080f685b42 h1:dSMZyLvRuCg24UgKWr68kmgQ1SNcFHtd0jD0nhvnQLU=
|
||||||
|
gordenko.dev/dima/web v0.0.0-20260719063304-9b080f685b42/go.mod h1:m6fQP/HjoYlcz9gw2pXZNkvRzb6I/9rsMYESpnFzG8Q=
|
||||||
|
gordenko.dev/dima/ws v0.0.0-20260720220933-ee2de7671d35 h1:/hZR1r74CR0OJ8kYVU701F2lVmnNtDh+MyuYMBZZDoo=
|
||||||
|
gordenko.dev/dima/ws v0.0.0-20260720220933-ee2de7671d35/go.mod h1:/51DHPs14MGkShfh7nXNOrzqlD3yMOS7WXWWNttcfCw=
|
||||||
95
model/db.sql
Executable file
95
model/db.sql
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
|
||||||
|
CREATE TABLE tips (
|
||||||
|
tipID INT NOT NULL AUTO_INCREMENT,
|
||||||
|
sportID INT NOT NULL,
|
||||||
|
strategyID INT NOT NULL,
|
||||||
|
matchID VARCHAR(255) NOT NULL, -- flashscore
|
||||||
|
tipTime INT NOT NULL,
|
||||||
|
champID VARCHAR(255) NOT NULL,
|
||||||
|
champName VARCHAR(255) NOT NULL,
|
||||||
|
matchName VARCHAR(255) NOT NULL,
|
||||||
|
market INT NOT NULL,
|
||||||
|
side INT NOT NULL,
|
||||||
|
param VARCHAR(255) NOT NULL,
|
||||||
|
price DOUBLE NOT NULL,
|
||||||
|
currentMatchTime INT NOT NULL,
|
||||||
|
notes TEXT NOT NULL, -- formatted json
|
||||||
|
result VARCHAR(255) NOT NULL,
|
||||||
|
status INT NOT NULL, -- won, lost, void
|
||||||
|
onexbetStatsMatchID VARCHAR(255) NOT NULL, -- for inplay tips only
|
||||||
|
isRated BOOL NOT NULL,
|
||||||
|
isTeamsSwapped BOOL NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY(tipID)
|
||||||
|
)
|
||||||
|
ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE markets (
|
||||||
|
market INT NOT NULL AUTO_INCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY(marketID)
|
||||||
|
)
|
||||||
|
ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE sides (
|
||||||
|
side INT NOT NULL AUTO_INCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY(side)
|
||||||
|
)
|
||||||
|
ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE sports (
|
||||||
|
sportID INT NOT NULL AUTO_INCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY(sportID)
|
||||||
|
)
|
||||||
|
ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE strategies (
|
||||||
|
strategyID INT NOT NULL AUTO_INCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
sportID INT NOT NULL,
|
||||||
|
notes TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY(strategyID)
|
||||||
|
)
|
||||||
|
ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER TABLE tips ADD COLUMN champID VARCHAR(255) NOT NULL;
|
||||||
|
--ALTER TABLE tips ADD COLUMN isRated BOOL NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
INSERT INTO sports VALUES (1, 'Football');
|
||||||
|
INSERT INTO sports VALUES (2, 'Handball');
|
||||||
|
INSERT INTO sports VALUES (3, 'Tennis');
|
||||||
|
INSERT INTO sports VALUES (4, 'Hockey');
|
||||||
|
INSERT INTO sports VALUES (5, 'Basketball');
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO strategies VALUES (1, 'H1 Over 0.5', 1, '');
|
||||||
|
INSERT INTO strategies VALUES (2, 'FT Over 0.5', 1, '');
|
||||||
|
INSERT INTO strategies VALUES (3, 'FT Over 1.5', 1, '');
|
||||||
|
INSERT INTO strategies VALUES (6, 'H1 Over 0.5 M', 1, '');
|
||||||
|
INSERT INTO strategies VALUES (7, 'FT Over 0.5 M', 1, '');
|
||||||
|
INSERT INTO strategies VALUES (8, 'FT Over 1.5 M', 1, '');
|
||||||
|
|
||||||
|
INSERT INTO strategies VALUES (4, 'FT Over (-8, 5 min)', 2, '');
|
||||||
|
INSERT INTO strategies VALUES (5, 'FT Under (+8, 5 min)', 2, '');
|
||||||
|
INSERT INTO strategies VALUES (9, 'FT Over (-6, 10 min)', 2, '');
|
||||||
|
INSERT INTO strategies VALUES (10, 'FT Under (+6, 10 min)', 2, '');
|
||||||
|
INSERT INTO strategies VALUES (11, 'P1 Over (-4, 5 min)', 2, '');
|
||||||
|
INSERT INTO strategies VALUES (12, 'P1 Under (+4, 5 min)', 2, '');
|
||||||
|
INSERT INTO strategies VALUES (13, 'P1 Over (-5, 5 min)', 2, '');
|
||||||
|
INSERT INTO strategies VALUES (14, 'P1 Under (+5, 5 min)', 2, '');
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE tips ADD COLUMN isTeamsSwapped BOOL NOT NULL;
|
||||||
17
model/errors.go
Executable file
17
model/errors.go
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "gordenko.dev/dima/fixme"
|
||||||
|
|
||||||
|
const (
|
||||||
|
EmptyValue = "100"
|
||||||
|
WrongValue = "101"
|
||||||
|
Duplicate = "104"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrorMessages = map[fixme.Code]string{
|
||||||
|
EmptyValue: "Пустое значение недопустимо",
|
||||||
|
WrongValue: "Неверное значение",
|
||||||
|
Duplicate: "Дубликат!",
|
||||||
|
}
|
||||||
|
|
||||||
|
var fix = fixme.New(ErrorMessages)
|
||||||
469
model/model.go
Executable file
469
model/model.go
Executable file
@@ -0,0 +1,469 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gordenko.dev/dima/qx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(qxdb *qx.Db) *Model {
|
||||||
|
if qxdb == nil {
|
||||||
|
panic("Param is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
s := new(Model)
|
||||||
|
s.qx = qxdb
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
qx *qx.Db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Model) AddTip(tip Tip) (tipID int64, err error) {
|
||||||
|
/*
|
||||||
|
if req.Name == "" {
|
||||||
|
err = api.Err{
|
||||||
|
Code: EmptyValue,
|
||||||
|
Field: "name",
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
tx, err := s.qx.Tx()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer tx.Close(&err)
|
||||||
|
|
||||||
|
tipID, err = tx.Insert(tip)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = tx.Commit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Model) GetTip(tipID int64) (_ *Tip, err error) {
|
||||||
|
tip := Tip{
|
||||||
|
TipID: tipID,
|
||||||
|
}
|
||||||
|
found, err := s.qx.One(&tip)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if found {
|
||||||
|
return &tip, nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Model) HasTip(matchID string, strategyID int64) (has bool, err error) {
|
||||||
|
has, err = s.qx.Has("tips", "WHERE matchID=? AND strategyID=?", matchID, strategyID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Model) ListSportTips(sportID int) (tips []Tip, err error) {
|
||||||
|
err = s.qx.ListBy(&tips, "WHERE sportID=? ORDER BY tipTime DESC", sportID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Model) ListTips() (tips []Tip, err error) {
|
||||||
|
err = s.qx.ListBy(&tips, "ORDER BY tipTime DESC")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Model) ListWaitingTips() (tips []Tip, err error) {
|
||||||
|
err = s.qx.ListBy(&tips, "WHERE status=? ORDER BY tipTime DESC", Waiting)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Model) ListStrategyTips(strategyID int) (tips []Tip, err error) {
|
||||||
|
err = s.qx.ListBy(&tips, "WHERE strategyID=? ORDER BY tipTime DESC", strategyID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMatchTips - возвращает прогнозы с неизвестным результатом
|
||||||
|
func (s *Model) ListMatchTips(matchID string) (tips []Tip, err error) {
|
||||||
|
err = s.qx.ListBy(&tips, "WHERE matchID=? AND status=0", matchID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Model) SetTipResult(req SetTipResultReq) (err error) {
|
||||||
|
switch req.Status {
|
||||||
|
case Waiting, Won, Lost, Void:
|
||||||
|
// pass
|
||||||
|
|
||||||
|
default:
|
||||||
|
err = fix.Field(WrongValue, "status")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Result = strings.TrimSpace(req.Result)
|
||||||
|
|
||||||
|
if req.Result == "" {
|
||||||
|
err = fix.Field(EmptyValue, "result")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.qx.Exec(`UPDATE tips SET result=?, status=?, notes=? WHERE tipID=?`,
|
||||||
|
req.Result, req.Status, req.Notes, req.TipID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type StrategyPoint struct {
|
||||||
|
Date int64 `json:"date"`
|
||||||
|
Profit float64 `json:"profit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Model) GetStrategyHistory(strategyID int64) (list []StrategyPoint, err error) {
|
||||||
|
var (
|
||||||
|
tips []Tip
|
||||||
|
profit float64
|
||||||
|
)
|
||||||
|
|
||||||
|
err = s.qx.ListBy(&tips, "WHERE strategyID=? AND status IN (1, 2) ORDER BY tipTime ASC",
|
||||||
|
strategyID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tip := range tips {
|
||||||
|
if tip.Status == Won {
|
||||||
|
profit += (tip.Price - 1) * 10
|
||||||
|
} else {
|
||||||
|
profit -= 10
|
||||||
|
}
|
||||||
|
|
||||||
|
list = append(list, StrategyPoint{
|
||||||
|
Date: tip.TipTime,
|
||||||
|
Profit: profit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Model) GetStrategyRatedHistory(strategyID int64) (list []StrategyPoint, err error) {
|
||||||
|
var (
|
||||||
|
tips []Tip
|
||||||
|
profit float64
|
||||||
|
)
|
||||||
|
|
||||||
|
err = s.qx.ListBy(&tips, "WHERE strategyID=? AND status IN (1, 2) AND isRated=true ORDER BY tipTime ASC",
|
||||||
|
strategyID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tip := range tips {
|
||||||
|
if tip.Status == Won {
|
||||||
|
profit += (tip.Price - 1) * 10
|
||||||
|
} else {
|
||||||
|
profit -= 10
|
||||||
|
}
|
||||||
|
|
||||||
|
list = append(list, StrategyPoint{
|
||||||
|
Date: tip.TipTime,
|
||||||
|
Profit: profit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Model) ListStrategiesReports(sportID int) (list []StrategyReport, err error) {
|
||||||
|
var (
|
||||||
|
tips []Tip
|
||||||
|
reports = make(map[int64]*StrategyReport)
|
||||||
|
)
|
||||||
|
|
||||||
|
err = s.qx.ListBy(&tips, "WHERE sportID=? AND status IN (1, 2) ORDER BY tipTime ASC", sportID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tip := range tips {
|
||||||
|
report, ok := reports[tip.StrategyID]
|
||||||
|
if !ok {
|
||||||
|
report = &StrategyReport{
|
||||||
|
StrategyID: tip.StrategyID,
|
||||||
|
}
|
||||||
|
reports[tip.StrategyID] = report
|
||||||
|
}
|
||||||
|
|
||||||
|
report.Turnover += 10
|
||||||
|
|
||||||
|
if tip.Status == Won {
|
||||||
|
report.Profit += (tip.Price - 1) * 10
|
||||||
|
report.WonTips++
|
||||||
|
} else {
|
||||||
|
report.Profit -= 10
|
||||||
|
report.LostTips++
|
||||||
|
}
|
||||||
|
|
||||||
|
if report.Profit < report.Drawdown {
|
||||||
|
report.Drawdown = report.Profit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var strategies []Strategy
|
||||||
|
|
||||||
|
err = s.qx.ListQuery(&strategies,
|
||||||
|
"SELECT strategyID, name FROM strategies WHERE sportID=?", sportID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, strategy := range strategies {
|
||||||
|
report, ok := reports[strategy.StrategyID]
|
||||||
|
if ok {
|
||||||
|
report.Strategy = strategy.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, report := range reports {
|
||||||
|
// turnover = 100%
|
||||||
|
// profit = x (ROI)
|
||||||
|
report.ROI = (report.Profit * 100) / report.Turnover
|
||||||
|
|
||||||
|
// won + lost = 100%
|
||||||
|
// won = x (Accuracy)
|
||||||
|
report.Accuracy = (float64(report.WonTips) * 100) / float64(report.WonTips+report.LostTips)
|
||||||
|
|
||||||
|
list = append(list, *report)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(list, func(i, j int) bool {
|
||||||
|
return list[i].StrategyID < list[j].StrategyID
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Model) GetSportRatedReport(sportID int64) (report SportReport, err error) {
|
||||||
|
var tips []Tip
|
||||||
|
|
||||||
|
report.SportID = sportID
|
||||||
|
|
||||||
|
err = s.qx.ListBy(&tips, "WHERE sportID=? AND status IN (1, 2) AND isRated=true ORDER BY tipTime ASC", sportID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tip := range tips {
|
||||||
|
|
||||||
|
report.Turnover += 10
|
||||||
|
|
||||||
|
if tip.Status == Won {
|
||||||
|
report.Profit += (tip.Price - 1) * 10
|
||||||
|
report.WonTips++
|
||||||
|
} else {
|
||||||
|
report.Profit -= 10
|
||||||
|
report.LostTips++
|
||||||
|
}
|
||||||
|
|
||||||
|
if report.Profit < report.Drawdown {
|
||||||
|
report.Drawdown = report.Profit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// turnover = 100%
|
||||||
|
// profit = x (ROI)
|
||||||
|
report.ROI = (report.Profit * 100) / report.Turnover
|
||||||
|
|
||||||
|
// won + lost = 100%
|
||||||
|
// won = x (Accuracy)
|
||||||
|
report.Accuracy = (float64(report.WonTips) * 100) / float64(report.WonTips+report.LostTips)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAccuracy(won, lost int) (accuracy float64) {
|
||||||
|
// won + lost = 100%
|
||||||
|
// won = x%
|
||||||
|
// x = (won*100) / (won+lost)
|
||||||
|
|
||||||
|
total := won + lost
|
||||||
|
if total == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return float64(won*100) / float64(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StrategyChampStats struct {
|
||||||
|
//StrategyID int64 `json:"strategyID"`
|
||||||
|
ZoneID string `json:"zoneID" qx:"table=tips"`
|
||||||
|
ZoneName string `json:"zoneName"`
|
||||||
|
ChampID string `json:"champID"`
|
||||||
|
ChampName string `json:"champName"`
|
||||||
|
Won int `json:"won"`
|
||||||
|
Lost int `json:"lost"`
|
||||||
|
AvgPrice float64 `json:"avgPrice"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Статистика по лигам
|
||||||
|
func (s *Model) ListStrategyChampStats(strategyID int64) (list []StrategyChampStats, err error) {
|
||||||
|
err = s.qx.ListQuery(&list, `
|
||||||
|
SELECT z.zoneID, z.name, t.champID, c.name, COUNT(IF(t.status=1,1,NULL)) as won,
|
||||||
|
COUNT(IF(t.status=2,1,NULL)) as lost, AVG(t.price)
|
||||||
|
FROM tips t
|
||||||
|
INNER JOIN teamChamps c USING(champID)
|
||||||
|
INNER JOIN teamZones z USING(zoneID)
|
||||||
|
WHERE t.strategyID=? AND t.status IN (1,2)
|
||||||
|
GROUP BY t.champID
|
||||||
|
ORDER BY c.name ASC`, strategyID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eps := 0.001
|
||||||
|
|
||||||
|
sort.Slice(list, func(i, j int) bool {
|
||||||
|
// Сортируем по точности прогнозов
|
||||||
|
|
||||||
|
a := getAccuracy(list[i].Won, list[i].Lost)
|
||||||
|
b := getAccuracy(list[j].Won, list[j].Lost)
|
||||||
|
|
||||||
|
dif := math.Abs(a - b)
|
||||||
|
|
||||||
|
if dif < eps {
|
||||||
|
if list[i].Won == list[j].Won {
|
||||||
|
if list[i].ZoneName == list[j].ZoneName {
|
||||||
|
return list[i].ChampName < list[j].ChampName
|
||||||
|
}
|
||||||
|
return list[i].ZoneName < list[j].ZoneName
|
||||||
|
}
|
||||||
|
return list[i].Won > list[j].Won
|
||||||
|
}
|
||||||
|
|
||||||
|
return a > b
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Статистика по лигам
|
||||||
|
func (s *Model) ListStrategyRatedChampStats(strategyID int64) (list []StrategyChampStats, err error) {
|
||||||
|
err = s.qx.ListQuery(&list, `
|
||||||
|
SELECT z.zoneID, z.name, t.champID, c.name, COUNT(IF(t.status=1,1,NULL)) as won,
|
||||||
|
COUNT(IF(t.status=2,1,NULL)) as lost, AVG(t.price)
|
||||||
|
FROM tips t
|
||||||
|
INNER JOIN teamChamps c USING(champID)
|
||||||
|
INNER JOIN teamZones z USING(zoneID)
|
||||||
|
WHERE t.strategyID=? AND t.status IN (1,2) AND t.isRated=true
|
||||||
|
GROUP BY t.champID
|
||||||
|
ORDER BY c.name ASC`, strategyID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eps := 0.001
|
||||||
|
|
||||||
|
sort.Slice(list, func(i, j int) bool {
|
||||||
|
// Сортируем по точности прогнозов
|
||||||
|
|
||||||
|
a := getAccuracy(list[i].Won, list[i].Lost)
|
||||||
|
b := getAccuracy(list[j].Won, list[j].Lost)
|
||||||
|
|
||||||
|
dif := math.Abs(a - b)
|
||||||
|
|
||||||
|
if dif < eps {
|
||||||
|
if list[i].Won == list[j].Won {
|
||||||
|
if list[i].ZoneName == list[j].ZoneName {
|
||||||
|
return list[i].ChampName < list[j].ChampName
|
||||||
|
}
|
||||||
|
return list[i].ZoneName < list[j].ZoneName
|
||||||
|
}
|
||||||
|
return list[i].Won > list[j].Won
|
||||||
|
}
|
||||||
|
|
||||||
|
return a > b
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Strategy struct {
|
||||||
|
StrategyID int64 `json:"strategyID" qx:"table=strategies; pk"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Статистика по лигам
|
||||||
|
func (s *Model) ListStrategies() (list []Strategy, err error) {
|
||||||
|
tmp := []struct {
|
||||||
|
StrategyID int64
|
||||||
|
Name string
|
||||||
|
SportID int64
|
||||||
|
SportName string
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err = s.qx.ListQuery(&tmp, `
|
||||||
|
SELECT s.strategyID, s.name, x.sportID, x.name
|
||||||
|
FROM strategies s
|
||||||
|
INNER JOIN sports x USING(sportID)
|
||||||
|
ORDER BY x.sportID ASC, s.strategyID ASC`)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, x := range tmp {
|
||||||
|
list = append(list, Strategy{
|
||||||
|
StrategyID: x.StrategyID,
|
||||||
|
Name: fmt.Sprintf("#%d %s. %s", x.StrategyID, x.SportName, x.Name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
/*
|
||||||
|
return []Strategy{
|
||||||
|
{
|
||||||
|
StrategyID: 1,
|
||||||
|
Name: "#1 Алгоритм 1 (Футбол, гол в 1м тайме)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StrategyID: 2,
|
||||||
|
Name: "#2 Алгоритм 2 (Футбол, гол в матче)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StrategyID: 3,
|
||||||
|
Name: "#3 Алгоритм 3 (Футбол, 2й гол в матче)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StrategyID: 4,
|
||||||
|
Name: "#4 Алгоритм ТБ (-9) (Гандбол, ТБ в матче)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StrategyID: 5,
|
||||||
|
Name: "#5 Алгоритм ТМ (+8) (Гандбол, ТМ в матче)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StrategyID: 6,
|
||||||
|
Name: "#6 Алгоритм 1 модифицированный (Футбол, гол в 1м тайме)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StrategyID: 7,
|
||||||
|
Name: "#7 Алгоритм 2 модифицированный (Футбол, гол в матче)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StrategyID: 8,
|
||||||
|
Name: "#8 Алгоритм 3 модифицированный (Футбол, 2й гол в матче)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StrategyID: 11,
|
||||||
|
Name: "#11 Алгоритм ТБ (-6) (Гандбол, ТБ в 1м тайме)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StrategyID: 12,
|
||||||
|
Name: "#12 Алгоритм ТМ (+5) (Гандбол, ТМ в 1м тайме)",
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
}, nil
|
||||||
|
*/
|
||||||
|
}
|
||||||
29
model/model_test.go
Normal file
29
model/model_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gordenko.dev/dima/qx"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListStrategyCHampStats(t *testing.T) {
|
||||||
|
db, err := qx.Open("mysql", "user:password@/tipper")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
m := New(db)
|
||||||
|
|
||||||
|
list, err := m.ListStrategyChampStats(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range list {
|
||||||
|
fmt.Printf("%.2f%% %d - %d\n", getAccuracy(item.Won, item.Lost), item.Won, item.Lost)
|
||||||
|
}
|
||||||
|
}
|
||||||
209
model/tipper.Dec2.2020.dump
Normal file
209
model/tipper.Dec2.2020.dump
Normal file
File diff suppressed because one or more lines are too long
63
model/types.go
Executable file
63
model/types.go
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type TipStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Waiting TipStatus = 0
|
||||||
|
Won TipStatus = 1
|
||||||
|
Lost TipStatus = 2
|
||||||
|
Void TipStatus = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
type SportReport struct {
|
||||||
|
SportID int64 `json:"sportID"`
|
||||||
|
WonTips int `json:"wonTips"`
|
||||||
|
LostTips int `json:"lostTips"`
|
||||||
|
Profit float64 `json:"profit"` // прибыль / убыток в деньгах
|
||||||
|
Turnover float64 `json:"turnover"` // оборот, сумма поставленных денег
|
||||||
|
ROI float64 `json:"roi"` // return on investments
|
||||||
|
Drawdown float64 `json:"drawdown"` // просадка, в деньгах
|
||||||
|
Accuracy float64 `json:"accuracy"` // точность прогноза
|
||||||
|
}
|
||||||
|
|
||||||
|
type StrategyReport struct {
|
||||||
|
StrategyID int64 `json:"strategyID"`
|
||||||
|
Strategy string `json:"strategy"`
|
||||||
|
WonTips int `json:"wonTips"`
|
||||||
|
LostTips int `json:"lostTips"`
|
||||||
|
Profit float64 `json:"profit"` // прибыль / убыток в деньгах
|
||||||
|
Turnover float64 `json:"turnover"` // оборот, сумма поставленных денег
|
||||||
|
ROI float64 `json:"roi"` // return on investments
|
||||||
|
Drawdown float64 `json:"drawdown"` // просадка, в деньгах
|
||||||
|
Accuracy float64 `json:"accuracy"` // точность прогноза
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetTipResultReq struct {
|
||||||
|
TipID int64 `json:"tipID"`
|
||||||
|
Result string `json:"result"`
|
||||||
|
Status TipStatus `json:"status"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tip struct {
|
||||||
|
TipID int64 `json:"tipID" qx:"table=tips; pk; seq=true"`
|
||||||
|
SportID int64 `json:"sportID"`
|
||||||
|
StrategyID int64 `json:"strategyID"`
|
||||||
|
MatchID string `json:"matchID"`
|
||||||
|
TipTime int64 `json:"tipTime"`
|
||||||
|
ChampID string `json:"champID"`
|
||||||
|
ChampName string `json:"champName"`
|
||||||
|
MatchName string `json:"matchName"`
|
||||||
|
Market int `json:"market"`
|
||||||
|
Side int `json:"side"`
|
||||||
|
Strategy string `json:"strategy" qx:"-"`
|
||||||
|
Param string `json:"param"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
CurrentMatchTime int `json:"currentMatchTime"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
Result string `json:"result"`
|
||||||
|
Status TipStatus `json:"status"`
|
||||||
|
OnexbetStatsMatchID string `json:"onexbetStatsMatchID"`
|
||||||
|
IsRated bool `json:"isRated"`
|
||||||
|
IsTeamsSwapped bool `json:"isTeamsSwapped"`
|
||||||
|
}
|
||||||
12
services/flashscore.service
Executable file
12
services/flashscore.service
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Flashscore
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
ExecStart=/home/ubuntu/flashscore/daemon
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
11
services/onexbet.service
Executable file
11
services/onexbet.service
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=1xBet
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
ExecStart=/home/ubuntu/onexbet/daemon
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
12
services/tipper.service
Executable file
12
services/tipper.service
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tipper
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
ExecStart=/home/ubuntu/tipper/tipper
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
112
static/aliases.js
Executable file
112
static/aliases.js
Executable file
@@ -0,0 +1,112 @@
|
|||||||
|
let u = new URL(location.href)
|
||||||
|
urlPrivateAPI = u.origin + '/api'
|
||||||
|
|
||||||
|
api = new API({
|
||||||
|
url: urlPrivateAPI,
|
||||||
|
timeout: 10000, // 10 sec
|
||||||
|
})
|
||||||
|
|
||||||
|
Handball = 2
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
let lastTeamID;
|
||||||
|
|
||||||
|
let zoneSelect = new Select({
|
||||||
|
field: document.getElementById('zone'),
|
||||||
|
key: 'zoneID',
|
||||||
|
name: 'name'
|
||||||
|
})
|
||||||
|
|
||||||
|
zoneSelect.on('select', zoneID => {
|
||||||
|
aliasesModel.clear()
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listTeamsByZone',
|
||||||
|
data: zoneID,
|
||||||
|
onSuccess: resp => {
|
||||||
|
teamSelect.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lastTeamID = ''
|
||||||
|
|
||||||
|
let obj = f.getFormData()
|
||||||
|
obj.teamID = ''
|
||||||
|
|
||||||
|
f.setObj(obj)
|
||||||
|
})
|
||||||
|
|
||||||
|
let teamSelect = new Select({
|
||||||
|
field: document.getElementById('team'),
|
||||||
|
key: 'teamID',
|
||||||
|
name: 'canonicalName'
|
||||||
|
})
|
||||||
|
|
||||||
|
teamSelect.on('select', teamID => {
|
||||||
|
aliasesModel.load({
|
||||||
|
data: teamID,
|
||||||
|
})
|
||||||
|
/*
|
||||||
|
api.req({
|
||||||
|
func: 'listFootballTeamAliases',
|
||||||
|
data: teamID,
|
||||||
|
onSuccess: resp => {
|
||||||
|
//aliasesModel.setList(resp)
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
|
lastTeamID = teamID
|
||||||
|
|
||||||
|
let obj = f.getFormData()
|
||||||
|
obj.teamID = teamID
|
||||||
|
|
||||||
|
f.setObj(obj)
|
||||||
|
})
|
||||||
|
|
||||||
|
let aliasesModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'listItem'
|
||||||
|
},
|
||||||
|
key: 'aliasID',
|
||||||
|
container: 'aliases',
|
||||||
|
btnRemove: 'removeAlias',
|
||||||
|
ajax: {
|
||||||
|
api: api,
|
||||||
|
listFunc: 'listTeamAliases',
|
||||||
|
removeFunc: 'removeTeamAlias'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let f = new Formobj({
|
||||||
|
form: 'add',
|
||||||
|
schema: {
|
||||||
|
name: 'str',
|
||||||
|
teamID: 'str'
|
||||||
|
},
|
||||||
|
validate: obj => {
|
||||||
|
if (obj.teamID == '') {
|
||||||
|
return {
|
||||||
|
message: 'Не выбрана команда!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
api: api,
|
||||||
|
submitFunc: 'addTeamAlias',
|
||||||
|
editable: true,
|
||||||
|
onSubmitResult: () => {
|
||||||
|
aliasesModel.load({
|
||||||
|
data: lastTeamID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listTeamZones',
|
||||||
|
data: Handball,
|
||||||
|
onSuccess: resp => {
|
||||||
|
zoneSelect.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
609
static/football.js
Normal file
609
static/football.js
Normal file
@@ -0,0 +1,609 @@
|
|||||||
|
let u = new URL(location.href)
|
||||||
|
urlPrivateAPI = u.origin + '/api'
|
||||||
|
|
||||||
|
api = new API({
|
||||||
|
url: urlPrivateAPI,
|
||||||
|
timeout: 10000, // 10 sec
|
||||||
|
})
|
||||||
|
|
||||||
|
Football = 1
|
||||||
|
Handball = 2
|
||||||
|
|
||||||
|
OnexbetFootball = 1
|
||||||
|
OnexbetHandball = 8
|
||||||
|
|
||||||
|
onexbetWsURL = 'ws://195.189.227.88:7772/ws'
|
||||||
|
tipperWsURL = 'ws://195.189.227.88:7768/ws'
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', ()=> {
|
||||||
|
interesting = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'interestingMatch',
|
||||||
|
renders: {
|
||||||
|
interestingMatch: (tags, match) => {
|
||||||
|
//console.log('render', match)
|
||||||
|
let radios = new RadioList({
|
||||||
|
container: tags.candidates,
|
||||||
|
templateId: 'candidate-item',
|
||||||
|
setEmptyMessage: 'нет матчей кандидатов'
|
||||||
|
})
|
||||||
|
|
||||||
|
tags.flashscoreLink.href= `https://www.flashscore.com/match/${match.matchID}/#match-summary/match-summary`
|
||||||
|
|
||||||
|
tags.search.addEventListener('click', () => {
|
||||||
|
let teamName = tags.teamName.value.trim()
|
||||||
|
|
||||||
|
if (teamName == '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'searchMatchCandidatesByTeamName',
|
||||||
|
data: {
|
||||||
|
sportID: Football,
|
||||||
|
teamName: teamName,
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
console.log(err)
|
||||||
|
tags.candidates.textContent = "FUCK: " + err
|
||||||
|
},
|
||||||
|
onSuccess: resp => {
|
||||||
|
tags.submit.disabled = false
|
||||||
|
radios.clear()
|
||||||
|
|
||||||
|
console.log(resp)
|
||||||
|
if (Array.isArray(resp)) {
|
||||||
|
resp.forEach(candidate => {
|
||||||
|
candidate.startTimeStr = moment(candidate.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// show model + form
|
||||||
|
radios.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tags.submit.addEventListener('click', () => {
|
||||||
|
//console.log(radios.getSelected())
|
||||||
|
let candidate = radios.getSelected()
|
||||||
|
if (!candidate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let isReverse = tags.isReverse.checked
|
||||||
|
|
||||||
|
var home, away;
|
||||||
|
|
||||||
|
if (isReverse) {
|
||||||
|
home = {
|
||||||
|
teamID: match.home.teamID,
|
||||||
|
name: candidate.away
|
||||||
|
}
|
||||||
|
away = {
|
||||||
|
teamID: match.away.teamID,
|
||||||
|
name: candidate.home
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
home = {
|
||||||
|
teamID: match.home.teamID,
|
||||||
|
name: candidate.home
|
||||||
|
}
|
||||||
|
away = {
|
||||||
|
teamID: match.away.teamID,
|
||||||
|
name: candidate.away
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('home:', home);
|
||||||
|
console.log('away:', away);
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'addTeamsAliases',
|
||||||
|
data: {
|
||||||
|
home: home,
|
||||||
|
away: away
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
//tags.candidates.textContent = 'FUCK: ' + err
|
||||||
|
console.log(err)
|
||||||
|
},
|
||||||
|
onSuccess: resp => {
|
||||||
|
//console.log('ok')
|
||||||
|
//tags.identification.click()
|
||||||
|
radios.clear()
|
||||||
|
tags.candidates.textContent = "Алиасы добавлены!"
|
||||||
|
tags.submit.disabled = true
|
||||||
|
tags.isReverse.checked = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tags.identification.addEventListener('click', () => {
|
||||||
|
//console.log('details:', tags.identification.open)
|
||||||
|
//console.log(match)
|
||||||
|
if (tags.identification.open) {
|
||||||
|
//tags.candidates.innerHTML = ''
|
||||||
|
} else {
|
||||||
|
//tags.candidates.innerHTML = ''
|
||||||
|
radios.clear()
|
||||||
|
|
||||||
|
//radios.setList([
|
||||||
|
// {home: 'Arsenal', away: 'Mancity', sport: 'Football', champName: 'England. Premier League', startTimeStr: "Oct 06, 12:30:00"},
|
||||||
|
// {home: 'Arsenal', away: 'Aston villa', sport: 'Football', champName: 'England. Premier League', startTimeStr: "Oct 06, 12:30:00"},
|
||||||
|
//])
|
||||||
|
|
||||||
|
//tags.candidates.textContent = JSON.stringify(match)
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'searchMatchCandidates',
|
||||||
|
data: {
|
||||||
|
sportID: Football,
|
||||||
|
home: match.home.canonicalName,
|
||||||
|
away: match.away.canonicalName
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
console.log(err)
|
||||||
|
tags.candidates.textContent = "FUCK: " + err
|
||||||
|
},
|
||||||
|
onSuccess: resp => {
|
||||||
|
tags.submit.disabled = false
|
||||||
|
console.log(resp)
|
||||||
|
if (Array.isArray(resp)) {
|
||||||
|
resp.forEach(candidate => {
|
||||||
|
candidate.startTimeStr = moment(candidate.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// show model + form
|
||||||
|
radios.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
container: 'interesting',
|
||||||
|
preFunc: modifyInterestingMatch,
|
||||||
|
sort: {
|
||||||
|
key: 'startTime',
|
||||||
|
reverse: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// ONEXBET
|
||||||
|
|
||||||
|
//1xbet
|
||||||
|
bookLiveModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'liveMatch',
|
||||||
|
},
|
||||||
|
container: 'book-live',
|
||||||
|
preFunc: modifyLiveMatch,
|
||||||
|
sort: {
|
||||||
|
key: 'startTime',
|
||||||
|
reverse: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// tips
|
||||||
|
|
||||||
|
tipModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'tip',
|
||||||
|
renders: {
|
||||||
|
tip: (tags, tip) => {
|
||||||
|
//console.log('tags:', tags)
|
||||||
|
//console.log('x:', x)
|
||||||
|
if (tip.status == 1) {
|
||||||
|
tags.matchName.classList.add('profit')
|
||||||
|
} else if (tip.status == 2) {
|
||||||
|
tags.matchName.classList.add('errmsg')
|
||||||
|
} else if (tip.status == 3) {
|
||||||
|
tags.matchName.classList.add('void')
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.flashscoreLink.href= `https://www.flashscore.com/match/${tip.matchID}/#match-summary/match-summary`
|
||||||
|
|
||||||
|
tags.setResultLink.addEventListener('click', () => {
|
||||||
|
if (tags.setResultForm.hidden) {
|
||||||
|
let f = new Form({
|
||||||
|
formContainer: tags.setResultForm,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
name: 'result',
|
||||||
|
type: 'line',
|
||||||
|
label: 'Результат',
|
||||||
|
comment: 'например, 1й тайм: 1:1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
type: 'radios',
|
||||||
|
label: ' ',
|
||||||
|
getName: x => x.name,
|
||||||
|
data: [
|
||||||
|
{name: 'Выигрыш', status: 1},
|
||||||
|
{name: 'Проигрыш', status: 2},
|
||||||
|
{name: 'Возврат', status: 3},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
f.onSubmit((obj) => {
|
||||||
|
obj.tipID = tip.tipID
|
||||||
|
//console.log(obj)
|
||||||
|
api.req({
|
||||||
|
func: 'setTipResult',
|
||||||
|
data: obj,
|
||||||
|
onError: err => {
|
||||||
|
f.showError(err)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
// Перезагружаем список
|
||||||
|
api.req({
|
||||||
|
func: 'listSportTips',
|
||||||
|
data: Football,
|
||||||
|
onSuccess: resp => {
|
||||||
|
tipModel.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
f.init()
|
||||||
|
|
||||||
|
tags.setResultForm.hidden = false
|
||||||
|
} else {
|
||||||
|
tags.setResultForm.hidden = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
container: 'tips',
|
||||||
|
preFunc: modifyTip,
|
||||||
|
sort: {
|
||||||
|
key: 'tipTime',
|
||||||
|
//reverse: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// strategies
|
||||||
|
|
||||||
|
strategiesModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'strategy',
|
||||||
|
},
|
||||||
|
container: 'strategies',
|
||||||
|
preFunc: (r) => {
|
||||||
|
r.roi = Math.round(r.roi*100)/100
|
||||||
|
r.profit = Math.round(r.profit*100)/100
|
||||||
|
r.drawdown = Math.round(r.drawdown*100)/100
|
||||||
|
r.accuracy = Math.round(r.accuracy*100)/100
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
totalsModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'total',
|
||||||
|
},
|
||||||
|
container: 'totals',
|
||||||
|
preFunc: (r) => {
|
||||||
|
r.roi = Math.round(r.roi*100)/100
|
||||||
|
r.profit = Math.round(r.profit*100)/100
|
||||||
|
r.drawdown = Math.round(r.drawdown*100)/100
|
||||||
|
r.accuracy = Math.round(r.accuracy*100)/100
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let onexbetWs = new WebsocketClient({
|
||||||
|
url: onexbetWsURL
|
||||||
|
})
|
||||||
|
|
||||||
|
onexbetWs.handle('liveMatches', resp => {
|
||||||
|
//console.log('liveMatches:', resp)
|
||||||
|
|
||||||
|
if (resp.sportID == OnexbetFootball) {
|
||||||
|
if (Array.isArray(resp.matches)) {
|
||||||
|
bookLiveModel.setList(resp.matches)
|
||||||
|
|
||||||
|
resp.matches.forEach(match => {
|
||||||
|
interesting.partialUpdateBy(
|
||||||
|
// update func
|
||||||
|
(tags, obj) => {
|
||||||
|
tags.livescore.textContent = footballScoreToString(match.scoreByPeriods)
|
||||||
|
tags.livescore.hidden = false
|
||||||
|
},
|
||||||
|
// key func
|
||||||
|
(obj) => {
|
||||||
|
if (obj.onexbetMatchID == match.matchID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onexbetWs.handle('teamLiveMatchData', resp => {
|
||||||
|
//console.log('teamLiveMatchData:', resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// tipper
|
||||||
|
let tipperWs = new WebsocketClient({
|
||||||
|
url: tipperWsURL
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.onConnected = () => {
|
||||||
|
tipperWs.send('subscribe', {channels: ['football']})
|
||||||
|
}
|
||||||
|
|
||||||
|
tipperWs.handle('teamMatches', resp => {
|
||||||
|
//console.log('reports:', resp)
|
||||||
|
|
||||||
|
if (Array.isArray(resp.matches)) {
|
||||||
|
//console.log('set interesting:', resp.matches)
|
||||||
|
interesting.setList(resp.matches)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.handle('tipsChanged', resp => {
|
||||||
|
//console.log('tip:', resp)
|
||||||
|
|
||||||
|
//tipModel.add(resp)
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listSportTips',
|
||||||
|
data: Football,
|
||||||
|
onSuccess: resp => {
|
||||||
|
tipModel.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listStrategiesReports',
|
||||||
|
data: Football,
|
||||||
|
onSuccess: resp => {
|
||||||
|
strategiesModel.setList(resp)
|
||||||
|
showTotal(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
tipperWs.handle('whyNot', resp => {
|
||||||
|
//console.log('whyNot:', resp.matchID, resp.comment)
|
||||||
|
interesting.partialUpdateBy(
|
||||||
|
(tags, obj) => {
|
||||||
|
tags.comment.innerHTML = resp.comment
|
||||||
|
tags.time.textContent = resp.time
|
||||||
|
tags.score.textContent = `${resp.homeGoals} - ${resp.awayGoals}`
|
||||||
|
},
|
||||||
|
(obj) => {
|
||||||
|
if (obj.matchID == resp.matchID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.handle('inplayStatusChanged', resp => {
|
||||||
|
//console.log('inplayStatusChanged:', resp)
|
||||||
|
interesting.partialUpdateBy(
|
||||||
|
(tags, obj) => {
|
||||||
|
if (resp.isInplay) {
|
||||||
|
tags.isInplay.textContent = 'inplay'
|
||||||
|
} else {
|
||||||
|
tags.isInplay.textContent = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.isLinkedToOnexbet) {
|
||||||
|
tags.isLinked.textContent = 'linked to 1xBet'
|
||||||
|
} else {
|
||||||
|
tags.isLinked.textContent = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(obj) => {
|
||||||
|
if (obj.matchID == resp.matchID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
ws.handle('footballMatchReports', resp => {
|
||||||
|
console.log('reports:', resp)
|
||||||
|
updateChamp(resp)
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listSportTips',
|
||||||
|
data: Football,
|
||||||
|
onSuccess: resp => {
|
||||||
|
tipModel.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listStrategiesReports',
|
||||||
|
data: Football,
|
||||||
|
onSuccess: resp => {
|
||||||
|
strategiesModel.setList(resp)
|
||||||
|
showTotal(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'getSportRatedReport',
|
||||||
|
data: Football,
|
||||||
|
onSuccess: resp => {
|
||||||
|
showRatedTotal(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onexbetWs.connect()
|
||||||
|
tipperWs.connect()
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
function showRatedTotal(report) {
|
||||||
|
document.getElementById('ratedTotalsWonTips').textContent = report.wonTips
|
||||||
|
document.getElementById('ratedTotalsLostTips').textContent = report.lostTips
|
||||||
|
document.getElementById('ratedTotalsAccuracy').textContent = Math.round(report.accuracy*100)/100
|
||||||
|
document.getElementById('ratedTotalsTurnover').textContent = report.turnover
|
||||||
|
document.getElementById('ratedTotalsProfit').textContent = Math.round(report.profit*100)/100
|
||||||
|
document.getElementById('ratedTotalsROI').textContent = Math.round(report.roi*100)/100
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function showTotal(reports) {
|
||||||
|
// 1,2,3
|
||||||
|
let allChampsTotal = {
|
||||||
|
name: 'Все чемпионаты',
|
||||||
|
wonTips: 0,
|
||||||
|
lostTips: 0,
|
||||||
|
accuracy: 0,
|
||||||
|
profit: 0,
|
||||||
|
turnover: 0,
|
||||||
|
roi: 0
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
let topChampsTotal = {
|
||||||
|
name: 'Топ чемпионаты',
|
||||||
|
wonTips: 0,
|
||||||
|
lostTips: 0,
|
||||||
|
accuracy: 0,
|
||||||
|
profit: 0,
|
||||||
|
turnover: 0,
|
||||||
|
roi: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let myTotal = {
|
||||||
|
name: 'Алгоритмы от Программиста',
|
||||||
|
wonTips: 0,
|
||||||
|
lostTips: 0,
|
||||||
|
accuracy: 0,
|
||||||
|
profit: 0,
|
||||||
|
turnover: 0,
|
||||||
|
roi: 0
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let m = new Map()
|
||||||
|
|
||||||
|
|
||||||
|
//m.set(1, allChampsTotal)
|
||||||
|
//m.set(2, allChampsTotal)
|
||||||
|
//m.set(3, allChampsTotal)
|
||||||
|
m.set(6, allChampsTotal)
|
||||||
|
m.set(7, allChampsTotal)
|
||||||
|
m.set(8, allChampsTotal)
|
||||||
|
|
||||||
|
|
||||||
|
if (Array.isArray(reports)) {
|
||||||
|
reports.forEach(r => {
|
||||||
|
let total = m.get(r.strategyID)
|
||||||
|
if (!total) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
total.wonTips += r.wonTips
|
||||||
|
total.lostTips += r.lostTips
|
||||||
|
total.profit += r.profit
|
||||||
|
total.turnover += r.turnover
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clacROIandAccuracy(allChampsTotal)
|
||||||
|
//clacROIandAccuracy(topChampsTotal)
|
||||||
|
//clacROIandAccuracy(myTotal)
|
||||||
|
|
||||||
|
|
||||||
|
//totalsModel.setList([allChampsTotal, topChampsTotal, myTotal])
|
||||||
|
totalsModel.setList([allChampsTotal])
|
||||||
|
|
||||||
|
//document.getElementById('total').textContent = 'Общий итог: ' + total + ' грн'
|
||||||
|
}
|
||||||
|
|
||||||
|
function clacROIandAccuracy(total) {
|
||||||
|
total.roi = (total.profit * 100) / total.turnover
|
||||||
|
total.accuracy = (total.wonTips * 100) / (total.wonTips + total.lostTips)
|
||||||
|
|
||||||
|
//total.roi = Math.round(total.roi*100)/100
|
||||||
|
//total.accuracy = Math.round(total.accuracy*100)/100
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function modifyReport(report) {
|
||||||
|
report.match = `${report.home.canonicalName} - ${report.away.canonicalName}`
|
||||||
|
report.msg = JSON.stringify(report, null, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function modifyInterestingMatch(report) {
|
||||||
|
//console.log(report)
|
||||||
|
|
||||||
|
report.champName = `${report.zone.name}. ${report.champ.name}`
|
||||||
|
report.startTimeStr = moment(report.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
let home = report.home.canonicalName
|
||||||
|
if (!report.home.wasLinked) {
|
||||||
|
home = '*' + home
|
||||||
|
}
|
||||||
|
let away = report.away.canonicalName
|
||||||
|
if (!report.away.wasLinked) {
|
||||||
|
away = '*' + away
|
||||||
|
}
|
||||||
|
report.match = `${home} - ${away}`
|
||||||
|
if (report.isLinkedToOnexbet) {
|
||||||
|
report.isLinked = 'linked to 1xBet'
|
||||||
|
}
|
||||||
|
if (report.isInplay) {
|
||||||
|
report.isInplay = 'inplay'
|
||||||
|
} else {
|
||||||
|
report.isInplay = ''
|
||||||
|
}
|
||||||
|
report.notes = JSON.stringify(report, null, 3)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function modifyLiveMatch(report) {
|
||||||
|
if (!report.match) {
|
||||||
|
report.champName = `${report.sportName}. ${report.champName}`
|
||||||
|
}
|
||||||
|
report.startTimeStr = moment(report.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
report.match = `${report.home} - ${report.away}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function modifyTip(report) {
|
||||||
|
report.msg = JSON.stringify(report, null, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
function footballScoreToString(scoreByPeriods) {
|
||||||
|
if (!scoreByPeriods) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let list = []
|
||||||
|
|
||||||
|
for (var i=1; i <= 2; i++) {
|
||||||
|
let p = scoreByPeriods[i]
|
||||||
|
if (p) {
|
||||||
|
list.push(`${p.home}-${p.away}`)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.join(' ')
|
||||||
|
}
|
||||||
3550
static/form2.js
Normal file
3550
static/form2.js
Normal file
File diff suppressed because it is too large
Load Diff
2324
static/formobj.js
Executable file
2324
static/formobj.js
Executable file
File diff suppressed because it is too large
Load Diff
620
static/handball.js
Normal file
620
static/handball.js
Normal file
@@ -0,0 +1,620 @@
|
|||||||
|
let u = new URL(location.href)
|
||||||
|
urlPrivateAPI = u.origin + '/api'
|
||||||
|
|
||||||
|
api = new API({
|
||||||
|
url: urlPrivateAPI,
|
||||||
|
timeout: 10000, // 10 sec
|
||||||
|
})
|
||||||
|
|
||||||
|
Football = 1
|
||||||
|
Handball = 2
|
||||||
|
|
||||||
|
OnexbetFootball = 1
|
||||||
|
OnexbetHandball = 8
|
||||||
|
|
||||||
|
onexbetWsURL = 'ws://195.189.227.88:7772/ws'
|
||||||
|
tipperWsURL = 'ws://195.189.227.88:33465/ws'
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', ()=> {
|
||||||
|
interesting = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'interestingMatch',
|
||||||
|
renders: {
|
||||||
|
interestingMatch: (tags, match) => {
|
||||||
|
//console.log('render', match)
|
||||||
|
let radios = new RadioList({
|
||||||
|
container: tags.candidates,
|
||||||
|
templateId: 'candidate-item',
|
||||||
|
setEmptyMessage: 'нет матчей кандидатов'
|
||||||
|
})
|
||||||
|
|
||||||
|
tags.flashscoreLink.href= `https://www.flashscore.com/match/${match.matchID}/#match-summary/match-summary`
|
||||||
|
|
||||||
|
tags.search.addEventListener('click', () => {
|
||||||
|
let teamName = tags.teamName.value.trim()
|
||||||
|
|
||||||
|
if (teamName == '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'searchMatchCandidatesByTeamName',
|
||||||
|
data: {
|
||||||
|
sportID: Handball,
|
||||||
|
teamName: teamName,
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
console.log(err)
|
||||||
|
tags.candidates.textContent = "FUCK: " + err
|
||||||
|
},
|
||||||
|
onSuccess: resp => {
|
||||||
|
tags.submit.disabled = false
|
||||||
|
radios.clear()
|
||||||
|
|
||||||
|
console.log(resp)
|
||||||
|
if (Array.isArray(resp)) {
|
||||||
|
resp.forEach(candidate => {
|
||||||
|
candidate.startTimeStr = moment(candidate.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// show model + form
|
||||||
|
radios.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tags.submit.addEventListener('click', () => {
|
||||||
|
//console.log(radios.getSelected())
|
||||||
|
let candidate = radios.getSelected()
|
||||||
|
if (!candidate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let isReverse = tags.isReverse.checked
|
||||||
|
|
||||||
|
var home, away;
|
||||||
|
|
||||||
|
if (isReverse) {
|
||||||
|
home = {
|
||||||
|
teamID: match.home.teamID,
|
||||||
|
name: candidate.away
|
||||||
|
}
|
||||||
|
away = {
|
||||||
|
teamID: match.away.teamID,
|
||||||
|
name: candidate.home
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
home = {
|
||||||
|
teamID: match.home.teamID,
|
||||||
|
name: candidate.home
|
||||||
|
}
|
||||||
|
away = {
|
||||||
|
teamID: match.away.teamID,
|
||||||
|
name: candidate.away
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('home:', home);
|
||||||
|
console.log('away:', away);
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'addTeamsAliases',
|
||||||
|
data: {
|
||||||
|
home: home,
|
||||||
|
away: away
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
//tags.candidates.textContent = 'FUCK: ' + err
|
||||||
|
console.log(err)
|
||||||
|
},
|
||||||
|
onSuccess: resp => {
|
||||||
|
//console.log('ok')
|
||||||
|
//tags.identification.click()
|
||||||
|
radios.clear()
|
||||||
|
tags.isReverse.checked = false
|
||||||
|
tags.candidates.textContent = "Алиасы добавлены!"
|
||||||
|
tags.submit.disabled = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tags.identification.addEventListener('click', () => {
|
||||||
|
//console.log('details:', tags.identification.open)
|
||||||
|
//console.log(match)
|
||||||
|
if (tags.identification.open) {
|
||||||
|
//tags.candidates.innerHTML = ''
|
||||||
|
} else {
|
||||||
|
//tags.candidates.innerHTML = ''
|
||||||
|
radios.clear()
|
||||||
|
|
||||||
|
//radios.setList([
|
||||||
|
// {home: 'Arsenal', away: 'Mancity', sport: 'Football', champName: 'England. Premier League', startTimeStr: "Oct 06, 12:30:00"},
|
||||||
|
// {home: 'Arsenal', away: 'Aston villa', sport: 'Football', champName: 'England. Premier League', startTimeStr: "Oct 06, 12:30:00"},
|
||||||
|
//])
|
||||||
|
|
||||||
|
//tags.candidates.textContent = JSON.stringify(match)
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'searchMatchCandidates',
|
||||||
|
data: {
|
||||||
|
sportID: Handball,
|
||||||
|
home: match.home.canonicalName,
|
||||||
|
away: match.away.canonicalName
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
console.log(err)
|
||||||
|
tags.candidates.textContent = "FUCK: " + err
|
||||||
|
},
|
||||||
|
onSuccess: resp => {
|
||||||
|
tags.submit.disabled = false
|
||||||
|
console.log(resp)
|
||||||
|
if (Array.isArray(resp)) {
|
||||||
|
resp.forEach(candidate => {
|
||||||
|
candidate.startTimeStr = moment(candidate.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// show model + form
|
||||||
|
radios.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
container: 'interesting',
|
||||||
|
preFunc: modifyInterestingMatch,
|
||||||
|
sort: {
|
||||||
|
key: 'startTime',
|
||||||
|
reverse: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ONEXBET
|
||||||
|
|
||||||
|
//1xbet
|
||||||
|
bookLiveModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'liveMatch',
|
||||||
|
},
|
||||||
|
container: 'book-live',
|
||||||
|
preFunc: modifyLiveMatch,
|
||||||
|
sort: {
|
||||||
|
key: 'startTime',
|
||||||
|
reverse: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// tips
|
||||||
|
|
||||||
|
tipModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'tip',
|
||||||
|
renders: {
|
||||||
|
tip: (tags, tip) => {
|
||||||
|
//console.log('tags:', tags)
|
||||||
|
//console.log('x:', x)
|
||||||
|
if (tip.status == 1) {
|
||||||
|
tags.matchName.classList.add('profit')
|
||||||
|
} else if (tip.status == 2) {
|
||||||
|
tags.matchName.classList.add('errmsg')
|
||||||
|
} else if (tip.status == 3) {
|
||||||
|
tags.matchName.classList.add('void')
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.flashscoreLink.href= `https://www.flashscore.com/match/${tip.matchID}/#match-summary/match-summary`
|
||||||
|
|
||||||
|
tags.setResultLink.addEventListener('click', () => {
|
||||||
|
if (tags.setResultForm.hidden) {
|
||||||
|
let f = new Form({
|
||||||
|
formContainer: tags.setResultForm,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
name: 'result',
|
||||||
|
type: 'line',
|
||||||
|
label: 'Результат',
|
||||||
|
comment: 'например, 1й тайм: 1:1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
type: 'radios',
|
||||||
|
label: ' ',
|
||||||
|
getName: x => x.name,
|
||||||
|
data: [
|
||||||
|
{name: 'Выигрыш', status: 1},
|
||||||
|
{name: 'Проигрыш', status: 2},
|
||||||
|
{name: 'Возврат', status: 3},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
f.onSubmit((obj) => {
|
||||||
|
obj.tipID = tip.tipID
|
||||||
|
//console.log(obj)
|
||||||
|
api.req({
|
||||||
|
func: 'setTipResult',
|
||||||
|
data: obj,
|
||||||
|
onError: err => {
|
||||||
|
f.showError(err)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
// Перезагружаем список
|
||||||
|
api.req({
|
||||||
|
func: 'listSportTips',
|
||||||
|
data: Handball,
|
||||||
|
onSuccess: resp => {
|
||||||
|
tipModel.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
f.init()
|
||||||
|
|
||||||
|
tags.setResultForm.hidden = false
|
||||||
|
} else {
|
||||||
|
tags.setResultForm.hidden = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
container: 'tips',
|
||||||
|
preFunc: modifyTip,
|
||||||
|
sort: {
|
||||||
|
key: 'tipTime',
|
||||||
|
//reverse: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// strategies
|
||||||
|
|
||||||
|
strategiesModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'strategy',
|
||||||
|
},
|
||||||
|
container: 'strategies',
|
||||||
|
preFunc: (r) => {
|
||||||
|
r.roi = Math.round(r.roi*100)/100
|
||||||
|
r.profit = Math.round(r.profit*100)/100
|
||||||
|
r.drawdown = Math.round(r.drawdown*100)/100
|
||||||
|
r.accuracy = Math.round(r.accuracy*100)/100
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
totalsModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'total',
|
||||||
|
},
|
||||||
|
container: 'totals',
|
||||||
|
preFunc: (r) => {
|
||||||
|
r.roi = Math.round(r.roi*100)/100
|
||||||
|
r.profit = Math.round(r.profit*100)/100
|
||||||
|
r.drawdown = Math.round(r.drawdown*100)/100
|
||||||
|
r.accuracy = Math.round(r.accuracy*100)/100
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let onexbetWs = new WebsocketClient({
|
||||||
|
url: onexbetWsURL
|
||||||
|
})
|
||||||
|
|
||||||
|
onexbetWs.handle('liveMatches', resp => {
|
||||||
|
//console.log('liveMatches:', resp)
|
||||||
|
|
||||||
|
if (resp.sportID == OnexbetHandball) {
|
||||||
|
if (Array.isArray(resp.matches)) {
|
||||||
|
bookLiveModel.setList(resp.matches)
|
||||||
|
|
||||||
|
resp.matches.forEach(match => {
|
||||||
|
interesting.partialUpdateBy(
|
||||||
|
// update func
|
||||||
|
(tags, obj) => {
|
||||||
|
tags.livescore.textContent = handballScoreToString(match.scoreByPeriods)
|
||||||
|
tags.livescore.hidden = false
|
||||||
|
},
|
||||||
|
// key func
|
||||||
|
(obj) => {
|
||||||
|
if (obj.onexbetMatchID == match.matchID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onexbetWs.handle('teamLiveMatchData', resp => {
|
||||||
|
//console.log('teamLiveMatchData:', resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// tipper
|
||||||
|
let tipperWs = new WebsocketClient({
|
||||||
|
url: tipperWsURL
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.onConnected = () => {
|
||||||
|
tipperWs.send('subscribe', {channels: ['handball']})
|
||||||
|
}
|
||||||
|
|
||||||
|
tipperWs.handle('teamMatches', resp => {
|
||||||
|
//console.log('reports:', resp)
|
||||||
|
if (resp.sportID == Handball) {
|
||||||
|
if (Array.isArray(resp.matches)) {
|
||||||
|
//console.log('set interesting:', resp.matches)
|
||||||
|
interesting.setList(resp.matches)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.handle('tipsChanged', resp => {
|
||||||
|
//console.log('tip:', resp)
|
||||||
|
|
||||||
|
//tipModel.add(resp)
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listSportTips',
|
||||||
|
data: Handball,
|
||||||
|
onSuccess: resp => {
|
||||||
|
tipModel.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listStrategiesReports',
|
||||||
|
data: Handball,
|
||||||
|
onSuccess: resp => {
|
||||||
|
strategiesModel.setList(filterStrategies(resp))
|
||||||
|
showTotal(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.handle('whyNot', resp => {
|
||||||
|
//console.log('whyNot:', resp.matchID, resp.comment)
|
||||||
|
interesting.partialUpdateBy(
|
||||||
|
(tags, obj) => {
|
||||||
|
tags.comment.innerHTML = resp.comment
|
||||||
|
tags.time.textContent = resp.time
|
||||||
|
tags.score.textContent = `${resp.homeGoals} - ${resp.awayGoals}`
|
||||||
|
//tags.gamePace.textContent = resp.gamePace
|
||||||
|
//tags.gamePaceFrame.textContent = resp.gamePaceFrame + 1
|
||||||
|
},
|
||||||
|
(obj) => {
|
||||||
|
if (obj.matchID == resp.matchID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.handle('inplayStatusChanged', resp => {
|
||||||
|
//console.log('inplayStatusChanged:', resp)
|
||||||
|
interesting.partialUpdateBy(
|
||||||
|
(tags, obj) => {
|
||||||
|
if (resp.isInplay) {
|
||||||
|
tags.isInplay.textContent = 'inplay'
|
||||||
|
} else {
|
||||||
|
tags.isInplay.textContent = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.isLinkedToOnexbet) {
|
||||||
|
tags.isLinked.textContent = 'linked to 1xBet'
|
||||||
|
} else {
|
||||||
|
tags.isLinked.textContent = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(obj) => {
|
||||||
|
if (obj.matchID == resp.matchID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
ws.handle('footballMatchReports', resp => {
|
||||||
|
console.log('reports:', resp)
|
||||||
|
updateChamp(resp)
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listSportTips',
|
||||||
|
data: Handball,
|
||||||
|
onSuccess: resp => {
|
||||||
|
tipModel.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listStrategiesReports',
|
||||||
|
data: Handball,
|
||||||
|
onSuccess: resp => {
|
||||||
|
strategiesModel.setList(filterStrategies(resp))
|
||||||
|
showTotal(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'getSportRatedReport',
|
||||||
|
data: Handball,
|
||||||
|
onSuccess: resp => {
|
||||||
|
showRatedTotal(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onexbetWs.connect()
|
||||||
|
tipperWs.connect()
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function showRatedTotal(report) {
|
||||||
|
document.getElementById('ratedTotalsWonTips').textContent = report.wonTips
|
||||||
|
document.getElementById('ratedTotalsLostTips').textContent = report.lostTips
|
||||||
|
document.getElementById('ratedTotalsAccuracy').textContent = Math.round(report.accuracy*100)/100
|
||||||
|
document.getElementById('ratedTotalsTurnover').textContent = report.turnover
|
||||||
|
document.getElementById('ratedTotalsProfit').textContent = Math.round(report.profit*100)/100
|
||||||
|
document.getElementById('ratedTotalsROI').textContent = Math.round(report.roi*100)/100
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function showTotal(reports) {
|
||||||
|
// 1,2,3
|
||||||
|
let allChampsTotal = {
|
||||||
|
name: 'Все чемпионаты',
|
||||||
|
wonTips: 0,
|
||||||
|
lostTips: 0,
|
||||||
|
accuracy: 0,
|
||||||
|
profit: 0,
|
||||||
|
turnover: 0,
|
||||||
|
roi: 0
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
let topChampsTotal = {
|
||||||
|
name: 'Топ чемпионаты',
|
||||||
|
wonTips: 0,
|
||||||
|
lostTips: 0,
|
||||||
|
accuracy: 0,
|
||||||
|
profit: 0,
|
||||||
|
turnover: 0,
|
||||||
|
roi: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let myTotal = {
|
||||||
|
name: 'Алгоритмы от Программиста',
|
||||||
|
wonTips: 0,
|
||||||
|
lostTips: 0,
|
||||||
|
accuracy: 0,
|
||||||
|
profit: 0,
|
||||||
|
turnover: 0,
|
||||||
|
roi: 0
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let m = new Map()
|
||||||
|
|
||||||
|
m.set(4, allChampsTotal)
|
||||||
|
m.set(5, allChampsTotal)
|
||||||
|
|
||||||
|
m.set(11, allChampsTotal)
|
||||||
|
m.set(12, allChampsTotal)
|
||||||
|
|
||||||
|
if (Array.isArray(reports)) {
|
||||||
|
reports.forEach(r => {
|
||||||
|
let total = m.get(r.strategyID)
|
||||||
|
if (!total) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
total.wonTips += r.wonTips
|
||||||
|
total.lostTips += r.lostTips
|
||||||
|
total.profit += r.profit
|
||||||
|
total.turnover += r.turnover
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clacROIandAccuracy(allChampsTotal)
|
||||||
|
//clacROIandAccuracy(topChampsTotal)
|
||||||
|
//clacROIandAccuracy(myTotal)
|
||||||
|
|
||||||
|
|
||||||
|
//totalsModel.setList([allChampsTotal, topChampsTotal, myTotal])
|
||||||
|
totalsModel.setList([allChampsTotal])
|
||||||
|
|
||||||
|
//document.getElementById('total').textContent = 'Общий итог: ' + total + ' грн'
|
||||||
|
}
|
||||||
|
|
||||||
|
function clacROIandAccuracy(total) {
|
||||||
|
total.roi = (total.profit * 100) / total.turnover
|
||||||
|
total.accuracy = (total.wonTips * 100) / (total.wonTips + total.lostTips)
|
||||||
|
|
||||||
|
//total.roi = Math.round(total.roi*100)/100
|
||||||
|
//total.accuracy = Math.round(total.accuracy*100)/100
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function modifyReport(report) {
|
||||||
|
report.match = `${report.home.canonicalName} - ${report.away.canonicalName}`
|
||||||
|
report.msg = JSON.stringify(report, null, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function modifyInterestingMatch(report) {
|
||||||
|
//console.log(report)
|
||||||
|
|
||||||
|
report.champName = `${report.zone.name}. ${report.champ.name}`
|
||||||
|
report.startTimeStr = moment(report.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
let home = report.home.canonicalName
|
||||||
|
if (!report.home.wasLinked) {
|
||||||
|
home = '*' + home
|
||||||
|
}
|
||||||
|
let away = report.away.canonicalName
|
||||||
|
if (!report.away.wasLinked) {
|
||||||
|
away = '*' + away
|
||||||
|
}
|
||||||
|
report.match = `${home} - ${away}`
|
||||||
|
if (report.isLinkedToOnexbet) {
|
||||||
|
report.isLinked = 'linked to 1xBet'
|
||||||
|
}
|
||||||
|
if (report.isInplay) {
|
||||||
|
report.isInplay = 'inplay'
|
||||||
|
} else {
|
||||||
|
report.isInplay = ''
|
||||||
|
}
|
||||||
|
report.notes = JSON.stringify(report, null, 3)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function modifyLiveMatch(report) {
|
||||||
|
if (!report.match) {
|
||||||
|
report.champName = `${report.sportName}. ${report.champName}`
|
||||||
|
}
|
||||||
|
report.startTimeStr = moment(report.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
report.match = `${report.home} - ${report.away}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function modifyTip(report) {
|
||||||
|
report.msg = JSON.stringify(report, null, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterStrategies(src) {
|
||||||
|
let list = []
|
||||||
|
if (Array.isArray(src)) {
|
||||||
|
src.forEach(s => {
|
||||||
|
list.push(s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
function handballScoreToString(scoreByPeriods) {
|
||||||
|
if (!scoreByPeriods) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let list = []
|
||||||
|
|
||||||
|
for (var i=1; i <= 3; i++) {
|
||||||
|
let p = scoreByPeriods[i]
|
||||||
|
if (p) {
|
||||||
|
list.push(`${p.home}:${p.away}`)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.join(' ')
|
||||||
|
}
|
||||||
4602
static/moment.js
Executable file
4602
static/moment.js
Executable file
File diff suppressed because it is too large
Load Diff
2519
static/omg.js
Executable file
2519
static/omg.js
Executable file
File diff suppressed because it is too large
Load Diff
105
static/stats.js
Executable file
105
static/stats.js
Executable file
@@ -0,0 +1,105 @@
|
|||||||
|
let u = new URL(location.href)
|
||||||
|
urlPrivateAPI = u.origin + '/api'
|
||||||
|
|
||||||
|
api = new API({
|
||||||
|
url: urlPrivateAPI,
|
||||||
|
timeout: 10000, // 10 sec
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', ()=> {
|
||||||
|
google.charts.load('current', {'packages':['corechart']});
|
||||||
|
|
||||||
|
let model = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'champ',
|
||||||
|
},
|
||||||
|
container: 'champs',
|
||||||
|
preFunc: (r) => {
|
||||||
|
r.champ = `${r.zoneName}. ${r.champName}`
|
||||||
|
r.accuracy = (r.won * 100) / (r.won + r.lost)
|
||||||
|
|
||||||
|
r.avgPrice = Math.round(r.avgPrice*100)/100
|
||||||
|
r.accuracy = Math.round(r.accuracy*100)/100
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let s = new Select({
|
||||||
|
field: document.getElementById('strategyID'),
|
||||||
|
key: 'strategyID',
|
||||||
|
name: 'name'
|
||||||
|
})
|
||||||
|
|
||||||
|
s.on('select', strategyID => {
|
||||||
|
if (strategyID) {
|
||||||
|
api.req({
|
||||||
|
func: 'listStrategyChampStats',
|
||||||
|
data: strategyID,
|
||||||
|
onSuccess: resp => {
|
||||||
|
model.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
model.clear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
s.on('select', strategyID => {
|
||||||
|
if (strategyID) {
|
||||||
|
api.req({
|
||||||
|
func: 'getStrategyHistory',
|
||||||
|
data: strategyID,
|
||||||
|
onSuccess: resp => {
|
||||||
|
var rows = []
|
||||||
|
if (Array.isArray(resp)) {
|
||||||
|
resp.forEach(item => {
|
||||||
|
rows.push([
|
||||||
|
new Date(item.date * 1000),
|
||||||
|
item.profit
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
drawChart(strategyID, rows)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
drawChart(0, [])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listStrategies',
|
||||||
|
onSuccess: resp => {
|
||||||
|
s.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//google.charts.setOnLoadCallback(drawChart);
|
||||||
|
|
||||||
|
function drawChart(strategyID, rows) {
|
||||||
|
var data = new google.visualization.DataTable();
|
||||||
|
data.addColumn('date', 'Date');
|
||||||
|
data.addColumn('number', 'Алгоритм ' + strategyID);
|
||||||
|
//data.addColumn('number', 'Алгоритм 2');
|
||||||
|
//data.addColumn('number', 'Алгоритм 3');
|
||||||
|
|
||||||
|
console.log(rows)
|
||||||
|
|
||||||
|
data.addRows(rows)
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
chart: {
|
||||||
|
title: 'Кривая доходности',
|
||||||
|
},
|
||||||
|
width: 900,
|
||||||
|
height: 500
|
||||||
|
};
|
||||||
|
var chart = new google.visualization.LineChart(document.getElementById('curve_chart'));
|
||||||
|
|
||||||
|
//chart.draw(data, google.charts.Line.convertOptions(options));
|
||||||
|
chart.draw(data, options);
|
||||||
|
}
|
||||||
105
static/temp_stats.js
Normal file
105
static/temp_stats.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
let u = new URL(location.href)
|
||||||
|
urlPrivateAPI = u.origin + '/api'
|
||||||
|
|
||||||
|
api = new API({
|
||||||
|
url: urlPrivateAPI,
|
||||||
|
timeout: 10000, // 10 sec
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', ()=> {
|
||||||
|
google.charts.load('current', {'packages':['corechart']});
|
||||||
|
|
||||||
|
let model = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'champ',
|
||||||
|
},
|
||||||
|
container: 'champs',
|
||||||
|
preFunc: (r) => {
|
||||||
|
r.champ = `${r.zoneName}. ${r.champName}`
|
||||||
|
r.accuracy = (r.won * 100) / (r.won + r.lost)
|
||||||
|
|
||||||
|
r.avgPrice = Math.round(r.avgPrice*100)/100
|
||||||
|
r.accuracy = Math.round(r.accuracy*100)/100
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let s = new Select({
|
||||||
|
field: document.getElementById('strategyID'),
|
||||||
|
key: 'strategyID',
|
||||||
|
name: 'name'
|
||||||
|
})
|
||||||
|
|
||||||
|
s.on('select', strategyID => {
|
||||||
|
if (strategyID) {
|
||||||
|
api.req({
|
||||||
|
func: 'listStrategyRatedChampStats',
|
||||||
|
data: strategyID,
|
||||||
|
onSuccess: resp => {
|
||||||
|
model.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
model.clear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
s.on('select', strategyID => {
|
||||||
|
if (strategyID) {
|
||||||
|
api.req({
|
||||||
|
func: 'getStrategyRatedHistory',
|
||||||
|
data: strategyID,
|
||||||
|
onSuccess: resp => {
|
||||||
|
var rows = []
|
||||||
|
if (Array.isArray(resp)) {
|
||||||
|
resp.forEach(item => {
|
||||||
|
rows.push([
|
||||||
|
new Date(item.date * 1000),
|
||||||
|
item.profit
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
drawChart(strategyID, rows)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
drawChart(0, [])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listStrategies',
|
||||||
|
onSuccess: resp => {
|
||||||
|
s.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//google.charts.setOnLoadCallback(drawChart);
|
||||||
|
|
||||||
|
function drawChart(strategyID, rows) {
|
||||||
|
var data = new google.visualization.DataTable();
|
||||||
|
data.addColumn('date', 'Date');
|
||||||
|
data.addColumn('number', 'Алгоритм ' + strategyID);
|
||||||
|
//data.addColumn('number', 'Алгоритм 2');
|
||||||
|
//data.addColumn('number', 'Алгоритм 3');
|
||||||
|
|
||||||
|
console.log(rows)
|
||||||
|
|
||||||
|
data.addRows(rows)
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
chart: {
|
||||||
|
title: 'Кривая доходности',
|
||||||
|
},
|
||||||
|
width: 900,
|
||||||
|
height: 500
|
||||||
|
};
|
||||||
|
var chart = new google.visualization.LineChart(document.getElementById('curve_chart'));
|
||||||
|
|
||||||
|
//chart.draw(data, google.charts.Line.convertOptions(options));
|
||||||
|
chart.draw(data, options);
|
||||||
|
}
|
||||||
605
static/tennis.js
Normal file
605
static/tennis.js
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
let u = new URL(location.href)
|
||||||
|
urlPrivateAPI = u.origin + '/api'
|
||||||
|
|
||||||
|
api = new API({
|
||||||
|
url: urlPrivateAPI,
|
||||||
|
timeout: 10000, // 10 sec
|
||||||
|
})
|
||||||
|
|
||||||
|
Football = 1
|
||||||
|
Handball = 2
|
||||||
|
Tennis = 3
|
||||||
|
|
||||||
|
OnexbetFootball = 1
|
||||||
|
OnexbetHandball = 8
|
||||||
|
OnexbetTennis = 4
|
||||||
|
|
||||||
|
onexbetWsURL = 'ws://195.189.227.88:7772/ws'
|
||||||
|
tipperWsURL = 'ws://195.189.227.88:7768/ws'
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', ()=> {
|
||||||
|
interesting = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'interestingMatch',
|
||||||
|
renders: {
|
||||||
|
interestingMatch: (tags, match) => {
|
||||||
|
//console.log('render', match)
|
||||||
|
let radios = new RadioList({
|
||||||
|
container: tags.candidates,
|
||||||
|
templateId: 'candidate-item',
|
||||||
|
setEmptyMessage: 'нет матчей кандидатов'
|
||||||
|
})
|
||||||
|
|
||||||
|
tags.flashscoreLink.href= `https://www.flashscore.com/match/${match.matchID}/#match-summary/match-summary`
|
||||||
|
|
||||||
|
tags.search.addEventListener('click', () => {
|
||||||
|
let teamName = tags.teamName.value.trim()
|
||||||
|
|
||||||
|
if (teamName == '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'searchMatchCandidatesByTeamName',
|
||||||
|
data: {
|
||||||
|
sportID: Tennis,
|
||||||
|
teamName: teamName,
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
console.log(err)
|
||||||
|
tags.candidates.textContent = "FUCK: " + err
|
||||||
|
},
|
||||||
|
onSuccess: resp => {
|
||||||
|
tags.submit.disabled = false
|
||||||
|
radios.clear()
|
||||||
|
|
||||||
|
console.log(resp)
|
||||||
|
if (Array.isArray(resp)) {
|
||||||
|
resp.forEach(candidate => {
|
||||||
|
candidate.startTimeStr = moment(candidate.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// show model + form
|
||||||
|
radios.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tags.submit.addEventListener('click', () => {
|
||||||
|
//console.log(radios.getSelected())
|
||||||
|
let candidate = radios.getSelected()
|
||||||
|
if (!candidate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let isReverse = tags.isReverse.checked
|
||||||
|
|
||||||
|
var home, away;
|
||||||
|
|
||||||
|
if (isReverse) {
|
||||||
|
home = {
|
||||||
|
teamID: match.home.teamID,
|
||||||
|
name: candidate.away
|
||||||
|
}
|
||||||
|
away = {
|
||||||
|
teamID: match.away.teamID,
|
||||||
|
name: candidate.home
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
home = {
|
||||||
|
teamID: match.home.teamID,
|
||||||
|
name: candidate.home
|
||||||
|
}
|
||||||
|
away = {
|
||||||
|
teamID: match.away.teamID,
|
||||||
|
name: candidate.away
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('home:', home);
|
||||||
|
console.log('away:', away);
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'addTeamsAliases',
|
||||||
|
data: {
|
||||||
|
home: home,
|
||||||
|
away: away
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
//tags.candidates.textContent = 'FUCK: ' + err
|
||||||
|
console.log(err)
|
||||||
|
},
|
||||||
|
onSuccess: resp => {
|
||||||
|
//console.log('ok')
|
||||||
|
//tags.identification.click()
|
||||||
|
radios.clear()
|
||||||
|
tags.isReverse.checked = false
|
||||||
|
tags.candidates.textContent = "Алиасы добавлены!"
|
||||||
|
tags.submit.disabled = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tags.identification.addEventListener('click', () => {
|
||||||
|
//console.log('details:', tags.identification.open)
|
||||||
|
//console.log(match)
|
||||||
|
if (tags.identification.open) {
|
||||||
|
//tags.candidates.innerHTML = ''
|
||||||
|
} else {
|
||||||
|
//tags.candidates.innerHTML = ''
|
||||||
|
radios.clear()
|
||||||
|
|
||||||
|
//radios.setList([
|
||||||
|
// {home: 'Arsenal', away: 'Mancity', sport: 'Football', champName: 'England. Premier League', startTimeStr: "Oct 06, 12:30:00"},
|
||||||
|
// {home: 'Arsenal', away: 'Aston villa', sport: 'Football', champName: 'England. Premier League', startTimeStr: "Oct 06, 12:30:00"},
|
||||||
|
//])
|
||||||
|
|
||||||
|
//tags.candidates.textContent = JSON.stringify(match)
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'searchMatchCandidates',
|
||||||
|
data: {
|
||||||
|
sportID: Tennis,
|
||||||
|
home: match.home.canonicalName,
|
||||||
|
away: match.away.canonicalName
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
console.log(err)
|
||||||
|
tags.candidates.textContent = "FUCK: " + err
|
||||||
|
},
|
||||||
|
onSuccess: resp => {
|
||||||
|
tags.submit.disabled = false
|
||||||
|
console.log(resp)
|
||||||
|
if (Array.isArray(resp)) {
|
||||||
|
resp.forEach(candidate => {
|
||||||
|
candidate.startTimeStr = moment(candidate.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// show model + form
|
||||||
|
radios.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
container: 'interesting',
|
||||||
|
preFunc: modifyInterestingMatch,
|
||||||
|
sort: {
|
||||||
|
key: 'startTime',
|
||||||
|
reverse: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ONEXBET
|
||||||
|
|
||||||
|
//1xbet
|
||||||
|
bookLiveModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'liveMatch',
|
||||||
|
},
|
||||||
|
container: 'book-live',
|
||||||
|
preFunc: modifyLiveMatch,
|
||||||
|
sort: {
|
||||||
|
key: 'startTime',
|
||||||
|
reverse: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// tips
|
||||||
|
|
||||||
|
tipModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'tip',
|
||||||
|
renders: {
|
||||||
|
tip: (tags, tip) => {
|
||||||
|
//console.log('tags:', tags)
|
||||||
|
//console.log('x:', x)
|
||||||
|
if (tip.status == 1) {
|
||||||
|
tags.matchName.classList.add('profit')
|
||||||
|
} else if (tip.status == 2) {
|
||||||
|
tags.matchName.classList.add('errmsg')
|
||||||
|
} else if (tip.status == 3) {
|
||||||
|
tags.matchName.classList.add('void')
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.flashscoreLink.href= `https://www.flashscore.com/match/${tip.matchID}/#match-summary/match-summary`
|
||||||
|
|
||||||
|
tags.setResultLink.addEventListener('click', () => {
|
||||||
|
if (tags.setResultForm.hidden) {
|
||||||
|
let f = new Form({
|
||||||
|
formContainer: tags.setResultForm,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
name: 'result',
|
||||||
|
type: 'line',
|
||||||
|
label: 'Результат',
|
||||||
|
comment: 'например, 1й тайм: 1:1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
type: 'radios',
|
||||||
|
label: ' ',
|
||||||
|
getName: x => x.name,
|
||||||
|
data: [
|
||||||
|
{name: 'Выигрыш', status: 1},
|
||||||
|
{name: 'Проигрыш', status: 2},
|
||||||
|
{name: 'Возврат', status: 3},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
f.onSubmit((obj) => {
|
||||||
|
obj.tipID = tip.tipID
|
||||||
|
//console.log(obj)
|
||||||
|
api.req({
|
||||||
|
func: 'setTipResult',
|
||||||
|
data: obj,
|
||||||
|
onError: err => {
|
||||||
|
f.showError(err)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
// Перезагружаем список
|
||||||
|
api.req({
|
||||||
|
func: 'listSportTips',
|
||||||
|
data: Tennis,
|
||||||
|
onSuccess: resp => {
|
||||||
|
tipModel.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
f.init()
|
||||||
|
|
||||||
|
tags.setResultForm.hidden = false
|
||||||
|
} else {
|
||||||
|
tags.setResultForm.hidden = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
container: 'tips',
|
||||||
|
preFunc: modifyTip,
|
||||||
|
sort: {
|
||||||
|
key: 'tipTime',
|
||||||
|
//reverse: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// strategies
|
||||||
|
|
||||||
|
strategiesModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'strategy',
|
||||||
|
},
|
||||||
|
container: 'strategies',
|
||||||
|
preFunc: (r) => {
|
||||||
|
r.roi = Math.round(r.roi*100)/100
|
||||||
|
r.profit = Math.round(r.profit*100)/100
|
||||||
|
r.drawdown = Math.round(r.drawdown*100)/100
|
||||||
|
r.accuracy = Math.round(r.accuracy*100)/100
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
totalsModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'total',
|
||||||
|
},
|
||||||
|
container: 'totals',
|
||||||
|
preFunc: (r) => {
|
||||||
|
r.roi = Math.round(r.roi*100)/100
|
||||||
|
r.profit = Math.round(r.profit*100)/100
|
||||||
|
r.drawdown = Math.round(r.drawdown*100)/100
|
||||||
|
r.accuracy = Math.round(r.accuracy*100)/100
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let onexbetWs = new WebsocketClient({
|
||||||
|
url: onexbetWsURL
|
||||||
|
})
|
||||||
|
|
||||||
|
onexbetWs.handle('liveMatches', resp => {
|
||||||
|
//console.log('liveMatches:', resp)
|
||||||
|
|
||||||
|
if (resp.sportID == OnexbetTennis) {
|
||||||
|
if (Array.isArray(resp.matches)) {
|
||||||
|
bookLiveModel.setList(resp.matches)
|
||||||
|
|
||||||
|
resp.matches.forEach(match => {
|
||||||
|
//console.log('partialUpdateBy')
|
||||||
|
interesting.partialUpdateBy(
|
||||||
|
// update func
|
||||||
|
(tags, obj) => {
|
||||||
|
//console.log('SCORE: ', tennisScoreToString(match.scoreByPeriods))
|
||||||
|
tags.livescore.textContent = tennisScoreToString(match.scoreByPeriods)
|
||||||
|
tags.livescore.hidden = false
|
||||||
|
},
|
||||||
|
// key func
|
||||||
|
(obj) => {
|
||||||
|
//console.log('COMPARE:', obj.onexbetMatchID, '=', match.matchID)
|
||||||
|
if (obj.onexbetMatchID == match.matchID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onexbetWs.handle('teamLiveMatchData', resp => {
|
||||||
|
//console.log('teamLiveMatchData:', resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// tipper
|
||||||
|
let tipperWs = new WebsocketClient({
|
||||||
|
url: tipperWsURL
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.onConnected = () => {
|
||||||
|
tipperWs.send('subscribe', {channels: ['tennis']})
|
||||||
|
}
|
||||||
|
|
||||||
|
tipperWs.handle('teamMatches', resp => {
|
||||||
|
//console.log('reports:', resp)
|
||||||
|
if (resp.sportID == Tennis) {
|
||||||
|
if (Array.isArray(resp.matches)) {
|
||||||
|
//console.log('set interesting:', resp.matches)
|
||||||
|
interesting.setList(resp.matches)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.handle('tipsChanged', resp => {
|
||||||
|
//console.log('tip:', resp)
|
||||||
|
|
||||||
|
//tipModel.add(resp)
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listSportTips',
|
||||||
|
data: Tennis,
|
||||||
|
onSuccess: resp => {
|
||||||
|
tipModel.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listStrategiesReports',
|
||||||
|
data: Tennis,
|
||||||
|
onSuccess: resp => {
|
||||||
|
strategiesModel.setList(filterStrategies(resp))
|
||||||
|
showTotal(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.handle('whyNot', resp => {
|
||||||
|
//console.log('whyNot:', resp.matchID, resp.comment)
|
||||||
|
interesting.partialUpdateBy(
|
||||||
|
(tags, obj) => {
|
||||||
|
tags.comment.innerHTML = resp.comment
|
||||||
|
tags.time.textContent = resp.time
|
||||||
|
tags.score.textContent = `${resp.homeGoals} - ${resp.awayGoals}`
|
||||||
|
//tags.gamePace.textContent = resp.gamePace
|
||||||
|
//tags.gamePaceFrame.textContent = resp.gamePaceFrame + 1
|
||||||
|
},
|
||||||
|
(obj) => {
|
||||||
|
if (obj.matchID == resp.matchID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.handle('inplayStatusChanged', resp => {
|
||||||
|
//console.log('inplayStatusChanged:', resp)
|
||||||
|
interesting.partialUpdateBy(
|
||||||
|
(tags, obj) => {
|
||||||
|
if (resp.isInplay) {
|
||||||
|
tags.isInplay.textContent = 'inplay'
|
||||||
|
} else {
|
||||||
|
tags.isInplay.textContent = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.isLinkedToOnexbet) {
|
||||||
|
tags.isLinked.textContent = 'linked to 1xBet'
|
||||||
|
} else {
|
||||||
|
tags.isLinked.textContent = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(obj) => {
|
||||||
|
if (obj.matchID == resp.matchID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
ws.handle('footballMatchReports', resp => {
|
||||||
|
console.log('reports:', resp)
|
||||||
|
updateChamp(resp)
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listSportTips',
|
||||||
|
data: Tennis,
|
||||||
|
onSuccess: resp => {
|
||||||
|
tipModel.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listStrategiesReports',
|
||||||
|
data: Tennis,
|
||||||
|
onSuccess: resp => {
|
||||||
|
strategiesModel.setList(filterStrategies(resp))
|
||||||
|
showTotal(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onexbetWs.connect()
|
||||||
|
tipperWs.connect()
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function showTotal(reports) {
|
||||||
|
// 1,2,3
|
||||||
|
let allChampsTotal = {
|
||||||
|
name: 'Все чемпионаты',
|
||||||
|
wonTips: 0,
|
||||||
|
lostTips: 0,
|
||||||
|
accuracy: 0,
|
||||||
|
profit: 0,
|
||||||
|
turnover: 0,
|
||||||
|
roi: 0
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
let topChampsTotal = {
|
||||||
|
name: 'Топ чемпионаты',
|
||||||
|
wonTips: 0,
|
||||||
|
lostTips: 0,
|
||||||
|
accuracy: 0,
|
||||||
|
profit: 0,
|
||||||
|
turnover: 0,
|
||||||
|
roi: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let myTotal = {
|
||||||
|
name: 'Алгоритмы от Программиста',
|
||||||
|
wonTips: 0,
|
||||||
|
lostTips: 0,
|
||||||
|
accuracy: 0,
|
||||||
|
profit: 0,
|
||||||
|
turnover: 0,
|
||||||
|
roi: 0
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let m = new Map()
|
||||||
|
|
||||||
|
m.set(15, allChampsTotal)
|
||||||
|
m.set(16, allChampsTotal)
|
||||||
|
m.set(17, allChampsTotal)
|
||||||
|
m.set(18, allChampsTotal)
|
||||||
|
|
||||||
|
if (Array.isArray(reports)) {
|
||||||
|
reports.forEach(r => {
|
||||||
|
let total = m.get(r.strategyID)
|
||||||
|
if (!total) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
total.wonTips += r.wonTips
|
||||||
|
total.lostTips += r.lostTips
|
||||||
|
total.profit += r.profit
|
||||||
|
total.turnover += r.turnover
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clacROIandAccuracy(allChampsTotal)
|
||||||
|
//clacROIandAccuracy(topChampsTotal)
|
||||||
|
//clacROIandAccuracy(myTotal)
|
||||||
|
|
||||||
|
|
||||||
|
//totalsModel.setList([allChampsTotal, topChampsTotal, myTotal])
|
||||||
|
totalsModel.setList([allChampsTotal])
|
||||||
|
|
||||||
|
//document.getElementById('total').textContent = 'Общий итог: ' + total + ' грн'
|
||||||
|
}
|
||||||
|
|
||||||
|
function clacROIandAccuracy(total) {
|
||||||
|
total.roi = (total.profit * 100) / total.turnover
|
||||||
|
total.accuracy = (total.wonTips * 100) / (total.wonTips + total.lostTips)
|
||||||
|
|
||||||
|
//total.roi = Math.round(total.roi*100)/100
|
||||||
|
//total.accuracy = Math.round(total.accuracy*100)/100
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function modifyReport(report) {
|
||||||
|
report.match = `${report.home.canonicalName} - ${report.away.canonicalName}`
|
||||||
|
report.msg = JSON.stringify(report, null, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function modifyInterestingMatch(report) {
|
||||||
|
//console.log(report)
|
||||||
|
|
||||||
|
report.champName = `${report.zone.name}. ${report.champ.name}`
|
||||||
|
report.startTimeStr = moment(report.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
let home = report.home.canonicalName
|
||||||
|
if (!report.home.wasLinked) {
|
||||||
|
home = '*' + home
|
||||||
|
}
|
||||||
|
let away = report.away.canonicalName
|
||||||
|
if (!report.away.wasLinked) {
|
||||||
|
away = '*' + away
|
||||||
|
}
|
||||||
|
report.match = `${home} - ${away}`
|
||||||
|
if (report.isLinkedToOnexbet) {
|
||||||
|
report.isLinked = 'linked to 1xBet'
|
||||||
|
}
|
||||||
|
if (report.isInplay) {
|
||||||
|
report.isInplay = 'inplay'
|
||||||
|
} else {
|
||||||
|
report.isInplay = ''
|
||||||
|
}
|
||||||
|
report.notes = JSON.stringify(report, null, 3)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function modifyLiveMatch(report) {
|
||||||
|
if (!report.match) {
|
||||||
|
report.champName = `${report.sportName}. ${report.champName}`
|
||||||
|
}
|
||||||
|
report.startTimeStr = moment(report.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
report.match = `${report.home} - ${report.away}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function modifyTip(report) {
|
||||||
|
report.msg = JSON.stringify(report, null, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterStrategies(src) {
|
||||||
|
let list = []
|
||||||
|
if (Array.isArray(src)) {
|
||||||
|
src.forEach(s => {
|
||||||
|
list.push(s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
function tennisScoreToString(scoreByPeriods) {
|
||||||
|
if (!scoreByPeriods) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let list = []
|
||||||
|
|
||||||
|
for (var i=1; i <= 5; i++) {
|
||||||
|
let p = scoreByPeriods[i]
|
||||||
|
if (p) {
|
||||||
|
list.push(`${p.home}:${p.away}`)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.join(' ')
|
||||||
|
}
|
||||||
77
static/tipper.css
Executable file
77
static/tipper.css
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
body {
|
||||||
|
font-family: 'Ubuntu';
|
||||||
|
}
|
||||||
|
|
||||||
|
.matches {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.champ {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: antiquewhite;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#main {
|
||||||
|
display: flex;
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main div {
|
||||||
|
margin-right: 15px;
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sport-menu {
|
||||||
|
padding-left: 45px;
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interesting-title {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #ff99cc;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.book-live {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #99ccff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #99ffcc;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategies {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: gold;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errmsg {
|
||||||
|
/*color: red;*/
|
||||||
|
color: #ff4040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profit {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.void {
|
||||||
|
color: #039be5;
|
||||||
|
}
|
||||||
789
static/tipper.js
Executable file
789
static/tipper.js
Executable file
@@ -0,0 +1,789 @@
|
|||||||
|
let u = new URL(location.href)
|
||||||
|
urlPrivateAPI = u.origin + '/api'
|
||||||
|
|
||||||
|
api = new API({
|
||||||
|
url: urlPrivateAPI,
|
||||||
|
timeout: 10000, // 10 sec
|
||||||
|
})
|
||||||
|
|
||||||
|
Football = 1
|
||||||
|
Handball = 2
|
||||||
|
|
||||||
|
flashscoreWsURL = 'ws://195.189.227.88:5757/ws'
|
||||||
|
onexbetWsURL = 'ws://195.189.227.88:5758/ws'
|
||||||
|
tipperWsURL = 'ws://195.189.227.88:5759/ws'
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', ()=> {
|
||||||
|
totalsModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'total',
|
||||||
|
},
|
||||||
|
container: 'totals',
|
||||||
|
preFunc: (r) => {
|
||||||
|
r.roi = Math.round(r.roi*100)/100
|
||||||
|
r.profit = Math.round(r.profit*100)/100
|
||||||
|
r.drawdown = Math.round(r.drawdown*100)/100
|
||||||
|
r.accuracy = Math.round(r.accuracy*100)/100
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
spa = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'spa',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
spa2 = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'spa2',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
eng = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'eng',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
eng2 = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'eng2',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
ger = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'ger',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
ita = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'ita',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
ita2 = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'ita2',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
por = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'por',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
ukr = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'ukr',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
swi = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'swi',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
tur = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'tur',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
rus = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'rus',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
rom = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'rom',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
mon = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'mon',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
bel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'bel',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
gre = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'gre',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
che = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'che',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
cro = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'cro',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
den = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: 'den',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
= new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: '',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
= new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: '',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
= new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: '',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
= new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'report',
|
||||||
|
},
|
||||||
|
container: '',
|
||||||
|
preFunc: modifyReport,
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
//1xbet
|
||||||
|
bookLiveModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'liveMatch',
|
||||||
|
},
|
||||||
|
container: 'book-live',
|
||||||
|
preFunc: modifyLiveMatch,
|
||||||
|
sort: {
|
||||||
|
key: 'startTime',
|
||||||
|
reverse: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// tips
|
||||||
|
|
||||||
|
tipModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'tip',
|
||||||
|
renders: {
|
||||||
|
tip: (tags, tip) => {
|
||||||
|
//console.log('tags:', tags)
|
||||||
|
//console.log('x:', x)
|
||||||
|
if (tip.status == 1) {
|
||||||
|
tags.matchName.classList.add('profit')
|
||||||
|
} else if (tip.status == 2) {
|
||||||
|
tags.matchName.classList.add('errmsg')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
container: 'tips',
|
||||||
|
preFunc: modifyTip,
|
||||||
|
sort: {
|
||||||
|
key: 'tipTime',
|
||||||
|
//reverse: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// strategies
|
||||||
|
|
||||||
|
strategiesModel = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'strategy',
|
||||||
|
},
|
||||||
|
container: 'strategies',
|
||||||
|
preFunc: (r) => {
|
||||||
|
r.roi = Math.round(r.roi*100)/100
|
||||||
|
r.profit = Math.round(r.profit*100)/100
|
||||||
|
r.drawdown = Math.round(r.drawdown*100)/100
|
||||||
|
r.accuracy = Math.round(r.accuracy*100)/100
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// flashscore
|
||||||
|
let ws = new WebsocketClient({
|
||||||
|
//url: 'ws://localhost:5757/ws'
|
||||||
|
//url: 'ws://167.99.157.181:5757/ws'
|
||||||
|
url: flashscoreWsURL
|
||||||
|
})
|
||||||
|
|
||||||
|
//ws.onconnected =
|
||||||
|
|
||||||
|
ws.handle('footballChamps', resp => {
|
||||||
|
console.log('champs:', resp)
|
||||||
|
if (Array.isArray(resp.Champs)) {
|
||||||
|
//console.log('array')
|
||||||
|
resp.Champs.forEach(champ => {
|
||||||
|
console.log(champ)
|
||||||
|
updateChamp(champ)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.handle('footballMatchReports', resp => {
|
||||||
|
console.log('reports:', resp)
|
||||||
|
if (resp) {
|
||||||
|
updateChamp(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
interesting = new ListModel({
|
||||||
|
template: {
|
||||||
|
id: 'interestingMatch',
|
||||||
|
renders: {
|
||||||
|
interestingMatch: (tags, match) => {
|
||||||
|
let radios = new RadioList({
|
||||||
|
container: tags.candidates,
|
||||||
|
templateId: 'candidate-item',
|
||||||
|
setEmptyMessage: 'нет матчей кандидатов'
|
||||||
|
})
|
||||||
|
|
||||||
|
tags.search.addEventListener('click', () => {
|
||||||
|
let teamName = tags.teamName.value.trim()
|
||||||
|
|
||||||
|
if (teamName == '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'searchMatchCandidatesByTeamName',
|
||||||
|
data: {
|
||||||
|
sportID: Football,
|
||||||
|
teamName: teamName
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
console.log(err)
|
||||||
|
tags.candidates.textContent = "FUCK: " + err
|
||||||
|
},
|
||||||
|
onSuccess: resp => {
|
||||||
|
tags.submit.disabled = false
|
||||||
|
radios.clear()
|
||||||
|
|
||||||
|
console.log(resp)
|
||||||
|
if (Array.isArray(resp)) {
|
||||||
|
resp.forEach(candidate => {
|
||||||
|
candidate.startTimeStr = moment(candidate.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// show model + form
|
||||||
|
radios.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tags.submit.addEventListener('click', () => {
|
||||||
|
//console.log(radios.getSelected())
|
||||||
|
let candidate = radios.getSelected()
|
||||||
|
if (!candidate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api.req({
|
||||||
|
func: 'addTeamsAliases',
|
||||||
|
data: {
|
||||||
|
home: {
|
||||||
|
teamID: match.home.teamID,
|
||||||
|
name: candidate.home
|
||||||
|
},
|
||||||
|
away: {
|
||||||
|
teamID: match.away.teamID,
|
||||||
|
name: candidate.away
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
//tags.candidates.textContent = 'FUCK: ' + err
|
||||||
|
console.log(err)
|
||||||
|
},
|
||||||
|
onSuccess: resp => {
|
||||||
|
//console.log('ok')
|
||||||
|
//tags.identification.click()
|
||||||
|
radios.clear()
|
||||||
|
tags.candidates.textContent = "Алиасы добавлены!"
|
||||||
|
tags.submit.disabled = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tags.identification.addEventListener('click', () => {
|
||||||
|
//console.log('details:', tags.identification.open)
|
||||||
|
//console.log(match)
|
||||||
|
if (tags.identification.open) {
|
||||||
|
//tags.candidates.innerHTML = ''
|
||||||
|
} else {
|
||||||
|
//tags.candidates.innerHTML = ''
|
||||||
|
radios.clear()
|
||||||
|
|
||||||
|
//radios.setList([
|
||||||
|
// {home: 'Arsenal', away: 'Mancity', sport: 'Football', champName: 'England. Premier League', startTimeStr: "Oct 06, 12:30:00"},
|
||||||
|
// {home: 'Arsenal', away: 'Aston villa', sport: 'Football', champName: 'England. Premier League', startTimeStr: "Oct 06, 12:30:00"},
|
||||||
|
//])
|
||||||
|
|
||||||
|
//tags.candidates.textContent = JSON.stringify(match)
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'searchMatchCandidates',
|
||||||
|
data: {
|
||||||
|
sportID: Football,
|
||||||
|
home: match.home.canonicalName,
|
||||||
|
away: match.away.canonicalName
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
console.log(err)
|
||||||
|
tags.candidates.textContent = "FUCK: " + err
|
||||||
|
},
|
||||||
|
onSuccess: resp => {
|
||||||
|
tags.submit.disabled = false
|
||||||
|
console.log(resp)
|
||||||
|
if (Array.isArray(resp)) {
|
||||||
|
resp.forEach(candidate => {
|
||||||
|
candidate.startTimeStr = moment(candidate.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// show model + form
|
||||||
|
radios.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
container: 'interesting',
|
||||||
|
preFunc: modifyInterestingMatch,
|
||||||
|
sort: {
|
||||||
|
key: 'startTime',
|
||||||
|
reverse: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 1xbet
|
||||||
|
let onexbetWs = new WebsocketClient({
|
||||||
|
//url: 'ws://localhost:5758/ws'
|
||||||
|
//url: 'ws://167.99.157.181:5758/ws'
|
||||||
|
url: onexbetWsURL
|
||||||
|
})
|
||||||
|
|
||||||
|
onexbetWs.handle('liveMatches', resp => {
|
||||||
|
console.log('liveMatches:', resp)
|
||||||
|
|
||||||
|
if (Array.isArray(resp.matches)) {
|
||||||
|
bookLiveModel.setList(resp.matches)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onexbetWs.handle('liveMatchesUpdate', resp => {
|
||||||
|
//console.log('liveMatchesUpdate:', resp)
|
||||||
|
let list = bookLiveModel.getList()
|
||||||
|
|
||||||
|
let removeMap = {}
|
||||||
|
if (resp.removedMatchIDs) {
|
||||||
|
resp.removedMatchIDs.forEach(matchID => {
|
||||||
|
removeMap[matchID] = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let newList = []
|
||||||
|
|
||||||
|
list.forEach(old => {
|
||||||
|
if (removeMap[old.matchID] === true) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
newList.push(old)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(resp.addedMatches)) {
|
||||||
|
resp.addedMatches.forEach(newMatch => {
|
||||||
|
newList.push(newMatch)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
bookLiveModel.setList(newList)
|
||||||
|
})
|
||||||
|
|
||||||
|
onexbetWs.handle('footballLiveMatchData', resp => {
|
||||||
|
//console.log('footballLiveMatchData:', resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// tipper
|
||||||
|
let tipperWs = new WebsocketClient({
|
||||||
|
// url: 'ws://localhost:5759/ws'
|
||||||
|
// url: 'ws://167.99.157.181:5759/ws'
|
||||||
|
url: tipperWsURL
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.handle('interestingFootballMatches', resp => {
|
||||||
|
console.log('interesting matches:', resp)
|
||||||
|
|
||||||
|
if (Array.isArray(resp.matches)) {
|
||||||
|
interesting.setList(resp.matches)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tipperWs.handle('tipsChanged', resp => {
|
||||||
|
//console.log('tip:', resp)
|
||||||
|
|
||||||
|
//tipModel.add(resp)
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listTips',
|
||||||
|
onSuccess: resp => {
|
||||||
|
tipModel.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listStrategiesReports',
|
||||||
|
onSuccess: resp => {
|
||||||
|
strategiesModel.setList(resp)
|
||||||
|
showTotal(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
ws.handle('footballMatchReports', resp => {
|
||||||
|
console.log('reports:', resp)
|
||||||
|
updateChamp(resp)
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
ws.connect()
|
||||||
|
onexbetWs.connect()
|
||||||
|
tipperWs.connect()
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listTips',
|
||||||
|
onSuccess: resp => {
|
||||||
|
tipModel.setList(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
api.req({
|
||||||
|
func: 'listStrategiesReports',
|
||||||
|
onSuccess: resp => {
|
||||||
|
strategiesModel.setList(resp)
|
||||||
|
showTotal(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function showTotal(reports) {
|
||||||
|
// 1,2,3
|
||||||
|
let allChampsTotal = {
|
||||||
|
name: 'Все чемпионаты',
|
||||||
|
wonTips: 0,
|
||||||
|
lostTips: 0,
|
||||||
|
accuracy: 0,
|
||||||
|
profit: 0,
|
||||||
|
turnover: 0,
|
||||||
|
roi: 0
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
let topChampsTotal = {
|
||||||
|
name: 'Топ чемпионаты',
|
||||||
|
wonTips: 0,
|
||||||
|
lostTips: 0,
|
||||||
|
accuracy: 0,
|
||||||
|
profit: 0,
|
||||||
|
turnover: 0,
|
||||||
|
roi: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let myTotal = {
|
||||||
|
name: 'Алгоритмы от Программиста',
|
||||||
|
wonTips: 0,
|
||||||
|
lostTips: 0,
|
||||||
|
accuracy: 0,
|
||||||
|
profit: 0,
|
||||||
|
turnover: 0,
|
||||||
|
roi: 0
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let m = new Map()
|
||||||
|
|
||||||
|
m.set(1, allChampsTotal)
|
||||||
|
m.set(2, allChampsTotal)
|
||||||
|
m.set(3, allChampsTotal)
|
||||||
|
|
||||||
|
/*
|
||||||
|
m.set(11, topChampsTotal)
|
||||||
|
m.set(22, topChampsTotal)
|
||||||
|
m.set(33, topChampsTotal)
|
||||||
|
|
||||||
|
m.set(701, myTotal)
|
||||||
|
m.set(702, myTotal)
|
||||||
|
m.set(703, myTotal)
|
||||||
|
*/
|
||||||
|
|
||||||
|
let total = 0
|
||||||
|
if (Array.isArray(reports)) {
|
||||||
|
reports.forEach(r => {
|
||||||
|
let total = m.get(r.strategyID)
|
||||||
|
if (!total) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
total.wonTips += r.wonTips
|
||||||
|
total.lostTips += r.lostTips
|
||||||
|
total.profit += r.profit
|
||||||
|
total.turnover += r.turnover
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clacROIandAccuracy(allChampsTotal)
|
||||||
|
//clacROIandAccuracy(topChampsTotal)
|
||||||
|
//clacROIandAccuracy(myTotal)
|
||||||
|
|
||||||
|
|
||||||
|
//totalsModel.setList([allChampsTotal, topChampsTotal, myTotal])
|
||||||
|
totalsModel.setList([allChampsTotal])
|
||||||
|
|
||||||
|
//document.getElementById('total').textContent = 'Общий итог: ' + total + ' грн'
|
||||||
|
}
|
||||||
|
|
||||||
|
function clacROIandAccuracy(total) {
|
||||||
|
total.roi = (total.profit * 100) / total.turnover
|
||||||
|
total.accuracy = (total.wonTips * 100) / (total.wonTips + total.lostTips)
|
||||||
|
|
||||||
|
//total.roi = Math.round(total.roi*100)/100
|
||||||
|
//total.accuracy = Math.round(total.accuracy*100)/100
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChamp(champ) {
|
||||||
|
if (!champ.Champ) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let lastSync = moment(champ.LastSync * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
|
||||||
|
switch (champ.Champ.champID) {
|
||||||
|
case "/england/premier-league":
|
||||||
|
document.getElementById('engLastSync').textContent = lastSync
|
||||||
|
eng.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/germany/bundesliga":
|
||||||
|
document.getElementById('gerLastSync').textContent = lastSync
|
||||||
|
ger.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/italy/serie-a":
|
||||||
|
document.getElementById('itaLastSync').textContent = lastSync
|
||||||
|
ita.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/spain/laliga":
|
||||||
|
document.getElementById('spaLastSync').textContent = lastSync
|
||||||
|
spa.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/ukraine/premier-league":
|
||||||
|
document.getElementById('ukrLastSync').textContent = lastSync
|
||||||
|
ukr.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/portugal/primeira-liga":
|
||||||
|
document.getElementById('porLastSync').textContent = lastSync
|
||||||
|
por.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/switzerland/super-league":
|
||||||
|
document.getElementById('swiLastSync').textContent = lastSync
|
||||||
|
swi.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/turkey/super-lig":
|
||||||
|
document.getElementById('turLastSync').textContent = lastSync
|
||||||
|
tur.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/russia/premier-league":
|
||||||
|
document.getElementById('rusLastSync').textContent = lastSync
|
||||||
|
rus.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/belarus/vysshaya-liga":
|
||||||
|
document.getElementById('belLastSync').textContent = lastSync
|
||||||
|
bel.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/greece/super-league":
|
||||||
|
document.getElementById('greLastSync').textContent = lastSync
|
||||||
|
gre.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/montenegro/prva-crnogorska-liga":
|
||||||
|
document.getElementById('monLastSync').textContent = lastSync
|
||||||
|
mon.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/romania/liga-1":
|
||||||
|
document.getElementById('romLastSync').textContent = lastSync
|
||||||
|
rom.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/croatia/1-hnl":
|
||||||
|
document.getElementById('croLastSync').textContent = lastSync
|
||||||
|
cro.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/czech-republic/1-liga":
|
||||||
|
document.getElementById('cheLastSync').textContent = lastSync
|
||||||
|
che.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/denmark/superliga":
|
||||||
|
document.getElementById('denLastSync').textContent = lastSync
|
||||||
|
den.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/england/championship":
|
||||||
|
document.getElementById('eng2LastSync').textContent = lastSync
|
||||||
|
eng2.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/italy/serie-b":
|
||||||
|
document.getElementById('ita2LastSync').textContent = lastSync
|
||||||
|
ita2.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "/spain/laliga2":
|
||||||
|
document.getElementById('spa2LastSync').textContent = lastSync
|
||||||
|
spa2.setList(champ.Reports)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function modifyReport(report) {
|
||||||
|
report.match = `${report.Home.canonicalName} - ${report.Away.canonicalName}`
|
||||||
|
report.msg = JSON.stringify(report, null, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function modifyInterestingMatch(report) {
|
||||||
|
report.champName = `${report.zone.name}. ${report.champ.name}`
|
||||||
|
report.startTimeStr = moment(report.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
let home = report.home.canonicalName
|
||||||
|
if (!report.home.wasLinked) {
|
||||||
|
home = '*' + home
|
||||||
|
}
|
||||||
|
let away = report.away.canonicalName
|
||||||
|
if (!report.away.wasLinked) {
|
||||||
|
away = '*' + away
|
||||||
|
}
|
||||||
|
report.match = `${home} - ${away}`
|
||||||
|
if (report.isLinkedToOnexbet) {
|
||||||
|
report.isLinked = 'linked to 1xBet'
|
||||||
|
}
|
||||||
|
if (report.isInplay) {
|
||||||
|
report.isInplay = 'inplay'
|
||||||
|
} else {
|
||||||
|
report.isInplay = ''
|
||||||
|
}
|
||||||
|
report.notes = JSON.stringify(report.notes, null, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function modifyLiveMatch(report) {
|
||||||
|
if (!report.match) {
|
||||||
|
report.champName = `${report.sportName}. ${report.champName}`
|
||||||
|
}
|
||||||
|
report.startTimeStr = moment(report.startTime * 1000).format("MMM DD, HH:mm:ss")
|
||||||
|
report.match = `${report.home} - ${report.away}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function modifyTip(report) {
|
||||||
|
report.msg = JSON.stringify(report, null, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
148
temp/main.go
Normal file
148
temp/main.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// "fmt"
|
||||||
|
// "time"
|
||||||
|
// "timeutil"
|
||||||
|
// "tipper/model"
|
||||||
|
// "web"
|
||||||
|
// "web/api"
|
||||||
|
// "web/kit/simplerouter"
|
||||||
|
|
||||||
|
// //"qx"
|
||||||
|
// "qx"
|
||||||
|
|
||||||
|
// _ "github.com/go-sql-driver/mysql"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const (
|
||||||
|
// port = 7770
|
||||||
|
// //baseDir = "/root/tipper"
|
||||||
|
// //baseDir = ""
|
||||||
|
// baseDir = "/home/ubuntu/tipper"
|
||||||
|
// //baseDir = "/home/dima/work/go/src/tipper"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// var m *model.Model
|
||||||
|
|
||||||
|
// func main() {
|
||||||
|
// //logger := log.New(os.Stdout, "", log.LstdFlags)
|
||||||
|
|
||||||
|
//db, err := qx.Open("mysql", "user:password@/tipper")
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// defer db.Close()
|
||||||
|
|
||||||
|
// m = model.New(db)
|
||||||
|
|
||||||
|
// publicAPI := api.NewAPI(api.Options{
|
||||||
|
// //Private: false,
|
||||||
|
// //ErrorMapping: fsmodel.ErrorMessages,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// publicAPI.Func("listStrategies", m.ListStrategies)
|
||||||
|
// publicAPI.Func("listStrategyRatedChampStats", m.ListStrategyRatedChampStats)
|
||||||
|
// publicAPI.Func("getStrategyRatedHistory", m.GetStrategyRatedHistory)
|
||||||
|
|
||||||
|
// router := simplerouter.New()
|
||||||
|
// router.Handle("/", pageTips)
|
||||||
|
// router.Handle("/best-strategies", pageStrategies)
|
||||||
|
// router.Handle("/api", publicAPI.Index)
|
||||||
|
|
||||||
|
// app := web.NewApp(web.AppOptions{
|
||||||
|
// Port: port,
|
||||||
|
// Router: router,
|
||||||
|
// BaseDir: baseDir,
|
||||||
|
// TemplateDir: "templates",
|
||||||
|
// StaticDirs: map[string]string{
|
||||||
|
// "static": "static",
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
|
// app.Run()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type TipView struct {
|
||||||
|
// Time string
|
||||||
|
// Name string
|
||||||
|
// Champ string
|
||||||
|
// Link string
|
||||||
|
// Result string
|
||||||
|
// Status model.TipStatus
|
||||||
|
// StrategyID int64
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type DayTips struct {
|
||||||
|
// Date string
|
||||||
|
// Tips []TipView
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type PageTipsData struct {
|
||||||
|
// Days []DayTips
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func pageTips(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
// tips, err := m.ListTips()
|
||||||
|
// if err != nil {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var days []DayTips
|
||||||
|
|
||||||
|
// var currentDay int64
|
||||||
|
// var dayTips DayTips
|
||||||
|
|
||||||
|
// for _, tip := range tips {
|
||||||
|
// if tip.Status != model.Won && tip.Status != model.Lost {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// tm := time.Unix(tip.TipTime, 0)
|
||||||
|
|
||||||
|
// tm, err = timeutil.FirstSecondInPeriod(tm.Format("2006-01-02"),
|
||||||
|
// tm.Location(), "d")
|
||||||
|
// if err != nil {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// day := tm.Unix()
|
||||||
|
|
||||||
|
// if day != currentDay {
|
||||||
|
// if currentDay != 0 {
|
||||||
|
// days = append(days, dayTips)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// dayTips = DayTips{
|
||||||
|
// Date: tm.Format("Monday Jan 2, 2006"),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// currentDay = day
|
||||||
|
// }
|
||||||
|
|
||||||
|
// dayTips.Tips = append(dayTips.Tips, TipView{
|
||||||
|
// Time: tm.Format("15:04"),
|
||||||
|
// Name: tip.MatchName,
|
||||||
|
// Champ: tip.ChampName,
|
||||||
|
// Link: fmt.Sprintf("https://www.flashscore.com/match/%s/#h2h;overall", tip.MatchID),
|
||||||
|
// Result: tip.Result,
|
||||||
|
// Status: tip.Status,
|
||||||
|
// StrategyID: tip.StrategyID,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if currentDay != 0 {
|
||||||
|
// days = append(days, dayTips)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// reply.Render("temp_tips", PageTipsData{
|
||||||
|
// Days: days,
|
||||||
|
// })
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func pageStrategies(state *web.State, reply *web.Reply) (err error) {
|
||||||
|
// reply.Render("temp_stats", nil)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
60
templates/aliases.html
Executable file
60
templates/aliases.html
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Алиасы</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Ubuntu&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||||
|
<link href="/static/tipper.css?123" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="/static/moment.js"></script>
|
||||||
|
<script src="/static/omg.js"></script>
|
||||||
|
<script src="/static/formobj.js"></script>
|
||||||
|
<script src="/static/aliases.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<template id="listItem">
|
||||||
|
<div class="model-item"><input data-id="id"> <span data-id="select"><span data-prop="name"></span></span></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div id="main">
|
||||||
|
<div style="margin: 50px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="zone">Регион</label>
|
||||||
|
<select id="zone" name="zone" class="form-control"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="team">Команда</label>
|
||||||
|
<select id="team" name="team" class="form-control"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div style="margin-bottom: 15px;">Варианты:</div>
|
||||||
|
<div id="aliases"></div>
|
||||||
|
<div>
|
||||||
|
<br>
|
||||||
|
<button id="removeAlias" class="btn btn-danger" hidden>Удалить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 50px;">
|
||||||
|
<form method="POST" id="add">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Вариант названия</label>
|
||||||
|
<input type="text" name="name" autocomplete="off" class="form-control">
|
||||||
|
<small class="form-text text-muted">пробелы, табуляции, большие или маленькие буквы - не имеет значения</small>
|
||||||
|
<input type="hidden" name="teamID" autocomplete="off" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" value="Добавить" class="btn btn-success">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
177
templates/football.html
Normal file
177
templates/football.html
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Football Dashboard</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Ubuntu&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<link href="/static/tipper.css?123" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="/static/moment.js"></script>
|
||||||
|
<script src="/static/omg.js"></script>
|
||||||
|
<script src="/static/form2.js"></script>
|
||||||
|
<script src="/static/football.js?yg3g3"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<template id="candidate-item">
|
||||||
|
<div style="padding: 10px;">
|
||||||
|
<span data-prop="sport" style="font-size:0.9em"></span> - <span data-prop="champName" style="font-size:0.9em"></span><br>
|
||||||
|
<span data-prop="startTimeStr" style="font-size:0.9em"></span><br>
|
||||||
|
<label><input data-id="radio"> <span data-prop="home"></span> - <span data-prop="away"></span></label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="interestingMatch">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div data-prop="champName" style="font-size:0.9em"></div>
|
||||||
|
<div data-prop="match"></div>
|
||||||
|
<div data-prop="startTimeStr" style="font-size:0.9em"></div>
|
||||||
|
<div data-id="isLinked" data-prop="isLinked" style="color:green;"></div>
|
||||||
|
<div data-id="isInplay" data-prop="isInplay" style="color:green;"></div>
|
||||||
|
<div data-id="livescore" style="color:blue;" hidden></div>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>онлайн</summary>
|
||||||
|
<div style="font-size:0.9em">время: <span data-id="time"></span></div>
|
||||||
|
<div style="font-size:0.9em">счет: <span data-id="score"></span></div>
|
||||||
|
<div data-id="comment" style="color:red;font-size:0.9em"></div>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>почему</summary>
|
||||||
|
<div style="margin-bottom: 15px; margin-top: 15px;"><a href="javascript:void(0);" data-id="flashscoreLink" target="_blank">на Flashscore</a></div>
|
||||||
|
<pre data-prop="notes"></pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details data-id="identification">
|
||||||
|
<summary>идентификация</summary>
|
||||||
|
<div style="background-color:ivory; margin-top: 20px; padding-top: 10px; padding-bottom: 10px; padding-left: 5px; padding-right: 5px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<input data-id="teamName" type="text" class="form-control" form="zzz">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button data-id="search" class="btn">Искать</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<p data-id="candidates" style="display: flex; flex-direction: column;">
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div data-id="divReverse">
|
||||||
|
<label style="padding-left: 10px;"><input type="checkbox" data-id="isReverse"> команды перепутаны местами</label>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button data-id="submit" class="btn btn-success" disabled>Добавить алиасы</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="liveMatch">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div data-prop="champName" style="font-size:0.9em"></div>
|
||||||
|
<div data-prop="match"></div>
|
||||||
|
<div data-prop="startTimeStr" style="font-size:0.9em"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tip">
|
||||||
|
<details>
|
||||||
|
<summary data-prop="matchName" data-id="matchName"></summary>
|
||||||
|
<div style="margin-bottom: 15px; margin-top: 15px;"><a href="javascript:void(0);" data-id="flashscoreLink" target="_blank">на Flashscore</a></div>
|
||||||
|
<pre data-prop="msg"></pre>
|
||||||
|
<div style="margin-bottom: 25px;"><a href="javascript:void(0);" data-id="setResultLink">Указать результат</a></div>
|
||||||
|
<div style="margin-bottom: 25px;" data-id="setResultForm" hidden></div>
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="strategy">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div><b>#<span data-prop="strategyID"></span> <span data-prop="strategy"></span></b></div>
|
||||||
|
<div>Won, Lost: +<span data-prop="wonTips"></span>, -<span data-prop="lostTips"></span></div>
|
||||||
|
<div>Точность: <span data-prop="accuracy"></span> %</div>
|
||||||
|
<div>Оборот: <span data-prop="turnover"></span> грн</div>
|
||||||
|
<div>Прибыль: <span data-prop="profit"></span> грн</div>
|
||||||
|
<div>ROI: <span data-prop="roi"></span>%</div>
|
||||||
|
<div>Максимальная просадка: <span data-prop="drawdown"></span> грн</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="total">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div><b><span data-prop="name"></span></b></div>
|
||||||
|
<div>Won, Lost: +<span data-prop="wonTips"></span>, -<span data-prop="lostTips"></span></div>
|
||||||
|
<div>Точность: <span data-prop="accuracy"></span> %</div>
|
||||||
|
<div>Оборот: <span data-prop="turnover"></span> грн</div>
|
||||||
|
<div>Прибыль: <span data-prop="profit"></span> грн</div>
|
||||||
|
<div>ROI: <span data-prop="roi"></span>%</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="sport-menu">
|
||||||
|
<div style="display: flex; margin-bottom: 0px;">
|
||||||
|
<a href="/football" style="margin-right: 50px;">Футбол</a>
|
||||||
|
<a href="/handball" style="margin-right: 50px;">Гандбол</a>
|
||||||
|
<a href="/tennis" style="margin-right: 50px;">Теннис</a>
|
||||||
|
<a href="/football-by-days" style="margin-right: 50px;">Футбол (по дням)</a>
|
||||||
|
<a href="/handball-by-days" style="margin-right: 50px;">Гандбол (по дням)</a>
|
||||||
|
<a href="/tennis-by-days" style="margin-right: 50px;">Теннис (по дням)</a>
|
||||||
|
<a href="/best-strategies">Статистика по лучшим стратегиям</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="main">
|
||||||
|
<div id="favorites">
|
||||||
|
<div class="interesting-title">Отобранные матчи:</div>
|
||||||
|
<div id="interesting" class="matches"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="book-live">1xBet live matches:</div>
|
||||||
|
<div id="book-live" class="matches"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="tips">Прогнозы:</div>
|
||||||
|
<div id="tips" class="matches"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="strategies">Стратегии:</div>
|
||||||
|
<a href="/stats" target="_blank">Статистика по лигам</a>
|
||||||
|
<hr>
|
||||||
|
<div id="strategies" class="matches"></div>
|
||||||
|
<hr>
|
||||||
|
<div id="totals" class="matches"></div>
|
||||||
|
<hr>
|
||||||
|
<div class="matches">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div><b><span>Рейтинговые матчи</span></b></div>
|
||||||
|
<div>Won, Lost: +<span id="ratedTotalsWonTips"></span>, -<span id="ratedTotalsLostTips"></span></div>
|
||||||
|
<div>Точность: <span id="ratedTotalsAccuracy"></span> %</div>
|
||||||
|
<div>Оборот: <span id="ratedTotalsTurnover"></span> грн</div>
|
||||||
|
<div>Прибыль: <span id="ratedTotalsProfit"></span> грн</div>
|
||||||
|
<div>ROI: <span id="ratedTotalsROI"></span>%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div>
|
||||||
|
<div id="total" class="matches" style="border: 1px solid #ddd"></div>
|
||||||
|
<div style="font-size:0.9em">(без учета алгоритма 100)</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
186
templates/handball.html
Executable file
186
templates/handball.html
Executable file
@@ -0,0 +1,186 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Handball Dashboard</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Ubuntu&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||||
|
|
||||||
|
|
||||||
|
<link href="/static/tipper.css?123" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
|
||||||
|
|
||||||
|
<script src="/static/moment.js"></script>
|
||||||
|
<script src="/static/omg.js"></script>
|
||||||
|
<script src="/static/form2.js"></script>
|
||||||
|
<script src="/static/handball.js?zd23"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<template id="candidate-item">
|
||||||
|
<div style="padding: 10px;">
|
||||||
|
<span data-prop="sport" style="font-size:0.9em"></span> - <span data-prop="champName" style="font-size:0.9em"></span><br>
|
||||||
|
<span data-prop="startTimeStr" style="font-size:0.9em"></span><br>
|
||||||
|
<label><input data-id="radio"> <span data-prop="home"></span> - <span data-prop="away"></span></label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="interestingMatch">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div data-prop="champName" style="font-size:0.9em"></div>
|
||||||
|
<div data-prop="match"></div>
|
||||||
|
<div data-prop="startTimeStr" style="font-size:0.9em"></div>
|
||||||
|
<div data-id="isLinked" data-prop="isLinked" style="color:green;"></div>
|
||||||
|
<div data-id="isInplay" data-prop="isInplay" style="color:green;"></div>
|
||||||
|
<div data-id="livescore" style="color:blue;" hidden></div>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>онлайн</summary>
|
||||||
|
<div style="font-size:0.9em">время: <span data-id="time"></span></div>
|
||||||
|
<div style="font-size:0.9em">счет: <span data-id="score"></span></div>
|
||||||
|
<!--<div style="font-size:0.9em">темп: <span data-id="gamePace"></span>за <span data-id="gamePaceFrame"></span> мин.</div>-->
|
||||||
|
<div data-id="comment" style="color:red;font-size:0.9em"></div>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>почему</summary>
|
||||||
|
<div style="margin-bottom: 15px; margin-top: 15px;"><a href="javascript:void(0);" data-id="flashscoreLink" target="_blank">на Flashscore</a></div>
|
||||||
|
<pre data-prop="notes"></pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details data-id="identification">
|
||||||
|
<summary>идентификация</summary>
|
||||||
|
<div style="background-color:ivory; margin-top: 20px; padding-top: 10px; padding-bottom: 10px; padding-left: 5px; padding-right: 5px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<input data-id="teamName" type="text" class="form-control" form="zzz">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button data-id="search" class="btn">Искать</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<p data-id="candidates" style="display: flex; flex-direction: column;">
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div data-id="divReverse">
|
||||||
|
<label style="padding-left: 10px;"><input type="checkbox" data-id="isReverse"> команды перепутаны местами</label>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button data-id="submit" class="btn btn-success" disabled>Добавить алиасы</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="liveMatch">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div data-prop="champName" style="font-size:0.9em"></div>
|
||||||
|
<div data-prop="match"></div>
|
||||||
|
<div data-prop="startTimeStr" style="font-size:0.9em"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<template id="tip">
|
||||||
|
<details>
|
||||||
|
<summary data-prop="matchName" data-id="matchName"></summary>
|
||||||
|
<div style="margin-bottom: 15px; margin-top: 15px;"><a href="javascript:void(0);" data-id="flashscoreLink" target="_blank">на Flashscore</a></div>
|
||||||
|
<pre data-prop="msg"></pre>
|
||||||
|
<div style="margin-bottom: 25px;"><a href="javascript:void(0);" data-id="setResultLink">Указать результат</a></div>
|
||||||
|
<div style="margin-bottom: 25px;" data-id="setResultForm" hidden></div>
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="strategy">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div><b>#<span data-prop="strategyID"></span> <span data-prop="strategy"></span></b></div>
|
||||||
|
<div>Won, Lost: +<span data-prop="wonTips"></span>, -<span data-prop="lostTips"></span></div>
|
||||||
|
<div>Точность: <span data-prop="accuracy"></span> %</div>
|
||||||
|
<div>Оборот: <span data-prop="turnover"></span> грн</div>
|
||||||
|
<div>Прибыль: <span data-prop="profit"></span> грн</div>
|
||||||
|
<div>ROI: <span data-prop="roi"></span>%</div>
|
||||||
|
<div>Максимальная просадка: <span data-prop="drawdown"></span> грн</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="total">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div><b><span data-prop="name"></span></b></div>
|
||||||
|
<div>Won, Lost: +<span data-prop="wonTips"></span>, -<span data-prop="lostTips"></span></div>
|
||||||
|
<div>Точность: <span data-prop="accuracy"></span> %</div>
|
||||||
|
<div>Оборот: <span data-prop="turnover"></span> грн</div>
|
||||||
|
<div>Прибыль: <span data-prop="profit"></span> грн</div>
|
||||||
|
<div>ROI: <span data-prop="roi"></span>%</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="sport-menu">
|
||||||
|
<div style="display: flex; margin-bottom: 0px;">
|
||||||
|
<a href="/football" style="margin-right: 50px;">Футбол</a>
|
||||||
|
<a href="/handball" style="margin-right: 50px;">Гандбол</a>
|
||||||
|
<a href="/tennis" style="margin-right: 50px;">Теннис</a>
|
||||||
|
<a href="/football-by-days" style="margin-right: 50px;">Футбол (по дням)</a>
|
||||||
|
<a href="/handball-by-days" style="margin-right: 50px;">Гандбол (по дням)</a>
|
||||||
|
<a href="/tennis-by-days" style="margin-right: 50px;">Теннис (по дням)</a>
|
||||||
|
<a href="/best-strategies">Статистика по лучшим стратегиям</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main">
|
||||||
|
<div id="favorites">
|
||||||
|
<div class="interesting-title">Отобранные матчи:</div>
|
||||||
|
<div id="interesting" class="matches"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="book-live">1xBet live matches:</div>
|
||||||
|
<div id="book-live" class="matches"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="tips">Прогнозы:</div>
|
||||||
|
<div id="tips" class="matches"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="strategies">Стратегии:</div>
|
||||||
|
<a href="/stats" target="_blank">Статистика по лигам</a>
|
||||||
|
<hr>
|
||||||
|
<div id="strategies" class="matches"></div>
|
||||||
|
<hr>
|
||||||
|
<div id="totals" class="matches"></div>
|
||||||
|
<hr>
|
||||||
|
<div class="matches">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div><b><span>Рейтинговые матчи</span></b></div>
|
||||||
|
<div>Won, Lost: +<span id="ratedTotalsWonTips"></span>, -<span id="ratedTotalsLostTips"></span></div>
|
||||||
|
<div>Точность: <span id="ratedTotalsAccuracy"></span> %</div>
|
||||||
|
<div>Оборот: <span id="ratedTotalsTurnover"></span> грн</div>
|
||||||
|
<div>Прибыль: <span id="ratedTotalsProfit"></span> грн</div>
|
||||||
|
<div>ROI: <span id="ratedTotalsROI"></span>%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div>
|
||||||
|
<div id="total" class="matches" style="border: 1px solid #ddd"></div>
|
||||||
|
<div style="font-size:0.9em">(без учета алгоритма 100)</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
74
templates/stats.html
Executable file
74
templates/stats.html
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Статистика по всем лигам</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Ubuntu&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<link href="/static/tipper.css?123" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="/static/moment.js"></script>
|
||||||
|
<script src="/static/omg.js"></script>
|
||||||
|
<script src="/static/formobj.js"></script>
|
||||||
|
<script src="/static/stats.js?xxx"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<template id="champ">
|
||||||
|
<tr>
|
||||||
|
<td data-prop="champ"></td>
|
||||||
|
<td data-prop="won"></td>
|
||||||
|
<td data-prop="lost"></td>
|
||||||
|
<td><span data-prop="accuracy"></span>%</td>
|
||||||
|
<td data-prop="avgPrice"></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="sport-menu">
|
||||||
|
<div style="display: flex; margin-bottom: 0px;">
|
||||||
|
<a href="/football" style="margin-right: 50px;">Футбол</a>
|
||||||
|
<a href="/handball" style="margin-right: 50px;">Гандбол</a>
|
||||||
|
<a href="/tennis" style="margin-right: 50px;">Теннис</a>
|
||||||
|
<a href="/football-by-days" style="margin-right: 50px;">Футбол (по дням)</a>
|
||||||
|
<a href="/handball-by-days" style="margin-right: 50px;">Гандбол (по дням)</a>
|
||||||
|
<a href="/tennis-by-days" style="margin-right: 50px;">Теннис (по дням)</a>
|
||||||
|
<a href="/best-strategies">Статистика по лучшим стратегиям</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div style="display: flex; margin: 50px;">
|
||||||
|
<div style="width: 500px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="">Стратегия</label>
|
||||||
|
<select id="strategyID" name="strategyID" class="form-control"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Чемпионат</th>
|
||||||
|
<th>Won</th>
|
||||||
|
<th>Lost</th>
|
||||||
|
<th>Точность</th>
|
||||||
|
<th>Средний коэф.</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="champs">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="curve_chart" style="width: 900px; height: 500px"></div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
77
templates/temp_stats.html
Normal file
77
templates/temp_stats.html
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Статистика по лучшим чемпионатам</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Ubuntu&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<link href="/static/tipper.css?123" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="/static/moment.js"></script>
|
||||||
|
<script src="/static/omg.js"></script>
|
||||||
|
<script src="/static/formobj.js"></script>
|
||||||
|
<script src="/static/temp_stats.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<template id="champ">
|
||||||
|
<tr>
|
||||||
|
<td data-prop="champ"></td>
|
||||||
|
<td data-prop="won"></td>
|
||||||
|
<td data-prop="lost"></td>
|
||||||
|
<td><span data-prop="accuracy"></span>%</td>
|
||||||
|
<td data-prop="avgPrice"></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="sport-menu">
|
||||||
|
<div style="display: flex; margin-bottom: 0px;">
|
||||||
|
<a href="/football" style="margin-right: 50px;">Футбол</a>
|
||||||
|
<a href="/handball" style="margin-right: 50px;">Гандбол</a>
|
||||||
|
<a href="/tennis" style="margin-right: 50px;">Теннис</a>
|
||||||
|
<a href="/football-by-days" style="margin-right: 50px;">Футбол (по дням)</a>
|
||||||
|
<a href="/handball-by-days" style="margin-right: 50px;">Гандбол (по дням)</a>
|
||||||
|
<a href="/tennis-by-days" style="margin-right: 50px;">Теннис (по дням)</a>
|
||||||
|
<a href="/best-strategies">Статистика по лучшим стратегиям</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div style="margin: 20px 50px;">
|
||||||
|
|
||||||
|
<div style="display: flex;">
|
||||||
|
<div style="width: 500px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="">Стратегия</label>
|
||||||
|
<select id="strategyID" name="strategyID" class="form-control"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Чемпионат</th>
|
||||||
|
<th>Won</th>
|
||||||
|
<th>Lost</th>
|
||||||
|
<th>Точность</th>
|
||||||
|
<th>Средний коэф.</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="champs">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="curve_chart" style="width: 900px; height: 500px"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
53
templates/temp_tips.html
Normal file
53
templates/temp_tips.html
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Прогнозы с разбивкой по дням</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Ubuntu&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<link href="/static/tipper.css?123" rel="stylesheet">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="sport-menu">
|
||||||
|
<div style="display: flex; margin-bottom: 0px;">
|
||||||
|
<a href="/football" style="margin-right: 50px;">Футбол</a>
|
||||||
|
<a href="/handball" style="margin-right: 50px;">Гандбол</a>
|
||||||
|
<a href="/tennis" style="margin-right: 50px;">Теннис</a>
|
||||||
|
<a href="/football-by-days" style="margin-right: 50px;">Футбол (по дням)</a>
|
||||||
|
<a href="/handball-by-days" style="margin-right: 50px;">Гандбол (по дням)</a>
|
||||||
|
<a href="/tennis-by-days" style="margin-right: 50px;">Теннис (по дням)</a>
|
||||||
|
<a href="/best-strategies">Статистика по лучшим стратегиям</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div style="display: flex-column; margin: 20px 50px;">
|
||||||
|
|
||||||
|
|
||||||
|
{{range .Days}}
|
||||||
|
<div>{{.Date}}</div>
|
||||||
|
<table class="table">
|
||||||
|
{{range .Tips}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Time}}</td>
|
||||||
|
<td><span style="font-size: 0.9rem;">{{.Champ}}.</span> {{.Name}}</td>
|
||||||
|
<td>{{.Strategy}}</td>
|
||||||
|
<td>{{.Result}}</td>
|
||||||
|
<td>{{if eq .Status 1}}<span style="color:green;">Выигрыш</span>{{else}}<span style="color:red;">Проигрыш</span>{{end}}</td>
|
||||||
|
<td><a href="{{.Link}}" target="_blank">Flashscore</a></td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
175
templates/tennis.html
Normal file
175
templates/tennis.html
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Tennis Dashboard</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Ubuntu&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||||
|
|
||||||
|
|
||||||
|
<link href="/static/tipper.css?123" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
|
||||||
|
|
||||||
|
<script src="/static/moment.js"></script>
|
||||||
|
<script src="/static/omg.js"></script>
|
||||||
|
<script src="/static/form2.js"></script>
|
||||||
|
<script src="/static/tennis.js?h423"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<template id="candidate-item">
|
||||||
|
<div style="padding: 10px;">
|
||||||
|
<span data-prop="sport" style="font-size:0.9em"></span> - <span data-prop="champName" style="font-size:0.9em"></span><br>
|
||||||
|
<span data-prop="startTimeStr" style="font-size:0.9em"></span><br>
|
||||||
|
<label><input data-id="radio"> <span data-prop="home"></span> - <span data-prop="away"></span></label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="interestingMatch">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div data-prop="champName" style="font-size:0.9em"></div>
|
||||||
|
<div data-prop="match"></div>
|
||||||
|
<div data-prop="startTimeStr" style="font-size:0.9em"></div>
|
||||||
|
<div data-id="isLinked" data-prop="isLinked" style="color:green;"></div>
|
||||||
|
<div data-id="isInplay" data-prop="isInplay" style="color:green;"></div>
|
||||||
|
<div data-id="livescore" style="color:blue;" hidden></div>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>онлайн</summary>
|
||||||
|
<div style="font-size:0.9em">время: <span data-id="time"></span></div>
|
||||||
|
<div style="font-size:0.9em">счет: <span data-id="score"></span></div>
|
||||||
|
<!--<div style="font-size:0.9em">темп: <span data-id="gamePace"></span>за <span data-id="gamePaceFrame"></span> мин.</div>-->
|
||||||
|
<div data-id="comment" style="color:red;font-size:0.9em"></div>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>почему</summary>
|
||||||
|
<div style="margin-bottom: 15px; margin-top: 15px;"><a href="javascript:void(0);" data-id="flashscoreLink" target="_blank">на Flashscore</a></div>
|
||||||
|
<pre data-prop="notes"></pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details data-id="identification">
|
||||||
|
<summary>идентификация</summary>
|
||||||
|
<div style="background-color:ivory; margin-top: 20px; padding-top: 10px; padding-bottom: 10px; padding-left: 5px; padding-right: 5px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<input data-id="teamName" type="text" class="form-control" form="zzz">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button data-id="search" class="btn">Искать</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<p data-id="candidates" style="display: flex; flex-direction: column;">
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div data-id="divReverse">
|
||||||
|
<label style="padding-left: 10px;"><input type="checkbox" data-id="isReverse"> команды перепутаны местами</label>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button data-id="submit" class="btn btn-success" disabled>Добавить алиасы</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="liveMatch">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div data-prop="champName" style="font-size:0.9em"></div>
|
||||||
|
<div data-prop="match"></div>
|
||||||
|
<div data-prop="startTimeStr" style="font-size:0.9em"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<template id="tip">
|
||||||
|
<details>
|
||||||
|
<summary data-prop="matchName" data-id="matchName"></summary>
|
||||||
|
<div style="margin-bottom: 15px; margin-top: 15px;"><a href="javascript:void(0);" data-id="flashscoreLink" target="_blank">на Flashscore</a></div>
|
||||||
|
<pre data-prop="msg"></pre>
|
||||||
|
<div style="margin-bottom: 25px;"><a href="javascript:void(0);" data-id="setResultLink">Указать результат</a></div>
|
||||||
|
<div style="margin-bottom: 25px;" data-id="setResultForm" hidden></div>
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="strategy">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div><b>#<span data-prop="strategyID"></span> <span data-prop="strategy"></span></b></div>
|
||||||
|
<div>Won, Lost: +<span data-prop="wonTips"></span>, -<span data-prop="lostTips"></span></div>
|
||||||
|
<div>Точность: <span data-prop="accuracy"></span> %</div>
|
||||||
|
<div>Оборот: <span data-prop="turnover"></span> грн</div>
|
||||||
|
<div>Прибыль: <span data-prop="profit"></span> грн</div>
|
||||||
|
<div>ROI: <span data-prop="roi"></span>%</div>
|
||||||
|
<div>Максимальная просадка: <span data-prop="drawdown"></span> грн</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="total">
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div><b><span data-prop="name"></span></b></div>
|
||||||
|
<div>Won, Lost: +<span data-prop="wonTips"></span>, -<span data-prop="lostTips"></span></div>
|
||||||
|
<div>Точность: <span data-prop="accuracy"></span> %</div>
|
||||||
|
<div>Оборот: <span data-prop="turnover"></span> грн</div>
|
||||||
|
<div>Прибыль: <span data-prop="profit"></span> грн</div>
|
||||||
|
<div>ROI: <span data-prop="roi"></span>%</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="sport-menu">
|
||||||
|
<div style="display: flex; margin-bottom: 0px;">
|
||||||
|
<a href="/football" style="margin-right: 50px;">Футбол</a>
|
||||||
|
<a href="/handball" style="margin-right: 50px;">Гандбол</a>
|
||||||
|
<a href="/tennis" style="margin-right: 50px;">Теннис</a>
|
||||||
|
<a href="/football-by-days" style="margin-right: 50px;">Футбол (по дням)</a>
|
||||||
|
<a href="/handball-by-days" style="margin-right: 50px;">Гандбол (по дням)</a>
|
||||||
|
<a href="/tennis-by-days" style="margin-right: 50px;">Теннис (по дням)</a>
|
||||||
|
<a href="/best-strategies">Статистика по лучшим стратегиям</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main">
|
||||||
|
<div id="favorites">
|
||||||
|
<div class="interesting-title">Отобранные матчи:</div>
|
||||||
|
<div id="interesting" class="matches"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="book-live">1xBet live matches:</div>
|
||||||
|
<div id="book-live" class="matches"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="tips">Прогнозы:</div>
|
||||||
|
<div id="tips" class="matches"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="strategies">Стратегии:</div>
|
||||||
|
<a href="/stats" target="_blank">Статистика по лигам</a>
|
||||||
|
<hr>
|
||||||
|
<div id="strategies" class="matches"></div>
|
||||||
|
<hr>
|
||||||
|
<div id="totals" class="matches"></div>
|
||||||
|
<!--
|
||||||
|
<div>
|
||||||
|
<div id="total" class="matches" style="border: 1px solid #ddd"></div>
|
||||||
|
<div style="font-size:0.9em">(без учета алгоритма 100)</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
tipper PASSWORD
Executable file
1
tipper PASSWORD
Executable file
@@ -0,0 +1 @@
|
|||||||
|
zNzHf9CdM
|
||||||
223
tipper.sql
Executable file
223
tipper.sql
Executable file
File diff suppressed because one or more lines are too long
27
vendor/filippo.io/edwards25519/LICENSE
generated
vendored
Normal file
27
vendor/filippo.io/edwards25519/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
16
vendor/filippo.io/edwards25519/README.md
generated
vendored
Normal file
16
vendor/filippo.io/edwards25519/README.md
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# filippo.io/edwards25519
|
||||||
|
|
||||||
|
```
|
||||||
|
import "filippo.io/edwards25519"
|
||||||
|
```
|
||||||
|
|
||||||
|
This library implements the edwards25519 elliptic curve, exposing the necessary APIs to build a wide array of higher-level primitives.
|
||||||
|
Read the docs at [pkg.go.dev/filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519).
|
||||||
|
|
||||||
|
The package tracks the upstream standard library package `crypto/internal/fips140/edwards25519` and extends it with additional functionality.
|
||||||
|
|
||||||
|
The code is originally derived from Adam Langley's internal implementation in the Go standard library, and includes George Tankersley's [performance improvements](https://golang.org/cl/71950). It was then further developed by Henry de Valence for use in ristretto255, and was finally [merged back into the Go standard library](https://golang.org/cl/276272) as of Go 1.17.
|
||||||
|
|
||||||
|
Most users don't need this package, and should instead use `crypto/ed25519` for signatures, `crypto/ecdh` for Diffie-Hellman, or `github.com/gtank/ristretto255` for prime order group logic. However, for anyone currently using a fork of the internal `edwards25519` package or of `github.com/agl/edwards25519`, this package should be a safer, faster, and more powerful alternative.
|
||||||
|
|
||||||
|
Since this package is meant to curb proliferation of edwards25519 implementations in the Go ecosystem, it welcomes requests for new APIs or reviewable performance improvements.
|
||||||
20
vendor/filippo.io/edwards25519/doc.go
generated
vendored
Normal file
20
vendor/filippo.io/edwards25519/doc.go
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) 2021 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package edwards25519 implements group logic for the twisted Edwards curve
|
||||||
|
//
|
||||||
|
// -x^2 + y^2 = 1 + -(121665/121666)*x^2*y^2
|
||||||
|
//
|
||||||
|
// This is better known as the Edwards curve equivalent to Curve25519, and is
|
||||||
|
// the curve used by the Ed25519 signature scheme.
|
||||||
|
//
|
||||||
|
// Most users don't need this package, and should instead use crypto/ed25519 for
|
||||||
|
// signatures, crypto/ecdh for Diffie-Hellman, or github.com/gtank/ristretto255
|
||||||
|
// for prime order group logic.
|
||||||
|
//
|
||||||
|
// However, developers who do need to interact with low-level edwards25519
|
||||||
|
// operations can use this package, which is an extended version of
|
||||||
|
// crypto/internal/fips140/edwards25519 from the standard library repackaged as
|
||||||
|
// an importable module.
|
||||||
|
package edwards25519
|
||||||
427
vendor/filippo.io/edwards25519/edwards25519.go
generated
vendored
Normal file
427
vendor/filippo.io/edwards25519/edwards25519.go
generated
vendored
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
// Copyright (c) 2017 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package edwards25519
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"filippo.io/edwards25519/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Point types.
|
||||||
|
|
||||||
|
type projP1xP1 struct {
|
||||||
|
X, Y, Z, T field.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
type projP2 struct {
|
||||||
|
X, Y, Z field.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point represents a point on the edwards25519 curve.
|
||||||
|
//
|
||||||
|
// This type works similarly to math/big.Int, and all arguments and receivers
|
||||||
|
// are allowed to alias.
|
||||||
|
//
|
||||||
|
// The zero value is NOT valid, and it may be used only as a receiver.
|
||||||
|
type Point struct {
|
||||||
|
// Make the type not comparable (i.e. used with == or as a map key), as
|
||||||
|
// equivalent points can be represented by different Go values.
|
||||||
|
_ incomparable
|
||||||
|
|
||||||
|
// The point is internally represented in extended coordinates (X, Y, Z, T)
|
||||||
|
// where x = X/Z, y = Y/Z, and xy = T/Z per https://eprint.iacr.org/2008/522.
|
||||||
|
x, y, z, t field.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
type incomparable [0]func()
|
||||||
|
|
||||||
|
func checkInitialized(points ...*Point) {
|
||||||
|
for _, p := range points {
|
||||||
|
if p.x == (field.Element{}) && p.y == (field.Element{}) {
|
||||||
|
panic("edwards25519: use of uninitialized Point")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type projCached struct {
|
||||||
|
YplusX, YminusX, Z, T2d field.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
type affineCached struct {
|
||||||
|
YplusX, YminusX, T2d field.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructors.
|
||||||
|
|
||||||
|
func (v *projP2) Zero() *projP2 {
|
||||||
|
v.X.Zero()
|
||||||
|
v.Y.One()
|
||||||
|
v.Z.One()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// identity is the point at infinity.
|
||||||
|
var identity, _ = new(Point).SetBytes([]byte{
|
||||||
|
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
||||||
|
|
||||||
|
// NewIdentityPoint returns a new Point set to the identity.
|
||||||
|
func NewIdentityPoint() *Point {
|
||||||
|
return new(Point).Set(identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generator is the canonical curve basepoint. See TestGenerator for the
|
||||||
|
// correspondence of this encoding with the values in RFC 8032.
|
||||||
|
var generator, _ = new(Point).SetBytes([]byte{
|
||||||
|
0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||||
|
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||||
|
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||||
|
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66})
|
||||||
|
|
||||||
|
// NewGeneratorPoint returns a new Point set to the canonical generator.
|
||||||
|
func NewGeneratorPoint() *Point {
|
||||||
|
return new(Point).Set(generator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *projCached) Zero() *projCached {
|
||||||
|
v.YplusX.One()
|
||||||
|
v.YminusX.One()
|
||||||
|
v.Z.One()
|
||||||
|
v.T2d.Zero()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *affineCached) Zero() *affineCached {
|
||||||
|
v.YplusX.One()
|
||||||
|
v.YminusX.One()
|
||||||
|
v.T2d.Zero()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assignments.
|
||||||
|
|
||||||
|
// Set sets v = u, and returns v.
|
||||||
|
func (v *Point) Set(u *Point) *Point {
|
||||||
|
*v = *u
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoding.
|
||||||
|
|
||||||
|
// Bytes returns the canonical 32-byte encoding of v, according to RFC 8032,
|
||||||
|
// Section 5.1.2.
|
||||||
|
func (v *Point) Bytes() []byte {
|
||||||
|
// This function is outlined to make the allocations inline in the caller
|
||||||
|
// rather than happen on the heap.
|
||||||
|
var buf [32]byte
|
||||||
|
return v.bytes(&buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Point) bytes(buf *[32]byte) []byte {
|
||||||
|
checkInitialized(v)
|
||||||
|
|
||||||
|
var zInv, x, y field.Element
|
||||||
|
zInv.Invert(&v.z) // zInv = 1 / Z
|
||||||
|
x.Multiply(&v.x, &zInv) // x = X / Z
|
||||||
|
y.Multiply(&v.y, &zInv) // y = Y / Z
|
||||||
|
|
||||||
|
out := copyFieldElement(buf, &y)
|
||||||
|
out[31] |= byte(x.IsNegative() << 7)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
var feOne = new(field.Element).One()
|
||||||
|
|
||||||
|
// SetBytes sets v = x, where x is a 32-byte encoding of v. If x does not
|
||||||
|
// represent a valid point on the curve, SetBytes returns nil and an error and
|
||||||
|
// the receiver is unchanged. Otherwise, SetBytes returns v.
|
||||||
|
//
|
||||||
|
// Note that SetBytes accepts all non-canonical encodings of valid points.
|
||||||
|
// That is, it follows decoding rules that match most implementations in
|
||||||
|
// the ecosystem rather than RFC 8032.
|
||||||
|
func (v *Point) SetBytes(x []byte) (*Point, error) {
|
||||||
|
// Specifically, the non-canonical encodings that are accepted are
|
||||||
|
// 1) the ones where the field element is not reduced (see the
|
||||||
|
// (*field.Element).SetBytes docs) and
|
||||||
|
// 2) the ones where the x-coordinate is zero and the sign bit is set.
|
||||||
|
//
|
||||||
|
// Read more at https://hdevalence.ca/blog/2020-10-04-its-25519am,
|
||||||
|
// specifically the "Canonical A, R" section.
|
||||||
|
|
||||||
|
y, err := new(field.Element).SetBytes(x)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("edwards25519: invalid point encoding length")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -x² + y² = 1 + dx²y²
|
||||||
|
// x² + dx²y² = x²(dy² + 1) = y² - 1
|
||||||
|
// x² = (y² - 1) / (dy² + 1)
|
||||||
|
|
||||||
|
// u = y² - 1
|
||||||
|
y2 := new(field.Element).Square(y)
|
||||||
|
u := new(field.Element).Subtract(y2, feOne)
|
||||||
|
|
||||||
|
// v = dy² + 1
|
||||||
|
vv := new(field.Element).Multiply(y2, d)
|
||||||
|
vv = vv.Add(vv, feOne)
|
||||||
|
|
||||||
|
// x = +√(u/v)
|
||||||
|
xx, wasSquare := new(field.Element).SqrtRatio(u, vv)
|
||||||
|
if wasSquare == 0 {
|
||||||
|
return nil, errors.New("edwards25519: invalid point encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the negative square root if the sign bit is set.
|
||||||
|
xxNeg := new(field.Element).Negate(xx)
|
||||||
|
xx = xx.Select(xxNeg, xx, int(x[31]>>7))
|
||||||
|
|
||||||
|
v.x.Set(xx)
|
||||||
|
v.y.Set(y)
|
||||||
|
v.z.One()
|
||||||
|
v.t.Multiply(xx, y) // xy = T / Z
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFieldElement(buf *[32]byte, v *field.Element) []byte {
|
||||||
|
copy(buf[:], v.Bytes())
|
||||||
|
return buf[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversions.
|
||||||
|
|
||||||
|
func (v *projP2) FromP1xP1(p *projP1xP1) *projP2 {
|
||||||
|
v.X.Multiply(&p.X, &p.T)
|
||||||
|
v.Y.Multiply(&p.Y, &p.Z)
|
||||||
|
v.Z.Multiply(&p.Z, &p.T)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *projP2) FromP3(p *Point) *projP2 {
|
||||||
|
v.X.Set(&p.x)
|
||||||
|
v.Y.Set(&p.y)
|
||||||
|
v.Z.Set(&p.z)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Point) fromP1xP1(p *projP1xP1) *Point {
|
||||||
|
v.x.Multiply(&p.X, &p.T)
|
||||||
|
v.y.Multiply(&p.Y, &p.Z)
|
||||||
|
v.z.Multiply(&p.Z, &p.T)
|
||||||
|
v.t.Multiply(&p.X, &p.Y)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Point) fromP2(p *projP2) *Point {
|
||||||
|
v.x.Multiply(&p.X, &p.Z)
|
||||||
|
v.y.Multiply(&p.Y, &p.Z)
|
||||||
|
v.z.Square(&p.Z)
|
||||||
|
v.t.Multiply(&p.X, &p.Y)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// d is a constant in the curve equation.
|
||||||
|
var d, _ = new(field.Element).SetBytes([]byte{
|
||||||
|
0xa3, 0x78, 0x59, 0x13, 0xca, 0x4d, 0xeb, 0x75,
|
||||||
|
0xab, 0xd8, 0x41, 0x41, 0x4d, 0x0a, 0x70, 0x00,
|
||||||
|
0x98, 0xe8, 0x79, 0x77, 0x79, 0x40, 0xc7, 0x8c,
|
||||||
|
0x73, 0xfe, 0x6f, 0x2b, 0xee, 0x6c, 0x03, 0x52})
|
||||||
|
var d2 = new(field.Element).Add(d, d)
|
||||||
|
|
||||||
|
func (v *projCached) FromP3(p *Point) *projCached {
|
||||||
|
v.YplusX.Add(&p.y, &p.x)
|
||||||
|
v.YminusX.Subtract(&p.y, &p.x)
|
||||||
|
v.Z.Set(&p.z)
|
||||||
|
v.T2d.Multiply(&p.t, d2)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *affineCached) FromP3(p *Point) *affineCached {
|
||||||
|
v.YplusX.Add(&p.y, &p.x)
|
||||||
|
v.YminusX.Subtract(&p.y, &p.x)
|
||||||
|
v.T2d.Multiply(&p.t, d2)
|
||||||
|
|
||||||
|
var invZ field.Element
|
||||||
|
invZ.Invert(&p.z)
|
||||||
|
v.YplusX.Multiply(&v.YplusX, &invZ)
|
||||||
|
v.YminusX.Multiply(&v.YminusX, &invZ)
|
||||||
|
v.T2d.Multiply(&v.T2d, &invZ)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// (Re)addition and subtraction.
|
||||||
|
|
||||||
|
// Add sets v = p + q, and returns v.
|
||||||
|
func (v *Point) Add(p, q *Point) *Point {
|
||||||
|
checkInitialized(p, q)
|
||||||
|
qCached := new(projCached).FromP3(q)
|
||||||
|
result := new(projP1xP1).Add(p, qCached)
|
||||||
|
return v.fromP1xP1(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract sets v = p - q, and returns v.
|
||||||
|
func (v *Point) Subtract(p, q *Point) *Point {
|
||||||
|
checkInitialized(p, q)
|
||||||
|
qCached := new(projCached).FromP3(q)
|
||||||
|
result := new(projP1xP1).Sub(p, qCached)
|
||||||
|
return v.fromP1xP1(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *projP1xP1) Add(p *Point, q *projCached) *projP1xP1 {
|
||||||
|
var YplusX, YminusX, PP, MM, TT2d, ZZ2 field.Element
|
||||||
|
|
||||||
|
YplusX.Add(&p.y, &p.x)
|
||||||
|
YminusX.Subtract(&p.y, &p.x)
|
||||||
|
|
||||||
|
PP.Multiply(&YplusX, &q.YplusX)
|
||||||
|
MM.Multiply(&YminusX, &q.YminusX)
|
||||||
|
TT2d.Multiply(&p.t, &q.T2d)
|
||||||
|
ZZ2.Multiply(&p.z, &q.Z)
|
||||||
|
|
||||||
|
ZZ2.Add(&ZZ2, &ZZ2)
|
||||||
|
|
||||||
|
v.X.Subtract(&PP, &MM)
|
||||||
|
v.Y.Add(&PP, &MM)
|
||||||
|
v.Z.Add(&ZZ2, &TT2d)
|
||||||
|
v.T.Subtract(&ZZ2, &TT2d)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *projP1xP1) Sub(p *Point, q *projCached) *projP1xP1 {
|
||||||
|
var YplusX, YminusX, PP, MM, TT2d, ZZ2 field.Element
|
||||||
|
|
||||||
|
YplusX.Add(&p.y, &p.x)
|
||||||
|
YminusX.Subtract(&p.y, &p.x)
|
||||||
|
|
||||||
|
PP.Multiply(&YplusX, &q.YminusX) // flipped sign
|
||||||
|
MM.Multiply(&YminusX, &q.YplusX) // flipped sign
|
||||||
|
TT2d.Multiply(&p.t, &q.T2d)
|
||||||
|
ZZ2.Multiply(&p.z, &q.Z)
|
||||||
|
|
||||||
|
ZZ2.Add(&ZZ2, &ZZ2)
|
||||||
|
|
||||||
|
v.X.Subtract(&PP, &MM)
|
||||||
|
v.Y.Add(&PP, &MM)
|
||||||
|
v.Z.Subtract(&ZZ2, &TT2d) // flipped sign
|
||||||
|
v.T.Add(&ZZ2, &TT2d) // flipped sign
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *projP1xP1) AddAffine(p *Point, q *affineCached) *projP1xP1 {
|
||||||
|
var YplusX, YminusX, PP, MM, TT2d, Z2 field.Element
|
||||||
|
|
||||||
|
YplusX.Add(&p.y, &p.x)
|
||||||
|
YminusX.Subtract(&p.y, &p.x)
|
||||||
|
|
||||||
|
PP.Multiply(&YplusX, &q.YplusX)
|
||||||
|
MM.Multiply(&YminusX, &q.YminusX)
|
||||||
|
TT2d.Multiply(&p.t, &q.T2d)
|
||||||
|
|
||||||
|
Z2.Add(&p.z, &p.z)
|
||||||
|
|
||||||
|
v.X.Subtract(&PP, &MM)
|
||||||
|
v.Y.Add(&PP, &MM)
|
||||||
|
v.Z.Add(&Z2, &TT2d)
|
||||||
|
v.T.Subtract(&Z2, &TT2d)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *projP1xP1) SubAffine(p *Point, q *affineCached) *projP1xP1 {
|
||||||
|
var YplusX, YminusX, PP, MM, TT2d, Z2 field.Element
|
||||||
|
|
||||||
|
YplusX.Add(&p.y, &p.x)
|
||||||
|
YminusX.Subtract(&p.y, &p.x)
|
||||||
|
|
||||||
|
PP.Multiply(&YplusX, &q.YminusX) // flipped sign
|
||||||
|
MM.Multiply(&YminusX, &q.YplusX) // flipped sign
|
||||||
|
TT2d.Multiply(&p.t, &q.T2d)
|
||||||
|
|
||||||
|
Z2.Add(&p.z, &p.z)
|
||||||
|
|
||||||
|
v.X.Subtract(&PP, &MM)
|
||||||
|
v.Y.Add(&PP, &MM)
|
||||||
|
v.Z.Subtract(&Z2, &TT2d) // flipped sign
|
||||||
|
v.T.Add(&Z2, &TT2d) // flipped sign
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doubling.
|
||||||
|
|
||||||
|
func (v *projP1xP1) Double(p *projP2) *projP1xP1 {
|
||||||
|
var XX, YY, ZZ2, XplusYsq field.Element
|
||||||
|
|
||||||
|
XX.Square(&p.X)
|
||||||
|
YY.Square(&p.Y)
|
||||||
|
ZZ2.Square(&p.Z)
|
||||||
|
ZZ2.Add(&ZZ2, &ZZ2)
|
||||||
|
XplusYsq.Add(&p.X, &p.Y)
|
||||||
|
XplusYsq.Square(&XplusYsq)
|
||||||
|
|
||||||
|
v.Y.Add(&YY, &XX)
|
||||||
|
v.Z.Subtract(&YY, &XX)
|
||||||
|
|
||||||
|
v.X.Subtract(&XplusYsq, &v.Y)
|
||||||
|
v.T.Subtract(&ZZ2, &v.Z)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negation.
|
||||||
|
|
||||||
|
// Negate sets v = -p, and returns v.
|
||||||
|
func (v *Point) Negate(p *Point) *Point {
|
||||||
|
checkInitialized(p)
|
||||||
|
v.x.Negate(&p.x)
|
||||||
|
v.y.Set(&p.y)
|
||||||
|
v.z.Set(&p.z)
|
||||||
|
v.t.Negate(&p.t)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns 1 if v is equivalent to u, and 0 otherwise.
|
||||||
|
func (v *Point) Equal(u *Point) int {
|
||||||
|
checkInitialized(v, u)
|
||||||
|
|
||||||
|
var t1, t2, t3, t4 field.Element
|
||||||
|
t1.Multiply(&v.x, &u.z)
|
||||||
|
t2.Multiply(&u.x, &v.z)
|
||||||
|
t3.Multiply(&v.y, &u.z)
|
||||||
|
t4.Multiply(&u.y, &v.z)
|
||||||
|
|
||||||
|
return t1.Equal(&t2) & t3.Equal(&t4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constant-time operations
|
||||||
|
|
||||||
|
// Select sets v to a if cond == 1 and to b if cond == 0.
|
||||||
|
func (v *projCached) Select(a, b *projCached, cond int) *projCached {
|
||||||
|
v.YplusX.Select(&a.YplusX, &b.YplusX, cond)
|
||||||
|
v.YminusX.Select(&a.YminusX, &b.YminusX, cond)
|
||||||
|
v.Z.Select(&a.Z, &b.Z, cond)
|
||||||
|
v.T2d.Select(&a.T2d, &b.T2d, cond)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select sets v to a if cond == 1 and to b if cond == 0.
|
||||||
|
func (v *affineCached) Select(a, b *affineCached, cond int) *affineCached {
|
||||||
|
v.YplusX.Select(&a.YplusX, &b.YplusX, cond)
|
||||||
|
v.YminusX.Select(&a.YminusX, &b.YminusX, cond)
|
||||||
|
v.T2d.Select(&a.T2d, &b.T2d, cond)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// CondNeg negates v if cond == 1 and leaves it unchanged if cond == 0.
|
||||||
|
func (v *projCached) CondNeg(cond int) *projCached {
|
||||||
|
v.YplusX.Swap(&v.YminusX, cond)
|
||||||
|
v.T2d.Select(new(field.Element).Negate(&v.T2d), &v.T2d, cond)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// CondNeg negates v if cond == 1 and leaves it unchanged if cond == 0.
|
||||||
|
func (v *affineCached) CondNeg(cond int) *affineCached {
|
||||||
|
v.YplusX.Swap(&v.YminusX, cond)
|
||||||
|
v.T2d.Select(new(field.Element).Negate(&v.T2d), &v.T2d, cond)
|
||||||
|
return v
|
||||||
|
}
|
||||||
401
vendor/filippo.io/edwards25519/extra.go
generated
vendored
Normal file
401
vendor/filippo.io/edwards25519/extra.go
generated
vendored
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
// Copyright (c) 2021 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package edwards25519
|
||||||
|
|
||||||
|
// This file contains additional functionality that is not included in the
|
||||||
|
// upstream crypto/internal/edwards25519 package.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"filippo.io/edwards25519/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExtendedCoordinates returns v in extended coordinates (X:Y:Z:T) where
|
||||||
|
// x = X/Z, y = Y/Z, and xy = T/Z as in https://eprint.iacr.org/2008/522.
|
||||||
|
func (v *Point) ExtendedCoordinates() (X, Y, Z, T *field.Element) {
|
||||||
|
// This function is outlined to make the allocations inline in the caller
|
||||||
|
// rather than happen on the heap. Don't change the style without making
|
||||||
|
// sure it doesn't increase the inliner cost.
|
||||||
|
var e [4]field.Element
|
||||||
|
X, Y, Z, T = v.extendedCoordinates(&e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Point) extendedCoordinates(e *[4]field.Element) (X, Y, Z, T *field.Element) {
|
||||||
|
checkInitialized(v)
|
||||||
|
X = e[0].Set(&v.x)
|
||||||
|
Y = e[1].Set(&v.y)
|
||||||
|
Z = e[2].Set(&v.z)
|
||||||
|
T = e[3].Set(&v.t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExtendedCoordinates sets v = (X:Y:Z:T) in extended coordinates where
|
||||||
|
// x = X/Z, y = Y/Z, and xy = T/Z as in https://eprint.iacr.org/2008/522.
|
||||||
|
//
|
||||||
|
// If the coordinates are invalid or don't represent a valid point on the curve,
|
||||||
|
// SetExtendedCoordinates returns nil and an error and the receiver is
|
||||||
|
// unchanged. Otherwise, SetExtendedCoordinates returns v.
|
||||||
|
func (v *Point) SetExtendedCoordinates(X, Y, Z, T *field.Element) (*Point, error) {
|
||||||
|
if !isOnCurve(X, Y, Z, T) {
|
||||||
|
return nil, errors.New("edwards25519: invalid point coordinates")
|
||||||
|
}
|
||||||
|
v.x.Set(X)
|
||||||
|
v.y.Set(Y)
|
||||||
|
v.z.Set(Z)
|
||||||
|
v.t.Set(T)
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOnCurve(X, Y, Z, T *field.Element) bool {
|
||||||
|
var lhs, rhs field.Element
|
||||||
|
XX := new(field.Element).Square(X)
|
||||||
|
YY := new(field.Element).Square(Y)
|
||||||
|
ZZ := new(field.Element).Square(Z)
|
||||||
|
TT := new(field.Element).Square(T)
|
||||||
|
// -x² + y² = 1 + dx²y²
|
||||||
|
// -(X/Z)² + (Y/Z)² = 1 + d(T/Z)²
|
||||||
|
// -X² + Y² = Z² + dT²
|
||||||
|
lhs.Subtract(YY, XX)
|
||||||
|
rhs.Multiply(d, TT).Add(&rhs, ZZ)
|
||||||
|
if lhs.Equal(&rhs) != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// xy = T/Z
|
||||||
|
// XY/Z² = T/Z
|
||||||
|
// XY = TZ
|
||||||
|
lhs.Multiply(X, Y)
|
||||||
|
rhs.Multiply(T, Z)
|
||||||
|
return lhs.Equal(&rhs) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// BytesMontgomery converts v to a point on the birationally-equivalent
|
||||||
|
// Curve25519 Montgomery curve, and returns its canonical 32 bytes encoding
|
||||||
|
// according to RFC 7748.
|
||||||
|
//
|
||||||
|
// Note that BytesMontgomery only encodes the u-coordinate, so v and -v encode
|
||||||
|
// to the same value. If v is the identity point, BytesMontgomery returns 32
|
||||||
|
// zero bytes, analogously to the X25519 function.
|
||||||
|
//
|
||||||
|
// The lack of an inverse operation (such as SetMontgomeryBytes) is deliberate:
|
||||||
|
// while every valid edwards25519 point has a unique u-coordinate Montgomery
|
||||||
|
// encoding, X25519 accepts inputs on the quadratic twist, which don't correspond
|
||||||
|
// to any edwards25519 point, and every other X25519 input corresponds to two
|
||||||
|
// edwards25519 points.
|
||||||
|
func (v *Point) BytesMontgomery() []byte {
|
||||||
|
// This function is outlined to make the allocations inline in the caller
|
||||||
|
// rather than happen on the heap.
|
||||||
|
var buf [32]byte
|
||||||
|
return v.bytesMontgomery(&buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Point) bytesMontgomery(buf *[32]byte) []byte {
|
||||||
|
checkInitialized(v)
|
||||||
|
|
||||||
|
// RFC 7748, Section 4.1 provides the bilinear map to calculate the
|
||||||
|
// Montgomery u-coordinate
|
||||||
|
//
|
||||||
|
// u = (1 + y) / (1 - y)
|
||||||
|
//
|
||||||
|
// where y = Y / Z and therefore
|
||||||
|
//
|
||||||
|
// u = (Z + Y) / (Z - Y)
|
||||||
|
|
||||||
|
var n, r, u field.Element
|
||||||
|
|
||||||
|
n.Add(&v.z, &v.y) // n = Z + Y
|
||||||
|
r.Invert(r.Subtract(&v.z, &v.y)) // r = 1 / (Z - Y)
|
||||||
|
u.Multiply(&n, &r) // u = n * r
|
||||||
|
|
||||||
|
return copyFieldElement(buf, &u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultByCofactor sets v = 8 * p, and returns v.
|
||||||
|
func (v *Point) MultByCofactor(p *Point) *Point {
|
||||||
|
checkInitialized(p)
|
||||||
|
result := projP1xP1{}
|
||||||
|
pp := (&projP2{}).FromP3(p)
|
||||||
|
result.Double(pp)
|
||||||
|
pp.FromP1xP1(&result)
|
||||||
|
result.Double(pp)
|
||||||
|
pp.FromP1xP1(&result)
|
||||||
|
result.Double(pp)
|
||||||
|
return v.fromP1xP1(&result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given k > 0, set s = s**(2*k).
|
||||||
|
func (s *Scalar) pow2k(k int) {
|
||||||
|
for i := 0; i < k; i++ {
|
||||||
|
s.Multiply(s, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invert sets s to the inverse of a nonzero scalar v, and returns s.
|
||||||
|
//
|
||||||
|
// If t is zero, Invert returns zero.
|
||||||
|
func (s *Scalar) Invert(t *Scalar) *Scalar {
|
||||||
|
// Uses a hardcoded sliding window of width 4.
|
||||||
|
var table [8]Scalar
|
||||||
|
var tt Scalar
|
||||||
|
tt.Multiply(t, t)
|
||||||
|
table[0] = *t
|
||||||
|
for i := 0; i < 7; i++ {
|
||||||
|
table[i+1].Multiply(&table[i], &tt)
|
||||||
|
}
|
||||||
|
// Now table = [t**1, t**3, t**5, t**7, t**9, t**11, t**13, t**15]
|
||||||
|
// so t**k = t[k/2] for odd k
|
||||||
|
|
||||||
|
// To compute the sliding window digits, use the following Sage script:
|
||||||
|
|
||||||
|
// sage: import itertools
|
||||||
|
// sage: def sliding_window(w,k):
|
||||||
|
// ....: digits = []
|
||||||
|
// ....: while k > 0:
|
||||||
|
// ....: if k % 2 == 1:
|
||||||
|
// ....: kmod = k % (2**w)
|
||||||
|
// ....: digits.append(kmod)
|
||||||
|
// ....: k = k - kmod
|
||||||
|
// ....: else:
|
||||||
|
// ....: digits.append(0)
|
||||||
|
// ....: k = k // 2
|
||||||
|
// ....: return digits
|
||||||
|
|
||||||
|
// Now we can compute s roughly as follows:
|
||||||
|
|
||||||
|
// sage: s = 1
|
||||||
|
// sage: for coeff in reversed(sliding_window(4,l-2)):
|
||||||
|
// ....: s = s*s
|
||||||
|
// ....: if coeff > 0 :
|
||||||
|
// ....: s = s*t**coeff
|
||||||
|
|
||||||
|
// This works on one bit at a time, with many runs of zeros.
|
||||||
|
// The digits can be collapsed into [(count, coeff)] as follows:
|
||||||
|
|
||||||
|
// sage: [(len(list(group)),d) for d,group in itertools.groupby(sliding_window(4,l-2))]
|
||||||
|
|
||||||
|
// Entries of the form (k, 0) turn into pow2k(k)
|
||||||
|
// Entries of the form (1, coeff) turn into a squaring and then a table lookup.
|
||||||
|
// We can fold the squaring into the previous pow2k(k) as pow2k(k+1).
|
||||||
|
|
||||||
|
*s = table[1/2]
|
||||||
|
s.pow2k(127 + 1)
|
||||||
|
s.Multiply(s, &table[1/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[9/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[11/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[13/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[15/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[7/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[15/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[5/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[1/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[15/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[15/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[7/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[3/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[11/2])
|
||||||
|
s.pow2k(5 + 1)
|
||||||
|
s.Multiply(s, &table[11/2])
|
||||||
|
s.pow2k(9 + 1)
|
||||||
|
s.Multiply(s, &table[9/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[3/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[3/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[3/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[9/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[7/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[3/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[13/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[7/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[9/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[15/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[11/2])
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiScalarMult sets v = sum(scalars[i] * points[i]), and returns v.
|
||||||
|
//
|
||||||
|
// Execution time depends only on the lengths of the two slices, which must match.
|
||||||
|
func (v *Point) MultiScalarMult(scalars []*Scalar, points []*Point) *Point {
|
||||||
|
if len(scalars) != len(points) {
|
||||||
|
panic("edwards25519: called MultiScalarMult with different size inputs")
|
||||||
|
}
|
||||||
|
checkInitialized(points...)
|
||||||
|
|
||||||
|
// Proceed as in the single-base case, but share doublings
|
||||||
|
// between each point in the multiscalar equation.
|
||||||
|
|
||||||
|
// Build lookup tables for each point
|
||||||
|
tables := make([]projLookupTable, 0, 2) // avoid allocation for small sizes
|
||||||
|
tables = slices.Grow(tables, len(points))[:len(points)]
|
||||||
|
for i := range tables {
|
||||||
|
tables[i].FromP3(points[i])
|
||||||
|
}
|
||||||
|
// Compute signed radix-16 digits for each scalar
|
||||||
|
digits := make([][64]int8, 0, 2) // avoid allocation for small sizes
|
||||||
|
digits = slices.Grow(digits, len(scalars))[:len(scalars)]
|
||||||
|
for i := range digits {
|
||||||
|
digits[i] = scalars[i].signedRadix16()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap first loop iteration to save computing 16*identity
|
||||||
|
multiple := &projCached{}
|
||||||
|
tmp1 := &projP1xP1{}
|
||||||
|
tmp2 := &projP2{}
|
||||||
|
// Lookup-and-add the appropriate multiple of each input point
|
||||||
|
v.Set(NewIdentityPoint())
|
||||||
|
for j := range tables {
|
||||||
|
tables[j].SelectInto(multiple, digits[j][63])
|
||||||
|
tmp1.Add(v, multiple) // tmp1 = v + x_(j,63)*Q in P1xP1 coords
|
||||||
|
v.fromP1xP1(tmp1) // update v
|
||||||
|
}
|
||||||
|
tmp2.FromP3(v) // set up tmp2 = v in P2 coords for next iteration
|
||||||
|
for i := 62; i >= 0; i-- {
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 2*(prev) in P1xP1 coords
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = 2*(prev) in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 4*(prev) in P1xP1 coords
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = 4*(prev) in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 8*(prev) in P1xP1 coords
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = 8*(prev) in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 16*(prev) in P1xP1 coords
|
||||||
|
v.fromP1xP1(tmp1) // v = 16*(prev) in P3 coords
|
||||||
|
// Lookup-and-add the appropriate multiple of each input point
|
||||||
|
for j := range tables {
|
||||||
|
tables[j].SelectInto(multiple, digits[j][i])
|
||||||
|
tmp1.Add(v, multiple) // tmp1 = v + x_(j,i)*Q in P1xP1 coords
|
||||||
|
v.fromP1xP1(tmp1) // update v
|
||||||
|
}
|
||||||
|
tmp2.FromP3(v) // set up tmp2 = v in P2 coords for next iteration
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// VarTimeMultiScalarMult sets v = sum(scalars[i] * points[i]), and returns v.
|
||||||
|
//
|
||||||
|
// Execution time depends on the inputs.
|
||||||
|
func (v *Point) VarTimeMultiScalarMult(scalars []*Scalar, points []*Point) *Point {
|
||||||
|
if len(scalars) != len(points) {
|
||||||
|
panic("edwards25519: called VarTimeMultiScalarMult with different size inputs")
|
||||||
|
}
|
||||||
|
checkInitialized(points...)
|
||||||
|
|
||||||
|
// Generalize double-base NAF computation to arbitrary sizes.
|
||||||
|
// Here all the points are dynamic, so we only use the smaller
|
||||||
|
// tables.
|
||||||
|
|
||||||
|
// Build lookup tables for each point
|
||||||
|
tables := make([]nafLookupTable5, len(points))
|
||||||
|
for i := range tables {
|
||||||
|
tables[i].FromP3(points[i])
|
||||||
|
}
|
||||||
|
// Compute a NAF for each scalar
|
||||||
|
nafs := make([][256]int8, len(scalars))
|
||||||
|
for i := range nafs {
|
||||||
|
nafs[i] = scalars[i].nonAdjacentForm(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
multiple := &projCached{}
|
||||||
|
tmp1 := &projP1xP1{}
|
||||||
|
tmp2 := &projP2{}
|
||||||
|
tmp2.Zero()
|
||||||
|
|
||||||
|
// Move from high to low bits, doubling the accumulator
|
||||||
|
// at each iteration and checking whether there is a nonzero
|
||||||
|
// coefficient to look up a multiple of.
|
||||||
|
//
|
||||||
|
// Skip trying to find the first nonzero coefficent, because
|
||||||
|
// searching might be more work than a few extra doublings.
|
||||||
|
for i := 255; i >= 0; i-- {
|
||||||
|
tmp1.Double(tmp2)
|
||||||
|
|
||||||
|
for j := range nafs {
|
||||||
|
if nafs[j][i] > 0 {
|
||||||
|
v.fromP1xP1(tmp1)
|
||||||
|
tables[j].SelectInto(multiple, nafs[j][i])
|
||||||
|
tmp1.Add(v, multiple)
|
||||||
|
} else if nafs[j][i] < 0 {
|
||||||
|
v.fromP1xP1(tmp1)
|
||||||
|
tables[j].SelectInto(multiple, -nafs[j][i])
|
||||||
|
tmp1.Sub(v, multiple)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp2.FromP1xP1(tmp1)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.fromP2(tmp2)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select sets v to a if cond == 1 and to b if cond == 0.
|
||||||
|
func (v *Point) Select(a, b *Point, cond int) *Point {
|
||||||
|
checkInitialized(a, b)
|
||||||
|
v.x.Select(&a.x, &b.x, cond)
|
||||||
|
v.y.Select(&a.y, &b.y, cond)
|
||||||
|
v.z.Select(&a.z, &b.z, cond)
|
||||||
|
v.t.Select(&a.t, &b.t, cond)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double sets v = p + p, and returns v.
|
||||||
|
func (v *Point) Double(p *Point) *Point {
|
||||||
|
checkInitialized(p)
|
||||||
|
|
||||||
|
pp := new(projP2).FromP3(p)
|
||||||
|
p1 := new(projP1xP1).Double(pp)
|
||||||
|
return v.fromP1xP1(p1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Point) addCached(p *Point, qCached *projCached) *Point {
|
||||||
|
result := new(projP1xP1).Add(p, qCached)
|
||||||
|
return v.fromP1xP1(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScalarMultSlow sets v = x * q, and returns v. It doesn't precompute a large
|
||||||
|
// table, so it is considerably slower, but requires less memory.
|
||||||
|
//
|
||||||
|
// The scalar multiplication is done in constant time.
|
||||||
|
func (v *Point) ScalarMultSlow(x *Scalar, q *Point) *Point {
|
||||||
|
checkInitialized(q)
|
||||||
|
|
||||||
|
s := x.Bytes()
|
||||||
|
qCached := new(projCached).FromP3(q)
|
||||||
|
v.Set(NewIdentityPoint())
|
||||||
|
t := new(Point)
|
||||||
|
|
||||||
|
for i := 255; i >= 0; i-- {
|
||||||
|
v.Double(v)
|
||||||
|
t.addCached(v, qCached)
|
||||||
|
cond := (s[i/8] >> (i % 8)) & 1
|
||||||
|
v.Select(t, v, int(cond))
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
420
vendor/filippo.io/edwards25519/field/fe.go
generated
vendored
Normal file
420
vendor/filippo.io/edwards25519/field/fe.go
generated
vendored
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
// Copyright (c) 2017 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package field implements fast arithmetic modulo 2^255-19.
|
||||||
|
package field
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math/bits"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Element represents an element of the field GF(2^255-19). Note that this
|
||||||
|
// is not a cryptographically secure group, and should only be used to interact
|
||||||
|
// with edwards25519.Point coordinates.
|
||||||
|
//
|
||||||
|
// This type works similarly to math/big.Int, and all arguments and receivers
|
||||||
|
// are allowed to alias.
|
||||||
|
//
|
||||||
|
// The zero value is a valid zero element.
|
||||||
|
type Element struct {
|
||||||
|
// An element t represents the integer
|
||||||
|
// t.l0 + t.l1*2^51 + t.l2*2^102 + t.l3*2^153 + t.l4*2^204
|
||||||
|
//
|
||||||
|
// Between operations, all limbs are expected to be lower than 2^52.
|
||||||
|
l0 uint64
|
||||||
|
l1 uint64
|
||||||
|
l2 uint64
|
||||||
|
l3 uint64
|
||||||
|
l4 uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskLow51Bits uint64 = (1 << 51) - 1
|
||||||
|
|
||||||
|
var feZero = &Element{0, 0, 0, 0, 0}
|
||||||
|
|
||||||
|
// Zero sets v = 0, and returns v.
|
||||||
|
func (v *Element) Zero() *Element {
|
||||||
|
*v = *feZero
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
var feOne = &Element{1, 0, 0, 0, 0}
|
||||||
|
|
||||||
|
// One sets v = 1, and returns v.
|
||||||
|
func (v *Element) One() *Element {
|
||||||
|
*v = *feOne
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// reduce reduces v modulo 2^255 - 19 and returns it.
|
||||||
|
func (v *Element) reduce() *Element {
|
||||||
|
v.carryPropagate()
|
||||||
|
|
||||||
|
// After the light reduction we now have a field element representation
|
||||||
|
// v < 2^255 + 2^13 * 19, but need v < 2^255 - 19.
|
||||||
|
|
||||||
|
// If v >= 2^255 - 19, then v + 19 >= 2^255, which would overflow 2^255 - 1,
|
||||||
|
// generating a carry. That is, c will be 0 if v < 2^255 - 19, and 1 otherwise.
|
||||||
|
c := (v.l0 + 19) >> 51
|
||||||
|
c = (v.l1 + c) >> 51
|
||||||
|
c = (v.l2 + c) >> 51
|
||||||
|
c = (v.l3 + c) >> 51
|
||||||
|
c = (v.l4 + c) >> 51
|
||||||
|
|
||||||
|
// If v < 2^255 - 19 and c = 0, this will be a no-op. Otherwise, it's
|
||||||
|
// effectively applying the reduction identity to the carry.
|
||||||
|
v.l0 += 19 * c
|
||||||
|
|
||||||
|
v.l1 += v.l0 >> 51
|
||||||
|
v.l0 = v.l0 & maskLow51Bits
|
||||||
|
v.l2 += v.l1 >> 51
|
||||||
|
v.l1 = v.l1 & maskLow51Bits
|
||||||
|
v.l3 += v.l2 >> 51
|
||||||
|
v.l2 = v.l2 & maskLow51Bits
|
||||||
|
v.l4 += v.l3 >> 51
|
||||||
|
v.l3 = v.l3 & maskLow51Bits
|
||||||
|
// no additional carry
|
||||||
|
v.l4 = v.l4 & maskLow51Bits
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sets v = a + b, and returns v.
|
||||||
|
func (v *Element) Add(a, b *Element) *Element {
|
||||||
|
v.l0 = a.l0 + b.l0
|
||||||
|
v.l1 = a.l1 + b.l1
|
||||||
|
v.l2 = a.l2 + b.l2
|
||||||
|
v.l3 = a.l3 + b.l3
|
||||||
|
v.l4 = a.l4 + b.l4
|
||||||
|
return v.carryPropagate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract sets v = a - b, and returns v.
|
||||||
|
func (v *Element) Subtract(a, b *Element) *Element {
|
||||||
|
// We first add 2 * p, to guarantee the subtraction won't underflow, and
|
||||||
|
// then subtract b (which can be up to 2^255 + 2^13 * 19).
|
||||||
|
v.l0 = (a.l0 + 0xFFFFFFFFFFFDA) - b.l0
|
||||||
|
v.l1 = (a.l1 + 0xFFFFFFFFFFFFE) - b.l1
|
||||||
|
v.l2 = (a.l2 + 0xFFFFFFFFFFFFE) - b.l2
|
||||||
|
v.l3 = (a.l3 + 0xFFFFFFFFFFFFE) - b.l3
|
||||||
|
v.l4 = (a.l4 + 0xFFFFFFFFFFFFE) - b.l4
|
||||||
|
return v.carryPropagate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negate sets v = -a, and returns v.
|
||||||
|
func (v *Element) Negate(a *Element) *Element {
|
||||||
|
return v.Subtract(feZero, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invert sets v = 1/z mod p, and returns v.
|
||||||
|
//
|
||||||
|
// If z == 0, Invert returns v = 0.
|
||||||
|
func (v *Element) Invert(z *Element) *Element {
|
||||||
|
// Inversion is implemented as exponentiation with exponent p − 2. It uses the
|
||||||
|
// same sequence of 255 squarings and 11 multiplications as [Curve25519].
|
||||||
|
var z2, z9, z11, z2_5_0, z2_10_0, z2_20_0, z2_50_0, z2_100_0, t Element
|
||||||
|
|
||||||
|
z2.Square(z) // 2
|
||||||
|
t.Square(&z2) // 4
|
||||||
|
t.Square(&t) // 8
|
||||||
|
z9.Multiply(&t, z) // 9
|
||||||
|
z11.Multiply(&z9, &z2) // 11
|
||||||
|
t.Square(&z11) // 22
|
||||||
|
z2_5_0.Multiply(&t, &z9) // 31 = 2^5 - 2^0
|
||||||
|
|
||||||
|
t.Square(&z2_5_0) // 2^6 - 2^1
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
t.Square(&t) // 2^10 - 2^5
|
||||||
|
}
|
||||||
|
z2_10_0.Multiply(&t, &z2_5_0) // 2^10 - 2^0
|
||||||
|
|
||||||
|
t.Square(&z2_10_0) // 2^11 - 2^1
|
||||||
|
for i := 0; i < 9; i++ {
|
||||||
|
t.Square(&t) // 2^20 - 2^10
|
||||||
|
}
|
||||||
|
z2_20_0.Multiply(&t, &z2_10_0) // 2^20 - 2^0
|
||||||
|
|
||||||
|
t.Square(&z2_20_0) // 2^21 - 2^1
|
||||||
|
for i := 0; i < 19; i++ {
|
||||||
|
t.Square(&t) // 2^40 - 2^20
|
||||||
|
}
|
||||||
|
t.Multiply(&t, &z2_20_0) // 2^40 - 2^0
|
||||||
|
|
||||||
|
t.Square(&t) // 2^41 - 2^1
|
||||||
|
for i := 0; i < 9; i++ {
|
||||||
|
t.Square(&t) // 2^50 - 2^10
|
||||||
|
}
|
||||||
|
z2_50_0.Multiply(&t, &z2_10_0) // 2^50 - 2^0
|
||||||
|
|
||||||
|
t.Square(&z2_50_0) // 2^51 - 2^1
|
||||||
|
for i := 0; i < 49; i++ {
|
||||||
|
t.Square(&t) // 2^100 - 2^50
|
||||||
|
}
|
||||||
|
z2_100_0.Multiply(&t, &z2_50_0) // 2^100 - 2^0
|
||||||
|
|
||||||
|
t.Square(&z2_100_0) // 2^101 - 2^1
|
||||||
|
for i := 0; i < 99; i++ {
|
||||||
|
t.Square(&t) // 2^200 - 2^100
|
||||||
|
}
|
||||||
|
t.Multiply(&t, &z2_100_0) // 2^200 - 2^0
|
||||||
|
|
||||||
|
t.Square(&t) // 2^201 - 2^1
|
||||||
|
for i := 0; i < 49; i++ {
|
||||||
|
t.Square(&t) // 2^250 - 2^50
|
||||||
|
}
|
||||||
|
t.Multiply(&t, &z2_50_0) // 2^250 - 2^0
|
||||||
|
|
||||||
|
t.Square(&t) // 2^251 - 2^1
|
||||||
|
t.Square(&t) // 2^252 - 2^2
|
||||||
|
t.Square(&t) // 2^253 - 2^3
|
||||||
|
t.Square(&t) // 2^254 - 2^4
|
||||||
|
t.Square(&t) // 2^255 - 2^5
|
||||||
|
|
||||||
|
return v.Multiply(&t, &z11) // 2^255 - 21
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets v = a, and returns v.
|
||||||
|
func (v *Element) Set(a *Element) *Element {
|
||||||
|
*v = *a
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBytes sets v to x, where x is a 32-byte little-endian encoding. If x is
|
||||||
|
// not of the right length, SetBytes returns nil and an error, and the
|
||||||
|
// receiver is unchanged.
|
||||||
|
//
|
||||||
|
// Consistent with RFC 7748, the most significant bit (the high bit of the
|
||||||
|
// last byte) is ignored, and non-canonical values (2^255-19 through 2^255-1)
|
||||||
|
// are accepted. Note that this is laxer than specified by RFC 8032, but
|
||||||
|
// consistent with most Ed25519 implementations.
|
||||||
|
func (v *Element) SetBytes(x []byte) (*Element, error) {
|
||||||
|
if len(x) != 32 {
|
||||||
|
return nil, errors.New("edwards25519: invalid field element input size")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bits 0:51 (bytes 0:8, bits 0:64, shift 0, mask 51).
|
||||||
|
v.l0 = binary.LittleEndian.Uint64(x[0:8])
|
||||||
|
v.l0 &= maskLow51Bits
|
||||||
|
// Bits 51:102 (bytes 6:14, bits 48:112, shift 3, mask 51).
|
||||||
|
v.l1 = binary.LittleEndian.Uint64(x[6:14]) >> 3
|
||||||
|
v.l1 &= maskLow51Bits
|
||||||
|
// Bits 102:153 (bytes 12:20, bits 96:160, shift 6, mask 51).
|
||||||
|
v.l2 = binary.LittleEndian.Uint64(x[12:20]) >> 6
|
||||||
|
v.l2 &= maskLow51Bits
|
||||||
|
// Bits 153:204 (bytes 19:27, bits 152:216, shift 1, mask 51).
|
||||||
|
v.l3 = binary.LittleEndian.Uint64(x[19:27]) >> 1
|
||||||
|
v.l3 &= maskLow51Bits
|
||||||
|
// Bits 204:255 (bytes 24:32, bits 192:256, shift 12, mask 51).
|
||||||
|
// Note: not bytes 25:33, shift 4, to avoid overread.
|
||||||
|
v.l4 = binary.LittleEndian.Uint64(x[24:32]) >> 12
|
||||||
|
v.l4 &= maskLow51Bits
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the canonical 32-byte little-endian encoding of v.
|
||||||
|
func (v *Element) Bytes() []byte {
|
||||||
|
// This function is outlined to make the allocations inline in the caller
|
||||||
|
// rather than happen on the heap.
|
||||||
|
var out [32]byte
|
||||||
|
return v.bytes(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Element) bytes(out *[32]byte) []byte {
|
||||||
|
t := *v
|
||||||
|
t.reduce()
|
||||||
|
|
||||||
|
// Pack five 51-bit limbs into four 64-bit words:
|
||||||
|
//
|
||||||
|
// 255 204 153 102 51 0
|
||||||
|
// ├──l4──┼──l3──┼──l2──┼──l1──┼──l0──┤
|
||||||
|
// ├───u3───┼───u2───┼───u1───┼───u0───┤
|
||||||
|
// 256 192 128 64 0
|
||||||
|
|
||||||
|
u0 := t.l1<<51 | t.l0
|
||||||
|
u1 := t.l2<<(102-64) | t.l1>>(64-51)
|
||||||
|
u2 := t.l3<<(153-128) | t.l2>>(128-102)
|
||||||
|
u3 := t.l4<<(204-192) | t.l3>>(192-153)
|
||||||
|
|
||||||
|
binary.LittleEndian.PutUint64(out[0*8:], u0)
|
||||||
|
binary.LittleEndian.PutUint64(out[1*8:], u1)
|
||||||
|
binary.LittleEndian.PutUint64(out[2*8:], u2)
|
||||||
|
binary.LittleEndian.PutUint64(out[3*8:], u3)
|
||||||
|
|
||||||
|
return out[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns 1 if v and u are equal, and 0 otherwise.
|
||||||
|
func (v *Element) Equal(u *Element) int {
|
||||||
|
sa, sv := u.Bytes(), v.Bytes()
|
||||||
|
return subtle.ConstantTimeCompare(sa, sv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mask64Bits returns 0xffffffff if cond is 1, and 0 otherwise.
|
||||||
|
func mask64Bits(cond int) uint64 { return ^(uint64(cond) - 1) }
|
||||||
|
|
||||||
|
// Select sets v to a if cond == 1, and to b if cond == 0.
|
||||||
|
func (v *Element) Select(a, b *Element, cond int) *Element {
|
||||||
|
m := mask64Bits(cond)
|
||||||
|
v.l0 = (m & a.l0) | (^m & b.l0)
|
||||||
|
v.l1 = (m & a.l1) | (^m & b.l1)
|
||||||
|
v.l2 = (m & a.l2) | (^m & b.l2)
|
||||||
|
v.l3 = (m & a.l3) | (^m & b.l3)
|
||||||
|
v.l4 = (m & a.l4) | (^m & b.l4)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap swaps v and u if cond == 1 or leaves them unchanged if cond == 0, and returns v.
|
||||||
|
func (v *Element) Swap(u *Element, cond int) {
|
||||||
|
m := mask64Bits(cond)
|
||||||
|
t := m & (v.l0 ^ u.l0)
|
||||||
|
v.l0 ^= t
|
||||||
|
u.l0 ^= t
|
||||||
|
t = m & (v.l1 ^ u.l1)
|
||||||
|
v.l1 ^= t
|
||||||
|
u.l1 ^= t
|
||||||
|
t = m & (v.l2 ^ u.l2)
|
||||||
|
v.l2 ^= t
|
||||||
|
u.l2 ^= t
|
||||||
|
t = m & (v.l3 ^ u.l3)
|
||||||
|
v.l3 ^= t
|
||||||
|
u.l3 ^= t
|
||||||
|
t = m & (v.l4 ^ u.l4)
|
||||||
|
v.l4 ^= t
|
||||||
|
u.l4 ^= t
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNegative returns 1 if v is negative, and 0 otherwise.
|
||||||
|
func (v *Element) IsNegative() int {
|
||||||
|
return int(v.Bytes()[0] & 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute sets v to |u|, and returns v.
|
||||||
|
func (v *Element) Absolute(u *Element) *Element {
|
||||||
|
return v.Select(new(Element).Negate(u), u, u.IsNegative())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply sets v = x * y, and returns v.
|
||||||
|
func (v *Element) Multiply(x, y *Element) *Element {
|
||||||
|
feMul(v, x, y)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Square sets v = x * x, and returns v.
|
||||||
|
func (v *Element) Square(x *Element) *Element {
|
||||||
|
feSquare(v, x)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mult32 sets v = x * y, and returns v.
|
||||||
|
func (v *Element) Mult32(x *Element, y uint32) *Element {
|
||||||
|
x0lo, x0hi := mul51(x.l0, y)
|
||||||
|
x1lo, x1hi := mul51(x.l1, y)
|
||||||
|
x2lo, x2hi := mul51(x.l2, y)
|
||||||
|
x3lo, x3hi := mul51(x.l3, y)
|
||||||
|
x4lo, x4hi := mul51(x.l4, y)
|
||||||
|
v.l0 = x0lo + 19*x4hi // carried over per the reduction identity
|
||||||
|
v.l1 = x1lo + x0hi
|
||||||
|
v.l2 = x2lo + x1hi
|
||||||
|
v.l3 = x3lo + x2hi
|
||||||
|
v.l4 = x4lo + x3hi
|
||||||
|
// The hi portions are going to be only 32 bits, plus any previous excess,
|
||||||
|
// so we can skip the carry propagation.
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// mul51 returns lo + hi * 2⁵¹ = a * b.
|
||||||
|
func mul51(a uint64, b uint32) (lo uint64, hi uint64) {
|
||||||
|
mh, ml := bits.Mul64(a, uint64(b))
|
||||||
|
lo = ml & maskLow51Bits
|
||||||
|
hi = (mh << 13) | (ml >> 51)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pow22523 set v = x^((p-5)/8), and returns v. (p-5)/8 is 2^252-3.
|
||||||
|
func (v *Element) Pow22523(x *Element) *Element {
|
||||||
|
var t0, t1, t2 Element
|
||||||
|
|
||||||
|
t0.Square(x) // x^2
|
||||||
|
t1.Square(&t0) // x^4
|
||||||
|
t1.Square(&t1) // x^8
|
||||||
|
t1.Multiply(x, &t1) // x^9
|
||||||
|
t0.Multiply(&t0, &t1) // x^11
|
||||||
|
t0.Square(&t0) // x^22
|
||||||
|
t0.Multiply(&t1, &t0) // x^31
|
||||||
|
t1.Square(&t0) // x^62
|
||||||
|
for i := 1; i < 5; i++ { // x^992
|
||||||
|
t1.Square(&t1)
|
||||||
|
}
|
||||||
|
t0.Multiply(&t1, &t0) // x^1023 -> 1023 = 2^10 - 1
|
||||||
|
t1.Square(&t0) // 2^11 - 2
|
||||||
|
for i := 1; i < 10; i++ { // 2^20 - 2^10
|
||||||
|
t1.Square(&t1)
|
||||||
|
}
|
||||||
|
t1.Multiply(&t1, &t0) // 2^20 - 1
|
||||||
|
t2.Square(&t1) // 2^21 - 2
|
||||||
|
for i := 1; i < 20; i++ { // 2^40 - 2^20
|
||||||
|
t2.Square(&t2)
|
||||||
|
}
|
||||||
|
t1.Multiply(&t2, &t1) // 2^40 - 1
|
||||||
|
t1.Square(&t1) // 2^41 - 2
|
||||||
|
for i := 1; i < 10; i++ { // 2^50 - 2^10
|
||||||
|
t1.Square(&t1)
|
||||||
|
}
|
||||||
|
t0.Multiply(&t1, &t0) // 2^50 - 1
|
||||||
|
t1.Square(&t0) // 2^51 - 2
|
||||||
|
for i := 1; i < 50; i++ { // 2^100 - 2^50
|
||||||
|
t1.Square(&t1)
|
||||||
|
}
|
||||||
|
t1.Multiply(&t1, &t0) // 2^100 - 1
|
||||||
|
t2.Square(&t1) // 2^101 - 2
|
||||||
|
for i := 1; i < 100; i++ { // 2^200 - 2^100
|
||||||
|
t2.Square(&t2)
|
||||||
|
}
|
||||||
|
t1.Multiply(&t2, &t1) // 2^200 - 1
|
||||||
|
t1.Square(&t1) // 2^201 - 2
|
||||||
|
for i := 1; i < 50; i++ { // 2^250 - 2^50
|
||||||
|
t1.Square(&t1)
|
||||||
|
}
|
||||||
|
t0.Multiply(&t1, &t0) // 2^250 - 1
|
||||||
|
t0.Square(&t0) // 2^251 - 2
|
||||||
|
t0.Square(&t0) // 2^252 - 4
|
||||||
|
return v.Multiply(&t0, x) // 2^252 - 3 -> x^(2^252-3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sqrtM1 is 2^((p-1)/4), which squared is equal to -1 by Euler's Criterion.
|
||||||
|
var sqrtM1 = &Element{1718705420411056, 234908883556509,
|
||||||
|
2233514472574048, 2117202627021982, 765476049583133}
|
||||||
|
|
||||||
|
// SqrtRatio sets r to the non-negative square root of the ratio of u and v.
|
||||||
|
//
|
||||||
|
// If u/v is square, SqrtRatio returns r and 1. If u/v is not square, SqrtRatio
|
||||||
|
// sets r according to Section 4.3 of draft-irtf-cfrg-ristretto255-decaf448-00,
|
||||||
|
// and returns r and 0.
|
||||||
|
func (r *Element) SqrtRatio(u, v *Element) (R *Element, wasSquare int) {
|
||||||
|
t0 := new(Element)
|
||||||
|
|
||||||
|
// r = (u * v3) * (u * v7)^((p-5)/8)
|
||||||
|
v2 := new(Element).Square(v)
|
||||||
|
uv3 := new(Element).Multiply(u, t0.Multiply(v2, v))
|
||||||
|
uv7 := new(Element).Multiply(uv3, t0.Square(v2))
|
||||||
|
rr := new(Element).Multiply(uv3, t0.Pow22523(uv7))
|
||||||
|
|
||||||
|
check := new(Element).Multiply(v, t0.Square(rr)) // check = v * r^2
|
||||||
|
|
||||||
|
uNeg := new(Element).Negate(u)
|
||||||
|
correctSignSqrt := check.Equal(u)
|
||||||
|
flippedSignSqrt := check.Equal(uNeg)
|
||||||
|
flippedSignSqrtI := check.Equal(t0.Multiply(uNeg, sqrtM1))
|
||||||
|
|
||||||
|
rPrime := new(Element).Multiply(rr, sqrtM1) // r_prime = SQRT_M1 * r
|
||||||
|
// r = CT_SELECT(r_prime IF flipped_sign_sqrt | flipped_sign_sqrt_i ELSE r)
|
||||||
|
rr.Select(rPrime, rr, flippedSignSqrt|flippedSignSqrtI)
|
||||||
|
|
||||||
|
r.Absolute(rr) // Choose the nonnegative square root.
|
||||||
|
return r, correctSignSqrt | flippedSignSqrt
|
||||||
|
}
|
||||||
15
vendor/filippo.io/edwards25519/field/fe_amd64.go
generated
vendored
Normal file
15
vendor/filippo.io/edwards25519/field/fe_amd64.go
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:build !purego
|
||||||
|
|
||||||
|
package field
|
||||||
|
|
||||||
|
// feMul sets out = a * b. It works like feMulGeneric.
|
||||||
|
//
|
||||||
|
//go:noescape
|
||||||
|
func feMul(out *Element, a *Element, b *Element)
|
||||||
|
|
||||||
|
// feSquare sets out = a * a. It works like feSquareGeneric.
|
||||||
|
//
|
||||||
|
//go:noescape
|
||||||
|
func feSquare(out *Element, a *Element)
|
||||||
398
vendor/filippo.io/edwards25519/field/fe_amd64.s
generated
vendored
Normal file
398
vendor/filippo.io/edwards25519/field/fe_amd64.s
generated
vendored
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:build !purego
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func feMul(out *Element, a *Element, b *Element)
|
||||||
|
TEXT ·feMul(SB), NOSPLIT, $0-24
|
||||||
|
MOVQ a+8(FP), CX
|
||||||
|
MOVQ b+16(FP), BX
|
||||||
|
|
||||||
|
// r0 = a0×b0
|
||||||
|
MOVQ (CX), AX
|
||||||
|
MULQ (BX)
|
||||||
|
MOVQ AX, DI
|
||||||
|
MOVQ DX, SI
|
||||||
|
|
||||||
|
// r0 += 19×a1×b4
|
||||||
|
MOVQ 8(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
MULQ 32(BX)
|
||||||
|
ADDQ AX, DI
|
||||||
|
ADCQ DX, SI
|
||||||
|
|
||||||
|
// r0 += 19×a2×b3
|
||||||
|
MOVQ 16(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
MULQ 24(BX)
|
||||||
|
ADDQ AX, DI
|
||||||
|
ADCQ DX, SI
|
||||||
|
|
||||||
|
// r0 += 19×a3×b2
|
||||||
|
MOVQ 24(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
MULQ 16(BX)
|
||||||
|
ADDQ AX, DI
|
||||||
|
ADCQ DX, SI
|
||||||
|
|
||||||
|
// r0 += 19×a4×b1
|
||||||
|
MOVQ 32(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
MULQ 8(BX)
|
||||||
|
ADDQ AX, DI
|
||||||
|
ADCQ DX, SI
|
||||||
|
|
||||||
|
// r1 = a0×b1
|
||||||
|
MOVQ (CX), AX
|
||||||
|
MULQ 8(BX)
|
||||||
|
MOVQ AX, R9
|
||||||
|
MOVQ DX, R8
|
||||||
|
|
||||||
|
// r1 += a1×b0
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
MULQ (BX)
|
||||||
|
ADDQ AX, R9
|
||||||
|
ADCQ DX, R8
|
||||||
|
|
||||||
|
// r1 += 19×a2×b4
|
||||||
|
MOVQ 16(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
MULQ 32(BX)
|
||||||
|
ADDQ AX, R9
|
||||||
|
ADCQ DX, R8
|
||||||
|
|
||||||
|
// r1 += 19×a3×b3
|
||||||
|
MOVQ 24(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
MULQ 24(BX)
|
||||||
|
ADDQ AX, R9
|
||||||
|
ADCQ DX, R8
|
||||||
|
|
||||||
|
// r1 += 19×a4×b2
|
||||||
|
MOVQ 32(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
MULQ 16(BX)
|
||||||
|
ADDQ AX, R9
|
||||||
|
ADCQ DX, R8
|
||||||
|
|
||||||
|
// r2 = a0×b2
|
||||||
|
MOVQ (CX), AX
|
||||||
|
MULQ 16(BX)
|
||||||
|
MOVQ AX, R11
|
||||||
|
MOVQ DX, R10
|
||||||
|
|
||||||
|
// r2 += a1×b1
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
MULQ 8(BX)
|
||||||
|
ADDQ AX, R11
|
||||||
|
ADCQ DX, R10
|
||||||
|
|
||||||
|
// r2 += a2×b0
|
||||||
|
MOVQ 16(CX), AX
|
||||||
|
MULQ (BX)
|
||||||
|
ADDQ AX, R11
|
||||||
|
ADCQ DX, R10
|
||||||
|
|
||||||
|
// r2 += 19×a3×b4
|
||||||
|
MOVQ 24(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
MULQ 32(BX)
|
||||||
|
ADDQ AX, R11
|
||||||
|
ADCQ DX, R10
|
||||||
|
|
||||||
|
// r2 += 19×a4×b3
|
||||||
|
MOVQ 32(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
MULQ 24(BX)
|
||||||
|
ADDQ AX, R11
|
||||||
|
ADCQ DX, R10
|
||||||
|
|
||||||
|
// r3 = a0×b3
|
||||||
|
MOVQ (CX), AX
|
||||||
|
MULQ 24(BX)
|
||||||
|
MOVQ AX, R13
|
||||||
|
MOVQ DX, R12
|
||||||
|
|
||||||
|
// r3 += a1×b2
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
MULQ 16(BX)
|
||||||
|
ADDQ AX, R13
|
||||||
|
ADCQ DX, R12
|
||||||
|
|
||||||
|
// r3 += a2×b1
|
||||||
|
MOVQ 16(CX), AX
|
||||||
|
MULQ 8(BX)
|
||||||
|
ADDQ AX, R13
|
||||||
|
ADCQ DX, R12
|
||||||
|
|
||||||
|
// r3 += a3×b0
|
||||||
|
MOVQ 24(CX), AX
|
||||||
|
MULQ (BX)
|
||||||
|
ADDQ AX, R13
|
||||||
|
ADCQ DX, R12
|
||||||
|
|
||||||
|
// r3 += 19×a4×b4
|
||||||
|
MOVQ 32(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
MULQ 32(BX)
|
||||||
|
ADDQ AX, R13
|
||||||
|
ADCQ DX, R12
|
||||||
|
|
||||||
|
// r4 = a0×b4
|
||||||
|
MOVQ (CX), AX
|
||||||
|
MULQ 32(BX)
|
||||||
|
MOVQ AX, R15
|
||||||
|
MOVQ DX, R14
|
||||||
|
|
||||||
|
// r4 += a1×b3
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
MULQ 24(BX)
|
||||||
|
ADDQ AX, R15
|
||||||
|
ADCQ DX, R14
|
||||||
|
|
||||||
|
// r4 += a2×b2
|
||||||
|
MOVQ 16(CX), AX
|
||||||
|
MULQ 16(BX)
|
||||||
|
ADDQ AX, R15
|
||||||
|
ADCQ DX, R14
|
||||||
|
|
||||||
|
// r4 += a3×b1
|
||||||
|
MOVQ 24(CX), AX
|
||||||
|
MULQ 8(BX)
|
||||||
|
ADDQ AX, R15
|
||||||
|
ADCQ DX, R14
|
||||||
|
|
||||||
|
// r4 += a4×b0
|
||||||
|
MOVQ 32(CX), AX
|
||||||
|
MULQ (BX)
|
||||||
|
ADDQ AX, R15
|
||||||
|
ADCQ DX, R14
|
||||||
|
|
||||||
|
// First reduction chain
|
||||||
|
MOVQ $0x0007ffffffffffff, AX
|
||||||
|
SHLQ $0x0d, DI, SI
|
||||||
|
SHLQ $0x0d, R9, R8
|
||||||
|
SHLQ $0x0d, R11, R10
|
||||||
|
SHLQ $0x0d, R13, R12
|
||||||
|
SHLQ $0x0d, R15, R14
|
||||||
|
ANDQ AX, DI
|
||||||
|
IMUL3Q $0x13, R14, R14
|
||||||
|
ADDQ R14, DI
|
||||||
|
ANDQ AX, R9
|
||||||
|
ADDQ SI, R9
|
||||||
|
ANDQ AX, R11
|
||||||
|
ADDQ R8, R11
|
||||||
|
ANDQ AX, R13
|
||||||
|
ADDQ R10, R13
|
||||||
|
ANDQ AX, R15
|
||||||
|
ADDQ R12, R15
|
||||||
|
|
||||||
|
// Second reduction chain (carryPropagate)
|
||||||
|
MOVQ DI, SI
|
||||||
|
SHRQ $0x33, SI
|
||||||
|
MOVQ R9, R8
|
||||||
|
SHRQ $0x33, R8
|
||||||
|
MOVQ R11, R10
|
||||||
|
SHRQ $0x33, R10
|
||||||
|
MOVQ R13, R12
|
||||||
|
SHRQ $0x33, R12
|
||||||
|
MOVQ R15, R14
|
||||||
|
SHRQ $0x33, R14
|
||||||
|
ANDQ AX, DI
|
||||||
|
IMUL3Q $0x13, R14, R14
|
||||||
|
ADDQ R14, DI
|
||||||
|
ANDQ AX, R9
|
||||||
|
ADDQ SI, R9
|
||||||
|
ANDQ AX, R11
|
||||||
|
ADDQ R8, R11
|
||||||
|
ANDQ AX, R13
|
||||||
|
ADDQ R10, R13
|
||||||
|
ANDQ AX, R15
|
||||||
|
ADDQ R12, R15
|
||||||
|
|
||||||
|
// Store output
|
||||||
|
MOVQ out+0(FP), AX
|
||||||
|
MOVQ DI, (AX)
|
||||||
|
MOVQ R9, 8(AX)
|
||||||
|
MOVQ R11, 16(AX)
|
||||||
|
MOVQ R13, 24(AX)
|
||||||
|
MOVQ R15, 32(AX)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func feSquare(out *Element, a *Element)
|
||||||
|
TEXT ·feSquare(SB), NOSPLIT, $0-16
|
||||||
|
MOVQ a+8(FP), CX
|
||||||
|
|
||||||
|
// r0 = l0×l0
|
||||||
|
MOVQ (CX), AX
|
||||||
|
MULQ (CX)
|
||||||
|
MOVQ AX, SI
|
||||||
|
MOVQ DX, BX
|
||||||
|
|
||||||
|
// r0 += 38×l1×l4
|
||||||
|
MOVQ 8(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 32(CX)
|
||||||
|
ADDQ AX, SI
|
||||||
|
ADCQ DX, BX
|
||||||
|
|
||||||
|
// r0 += 38×l2×l3
|
||||||
|
MOVQ 16(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 24(CX)
|
||||||
|
ADDQ AX, SI
|
||||||
|
ADCQ DX, BX
|
||||||
|
|
||||||
|
// r1 = 2×l0×l1
|
||||||
|
MOVQ (CX), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 8(CX)
|
||||||
|
MOVQ AX, R8
|
||||||
|
MOVQ DX, DI
|
||||||
|
|
||||||
|
// r1 += 38×l2×l4
|
||||||
|
MOVQ 16(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 32(CX)
|
||||||
|
ADDQ AX, R8
|
||||||
|
ADCQ DX, DI
|
||||||
|
|
||||||
|
// r1 += 19×l3×l3
|
||||||
|
MOVQ 24(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
MULQ 24(CX)
|
||||||
|
ADDQ AX, R8
|
||||||
|
ADCQ DX, DI
|
||||||
|
|
||||||
|
// r2 = 2×l0×l2
|
||||||
|
MOVQ (CX), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 16(CX)
|
||||||
|
MOVQ AX, R10
|
||||||
|
MOVQ DX, R9
|
||||||
|
|
||||||
|
// r2 += l1×l1
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
MULQ 8(CX)
|
||||||
|
ADDQ AX, R10
|
||||||
|
ADCQ DX, R9
|
||||||
|
|
||||||
|
// r2 += 38×l3×l4
|
||||||
|
MOVQ 24(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 32(CX)
|
||||||
|
ADDQ AX, R10
|
||||||
|
ADCQ DX, R9
|
||||||
|
|
||||||
|
// r3 = 2×l0×l3
|
||||||
|
MOVQ (CX), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 24(CX)
|
||||||
|
MOVQ AX, R12
|
||||||
|
MOVQ DX, R11
|
||||||
|
|
||||||
|
// r3 += 2×l1×l2
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 16(CX)
|
||||||
|
ADDQ AX, R12
|
||||||
|
ADCQ DX, R11
|
||||||
|
|
||||||
|
// r3 += 19×l4×l4
|
||||||
|
MOVQ 32(CX), DX
|
||||||
|
LEAQ (DX)(DX*8), AX
|
||||||
|
LEAQ (DX)(AX*2), AX
|
||||||
|
MULQ 32(CX)
|
||||||
|
ADDQ AX, R12
|
||||||
|
ADCQ DX, R11
|
||||||
|
|
||||||
|
// r4 = 2×l0×l4
|
||||||
|
MOVQ (CX), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 32(CX)
|
||||||
|
MOVQ AX, R14
|
||||||
|
MOVQ DX, R13
|
||||||
|
|
||||||
|
// r4 += 2×l1×l3
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 24(CX)
|
||||||
|
ADDQ AX, R14
|
||||||
|
ADCQ DX, R13
|
||||||
|
|
||||||
|
// r4 += l2×l2
|
||||||
|
MOVQ 16(CX), AX
|
||||||
|
MULQ 16(CX)
|
||||||
|
ADDQ AX, R14
|
||||||
|
ADCQ DX, R13
|
||||||
|
|
||||||
|
// First reduction chain
|
||||||
|
MOVQ $0x0007ffffffffffff, AX
|
||||||
|
SHLQ $0x0d, SI, BX
|
||||||
|
SHLQ $0x0d, R8, DI
|
||||||
|
SHLQ $0x0d, R10, R9
|
||||||
|
SHLQ $0x0d, R12, R11
|
||||||
|
SHLQ $0x0d, R14, R13
|
||||||
|
ANDQ AX, SI
|
||||||
|
IMUL3Q $0x13, R13, R13
|
||||||
|
ADDQ R13, SI
|
||||||
|
ANDQ AX, R8
|
||||||
|
ADDQ BX, R8
|
||||||
|
ANDQ AX, R10
|
||||||
|
ADDQ DI, R10
|
||||||
|
ANDQ AX, R12
|
||||||
|
ADDQ R9, R12
|
||||||
|
ANDQ AX, R14
|
||||||
|
ADDQ R11, R14
|
||||||
|
|
||||||
|
// Second reduction chain (carryPropagate)
|
||||||
|
MOVQ SI, BX
|
||||||
|
SHRQ $0x33, BX
|
||||||
|
MOVQ R8, DI
|
||||||
|
SHRQ $0x33, DI
|
||||||
|
MOVQ R10, R9
|
||||||
|
SHRQ $0x33, R9
|
||||||
|
MOVQ R12, R11
|
||||||
|
SHRQ $0x33, R11
|
||||||
|
MOVQ R14, R13
|
||||||
|
SHRQ $0x33, R13
|
||||||
|
ANDQ AX, SI
|
||||||
|
IMUL3Q $0x13, R13, R13
|
||||||
|
ADDQ R13, SI
|
||||||
|
ANDQ AX, R8
|
||||||
|
ADDQ BX, R8
|
||||||
|
ANDQ AX, R10
|
||||||
|
ADDQ DI, R10
|
||||||
|
ANDQ AX, R12
|
||||||
|
ADDQ R9, R12
|
||||||
|
ANDQ AX, R14
|
||||||
|
ADDQ R11, R14
|
||||||
|
|
||||||
|
// Store output
|
||||||
|
MOVQ out+0(FP), AX
|
||||||
|
MOVQ SI, (AX)
|
||||||
|
MOVQ R8, 8(AX)
|
||||||
|
MOVQ R10, 16(AX)
|
||||||
|
MOVQ R12, 24(AX)
|
||||||
|
MOVQ R14, 32(AX)
|
||||||
|
RET
|
||||||
11
vendor/filippo.io/edwards25519/field/fe_amd64_noasm.go
generated
vendored
Normal file
11
vendor/filippo.io/edwards25519/field/fe_amd64_noasm.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) 2019 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build !amd64 || purego
|
||||||
|
|
||||||
|
package field
|
||||||
|
|
||||||
|
func feMul(v, x, y *Element) { feMulGeneric(v, x, y) }
|
||||||
|
|
||||||
|
func feSquare(v, x *Element) { feSquareGeneric(v, x) }
|
||||||
50
vendor/filippo.io/edwards25519/field/fe_extra.go
generated
vendored
Normal file
50
vendor/filippo.io/edwards25519/field/fe_extra.go
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (c) 2021 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package field
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// This file contains additional functionality that is not included in the
|
||||||
|
// upstream crypto/ed25519/edwards25519/field package.
|
||||||
|
|
||||||
|
// SetWideBytes sets v to x, where x is a 64-byte little-endian encoding, which
|
||||||
|
// is reduced modulo the field order. If x is not of the right length,
|
||||||
|
// SetWideBytes returns nil and an error, and the receiver is unchanged.
|
||||||
|
//
|
||||||
|
// SetWideBytes is not necessary to select a uniformly distributed value, and is
|
||||||
|
// only provided for compatibility: SetBytes can be used instead as the chance
|
||||||
|
// of bias is less than 2⁻²⁵⁰.
|
||||||
|
func (v *Element) SetWideBytes(x []byte) (*Element, error) {
|
||||||
|
if len(x) != 64 {
|
||||||
|
return nil, errors.New("edwards25519: invalid SetWideBytes input size")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the 64 bytes into two elements, and extract the most significant
|
||||||
|
// bit of each, which is ignored by SetBytes.
|
||||||
|
lo, _ := new(Element).SetBytes(x[:32])
|
||||||
|
loMSB := uint64(x[31] >> 7)
|
||||||
|
hi, _ := new(Element).SetBytes(x[32:])
|
||||||
|
hiMSB := uint64(x[63] >> 7)
|
||||||
|
|
||||||
|
// The output we want is
|
||||||
|
//
|
||||||
|
// v = lo + loMSB * 2²⁵⁵ + hi * 2²⁵⁶ + hiMSB * 2⁵¹¹
|
||||||
|
//
|
||||||
|
// which applying the reduction identity comes out to
|
||||||
|
//
|
||||||
|
// v = lo + loMSB * 19 + hi * 2 * 19 + hiMSB * 2 * 19²
|
||||||
|
//
|
||||||
|
// l0 will be the sum of a 52 bits value (lo.l0), plus a 5 bits value
|
||||||
|
// (loMSB * 19), a 6 bits value (hi.l0 * 2 * 19), and a 10 bits value
|
||||||
|
// (hiMSB * 2 * 19²), so it fits in a uint64.
|
||||||
|
|
||||||
|
v.l0 = lo.l0 + loMSB*19 + hi.l0*2*19 + hiMSB*2*19*19
|
||||||
|
v.l1 = lo.l1 + hi.l1*2*19
|
||||||
|
v.l2 = lo.l2 + hi.l2*2*19
|
||||||
|
v.l3 = lo.l3 + hi.l3*2*19
|
||||||
|
v.l4 = lo.l4 + hi.l4*2*19
|
||||||
|
|
||||||
|
return v.carryPropagate(), nil
|
||||||
|
}
|
||||||
272
vendor/filippo.io/edwards25519/field/fe_generic.go
generated
vendored
Normal file
272
vendor/filippo.io/edwards25519/field/fe_generic.go
generated
vendored
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
// Copyright (c) 2017 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package field
|
||||||
|
|
||||||
|
import "math/bits"
|
||||||
|
|
||||||
|
// uint128 holds a 128-bit number as two 64-bit limbs, for use with the
|
||||||
|
// bits.Mul64 and bits.Add64 intrinsics.
|
||||||
|
type uint128 struct {
|
||||||
|
lo, hi uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// mul returns a * b.
|
||||||
|
func mul(a, b uint64) uint128 {
|
||||||
|
hi, lo := bits.Mul64(a, b)
|
||||||
|
return uint128{lo, hi}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addMul returns v + a * b.
|
||||||
|
func addMul(v uint128, a, b uint64) uint128 {
|
||||||
|
hi, lo := bits.Mul64(a, b)
|
||||||
|
lo, c := bits.Add64(lo, v.lo, 0)
|
||||||
|
hi, _ = bits.Add64(hi, v.hi, c)
|
||||||
|
return uint128{lo, hi}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mul19 returns v * 19.
|
||||||
|
func mul19(v uint64) uint64 {
|
||||||
|
// Using this approach seems to yield better optimizations than *19.
|
||||||
|
return v + (v+v<<3)<<1
|
||||||
|
}
|
||||||
|
|
||||||
|
// addMul19 returns v + 19 * a * b, where a and b are at most 52 bits.
|
||||||
|
func addMul19(v uint128, a, b uint64) uint128 {
|
||||||
|
hi, lo := bits.Mul64(mul19(a), b)
|
||||||
|
lo, c := bits.Add64(lo, v.lo, 0)
|
||||||
|
hi, _ = bits.Add64(hi, v.hi, c)
|
||||||
|
return uint128{lo, hi}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addMul38 returns v + 38 * a * b, where a and b are at most 52 bits.
|
||||||
|
func addMul38(v uint128, a, b uint64) uint128 {
|
||||||
|
hi, lo := bits.Mul64(mul19(a), b*2)
|
||||||
|
lo, c := bits.Add64(lo, v.lo, 0)
|
||||||
|
hi, _ = bits.Add64(hi, v.hi, c)
|
||||||
|
return uint128{lo, hi}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shiftRightBy51 returns a >> 51. a is assumed to be at most 115 bits.
|
||||||
|
func shiftRightBy51(a uint128) uint64 {
|
||||||
|
return (a.hi << (64 - 51)) | (a.lo >> 51)
|
||||||
|
}
|
||||||
|
|
||||||
|
func feMulGeneric(v, a, b *Element) {
|
||||||
|
a0 := a.l0
|
||||||
|
a1 := a.l1
|
||||||
|
a2 := a.l2
|
||||||
|
a3 := a.l3
|
||||||
|
a4 := a.l4
|
||||||
|
|
||||||
|
b0 := b.l0
|
||||||
|
b1 := b.l1
|
||||||
|
b2 := b.l2
|
||||||
|
b3 := b.l3
|
||||||
|
b4 := b.l4
|
||||||
|
|
||||||
|
// Limb multiplication works like pen-and-paper columnar multiplication, but
|
||||||
|
// with 51-bit limbs instead of digits.
|
||||||
|
//
|
||||||
|
// a4 a3 a2 a1 a0 x
|
||||||
|
// b4 b3 b2 b1 b0 =
|
||||||
|
// ------------------------
|
||||||
|
// a4b0 a3b0 a2b0 a1b0 a0b0 +
|
||||||
|
// a4b1 a3b1 a2b1 a1b1 a0b1 +
|
||||||
|
// a4b2 a3b2 a2b2 a1b2 a0b2 +
|
||||||
|
// a4b3 a3b3 a2b3 a1b3 a0b3 +
|
||||||
|
// a4b4 a3b4 a2b4 a1b4 a0b4 =
|
||||||
|
// ----------------------------------------------
|
||||||
|
// r8 r7 r6 r5 r4 r3 r2 r1 r0
|
||||||
|
//
|
||||||
|
// We can then use the reduction identity (a * 2²⁵⁵ + b = a * 19 + b) to
|
||||||
|
// reduce the limbs that would overflow 255 bits. r5 * 2²⁵⁵ becomes 19 * r5,
|
||||||
|
// r6 * 2³⁰⁶ becomes 19 * r6 * 2⁵¹, etc.
|
||||||
|
//
|
||||||
|
// Reduction can be carried out simultaneously to multiplication. For
|
||||||
|
// example, we do not compute r5: whenever the result of a multiplication
|
||||||
|
// belongs to r5, like a1b4, we multiply it by 19 and add the result to r0.
|
||||||
|
//
|
||||||
|
// a4b0 a3b0 a2b0 a1b0 a0b0 +
|
||||||
|
// a3b1 a2b1 a1b1 a0b1 19×a4b1 +
|
||||||
|
// a2b2 a1b2 a0b2 19×a4b2 19×a3b2 +
|
||||||
|
// a1b3 a0b3 19×a4b3 19×a3b3 19×a2b3 +
|
||||||
|
// a0b4 19×a4b4 19×a3b4 19×a2b4 19×a1b4 =
|
||||||
|
// --------------------------------------
|
||||||
|
// r4 r3 r2 r1 r0
|
||||||
|
//
|
||||||
|
// Finally we add up the columns into wide, overlapping limbs.
|
||||||
|
|
||||||
|
// r0 = a0×b0 + 19×(a1×b4 + a2×b3 + a3×b2 + a4×b1)
|
||||||
|
r0 := mul(a0, b0)
|
||||||
|
r0 = addMul19(r0, a1, b4)
|
||||||
|
r0 = addMul19(r0, a2, b3)
|
||||||
|
r0 = addMul19(r0, a3, b2)
|
||||||
|
r0 = addMul19(r0, a4, b1)
|
||||||
|
|
||||||
|
// r1 = a0×b1 + a1×b0 + 19×(a2×b4 + a3×b3 + a4×b2)
|
||||||
|
r1 := mul(a0, b1)
|
||||||
|
r1 = addMul(r1, a1, b0)
|
||||||
|
r1 = addMul19(r1, a2, b4)
|
||||||
|
r1 = addMul19(r1, a3, b3)
|
||||||
|
r1 = addMul19(r1, a4, b2)
|
||||||
|
|
||||||
|
// r2 = a0×b2 + a1×b1 + a2×b0 + 19×(a3×b4 + a4×b3)
|
||||||
|
r2 := mul(a0, b2)
|
||||||
|
r2 = addMul(r2, a1, b1)
|
||||||
|
r2 = addMul(r2, a2, b0)
|
||||||
|
r2 = addMul19(r2, a3, b4)
|
||||||
|
r2 = addMul19(r2, a4, b3)
|
||||||
|
|
||||||
|
// r3 = a0×b3 + a1×b2 + a2×b1 + a3×b0 + 19×a4×b4
|
||||||
|
r3 := mul(a0, b3)
|
||||||
|
r3 = addMul(r3, a1, b2)
|
||||||
|
r3 = addMul(r3, a2, b1)
|
||||||
|
r3 = addMul(r3, a3, b0)
|
||||||
|
r3 = addMul19(r3, a4, b4)
|
||||||
|
|
||||||
|
// r4 = a0×b4 + a1×b3 + a2×b2 + a3×b1 + a4×b0
|
||||||
|
r4 := mul(a0, b4)
|
||||||
|
r4 = addMul(r4, a1, b3)
|
||||||
|
r4 = addMul(r4, a2, b2)
|
||||||
|
r4 = addMul(r4, a3, b1)
|
||||||
|
r4 = addMul(r4, a4, b0)
|
||||||
|
|
||||||
|
// After the multiplication, we need to reduce (carry) the five coefficients
|
||||||
|
// to obtain a result with limbs that are at most slightly larger than 2⁵¹,
|
||||||
|
// to respect the Element invariant.
|
||||||
|
//
|
||||||
|
// Overall, the reduction works the same as carryPropagate, except with
|
||||||
|
// wider inputs: we take the carry for each coefficient by shifting it right
|
||||||
|
// by 51, and add it to the limb above it. The top carry is multiplied by 19
|
||||||
|
// according to the reduction identity and added to the lowest limb.
|
||||||
|
//
|
||||||
|
// The largest coefficient (r0) will be at most 111 bits, which guarantees
|
||||||
|
// that all carries are at most 111 - 51 = 60 bits, which fits in a uint64.
|
||||||
|
//
|
||||||
|
// r0 = a0×b0 + 19×(a1×b4 + a2×b3 + a3×b2 + a4×b1)
|
||||||
|
// r0 < 2⁵²×2⁵² + 19×(2⁵²×2⁵² + 2⁵²×2⁵² + 2⁵²×2⁵² + 2⁵²×2⁵²)
|
||||||
|
// r0 < (1 + 19 × 4) × 2⁵² × 2⁵²
|
||||||
|
// r0 < 2⁷ × 2⁵² × 2⁵²
|
||||||
|
// r0 < 2¹¹¹
|
||||||
|
//
|
||||||
|
// Moreover, the top coefficient (r4) is at most 107 bits, so c4 is at most
|
||||||
|
// 56 bits, and c4 * 19 is at most 61 bits, which again fits in a uint64 and
|
||||||
|
// allows us to easily apply the reduction identity.
|
||||||
|
//
|
||||||
|
// r4 = a0×b4 + a1×b3 + a2×b2 + a3×b1 + a4×b0
|
||||||
|
// r4 < 5 × 2⁵² × 2⁵²
|
||||||
|
// r4 < 2¹⁰⁷
|
||||||
|
//
|
||||||
|
|
||||||
|
c0 := shiftRightBy51(r0)
|
||||||
|
c1 := shiftRightBy51(r1)
|
||||||
|
c2 := shiftRightBy51(r2)
|
||||||
|
c3 := shiftRightBy51(r3)
|
||||||
|
c4 := shiftRightBy51(r4)
|
||||||
|
|
||||||
|
rr0 := r0.lo&maskLow51Bits + mul19(c4)
|
||||||
|
rr1 := r1.lo&maskLow51Bits + c0
|
||||||
|
rr2 := r2.lo&maskLow51Bits + c1
|
||||||
|
rr3 := r3.lo&maskLow51Bits + c2
|
||||||
|
rr4 := r4.lo&maskLow51Bits + c3
|
||||||
|
|
||||||
|
// Now all coefficients fit into 64-bit registers but are still too large to
|
||||||
|
// be passed around as an Element. We therefore do one last carry chain,
|
||||||
|
// where the carries will be small enough to fit in the wiggle room above 2⁵¹.
|
||||||
|
|
||||||
|
v.l0 = rr0&maskLow51Bits + mul19(rr4>>51)
|
||||||
|
v.l1 = rr1&maskLow51Bits + rr0>>51
|
||||||
|
v.l2 = rr2&maskLow51Bits + rr1>>51
|
||||||
|
v.l3 = rr3&maskLow51Bits + rr2>>51
|
||||||
|
v.l4 = rr4&maskLow51Bits + rr3>>51
|
||||||
|
}
|
||||||
|
|
||||||
|
func feSquareGeneric(v, a *Element) {
|
||||||
|
l0 := a.l0
|
||||||
|
l1 := a.l1
|
||||||
|
l2 := a.l2
|
||||||
|
l3 := a.l3
|
||||||
|
l4 := a.l4
|
||||||
|
|
||||||
|
// Squaring works precisely like multiplication above, but thanks to its
|
||||||
|
// symmetry we get to group a few terms together.
|
||||||
|
//
|
||||||
|
// l4 l3 l2 l1 l0 x
|
||||||
|
// l4 l3 l2 l1 l0 =
|
||||||
|
// ------------------------
|
||||||
|
// l4l0 l3l0 l2l0 l1l0 l0l0 +
|
||||||
|
// l4l1 l3l1 l2l1 l1l1 l0l1 +
|
||||||
|
// l4l2 l3l2 l2l2 l1l2 l0l2 +
|
||||||
|
// l4l3 l3l3 l2l3 l1l3 l0l3 +
|
||||||
|
// l4l4 l3l4 l2l4 l1l4 l0l4 =
|
||||||
|
// ----------------------------------------------
|
||||||
|
// r8 r7 r6 r5 r4 r3 r2 r1 r0
|
||||||
|
//
|
||||||
|
// l4l0 l3l0 l2l0 l1l0 l0l0 +
|
||||||
|
// l3l1 l2l1 l1l1 l0l1 19×l4l1 +
|
||||||
|
// l2l2 l1l2 l0l2 19×l4l2 19×l3l2 +
|
||||||
|
// l1l3 l0l3 19×l4l3 19×l3l3 19×l2l3 +
|
||||||
|
// l0l4 19×l4l4 19×l3l4 19×l2l4 19×l1l4 =
|
||||||
|
// --------------------------------------
|
||||||
|
// r4 r3 r2 r1 r0
|
||||||
|
|
||||||
|
// r0 = l0×l0 + 19×(l1×l4 + l2×l3 + l3×l2 + l4×l1) = l0×l0 + 19×2×(l1×l4 + l2×l3)
|
||||||
|
r0 := mul(l0, l0)
|
||||||
|
r0 = addMul38(r0, l1, l4)
|
||||||
|
r0 = addMul38(r0, l2, l3)
|
||||||
|
|
||||||
|
// r1 = l0×l1 + l1×l0 + 19×(l2×l4 + l3×l3 + l4×l2) = 2×l0×l1 + 19×2×l2×l4 + 19×l3×l3
|
||||||
|
r1 := mul(l0*2, l1)
|
||||||
|
r1 = addMul38(r1, l2, l4)
|
||||||
|
r1 = addMul19(r1, l3, l3)
|
||||||
|
|
||||||
|
// r2 = l0×l2 + l1×l1 + l2×l0 + 19×(l3×l4 + l4×l3) = 2×l0×l2 + l1×l1 + 19×2×l3×l4
|
||||||
|
r2 := mul(l0*2, l2)
|
||||||
|
r2 = addMul(r2, l1, l1)
|
||||||
|
r2 = addMul38(r2, l3, l4)
|
||||||
|
|
||||||
|
// r3 = l0×l3 + l1×l2 + l2×l1 + l3×l0 + 19×l4×l4 = 2×l0×l3 + 2×l1×l2 + 19×l4×l4
|
||||||
|
r3 := mul(l0*2, l3)
|
||||||
|
r3 = addMul(r3, l1*2, l2)
|
||||||
|
r3 = addMul19(r3, l4, l4)
|
||||||
|
|
||||||
|
// r4 = l0×l4 + l1×l3 + l2×l2 + l3×l1 + l4×l0 = 2×l0×l4 + 2×l1×l3 + l2×l2
|
||||||
|
r4 := mul(l0*2, l4)
|
||||||
|
r4 = addMul(r4, l1*2, l3)
|
||||||
|
r4 = addMul(r4, l2, l2)
|
||||||
|
|
||||||
|
c0 := shiftRightBy51(r0)
|
||||||
|
c1 := shiftRightBy51(r1)
|
||||||
|
c2 := shiftRightBy51(r2)
|
||||||
|
c3 := shiftRightBy51(r3)
|
||||||
|
c4 := shiftRightBy51(r4)
|
||||||
|
|
||||||
|
rr0 := r0.lo&maskLow51Bits + mul19(c4)
|
||||||
|
rr1 := r1.lo&maskLow51Bits + c0
|
||||||
|
rr2 := r2.lo&maskLow51Bits + c1
|
||||||
|
rr3 := r3.lo&maskLow51Bits + c2
|
||||||
|
rr4 := r4.lo&maskLow51Bits + c3
|
||||||
|
|
||||||
|
v.l0 = rr0&maskLow51Bits + mul19(rr4>>51)
|
||||||
|
v.l1 = rr1&maskLow51Bits + rr0>>51
|
||||||
|
v.l2 = rr2&maskLow51Bits + rr1>>51
|
||||||
|
v.l3 = rr3&maskLow51Bits + rr2>>51
|
||||||
|
v.l4 = rr4&maskLow51Bits + rr3>>51
|
||||||
|
}
|
||||||
|
|
||||||
|
// carryPropagate brings the limbs below 52 bits by applying the reduction
|
||||||
|
// identity (a * 2²⁵⁵ + b = a * 19 + b) to the l4 carry.
|
||||||
|
func (v *Element) carryPropagate() *Element {
|
||||||
|
// (l4>>51) is at most 64 - 51 = 13 bits, so (l4>>51)*19 is at most 18 bits, and
|
||||||
|
// the final l0 will be at most 52 bits. Similarly for the rest.
|
||||||
|
l0 := v.l0
|
||||||
|
v.l0 = v.l0&maskLow51Bits + mul19(v.l4>>51)
|
||||||
|
v.l4 = v.l4&maskLow51Bits + v.l3>>51
|
||||||
|
v.l3 = v.l3&maskLow51Bits + v.l2>>51
|
||||||
|
v.l2 = v.l2&maskLow51Bits + v.l1>>51
|
||||||
|
v.l1 = v.l1&maskLow51Bits + l0>>51
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
53
vendor/filippo.io/edwards25519/pull.sh
generated
vendored
Normal file
53
vendor/filippo.io/edwards25519/pull.sh
generated
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
echo "Usage: $0 <tag>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAG="$1"
|
||||||
|
TMPDIR="$(mktemp -d)"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$TMPDIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
command -v git >/dev/null
|
||||||
|
command -v git-filter-repo >/dev/null
|
||||||
|
|
||||||
|
if [ -d "$HOME/go/.git" ]; then
|
||||||
|
REFERENCE=(--reference "$HOME/go" --dissociate)
|
||||||
|
else
|
||||||
|
REFERENCE=()
|
||||||
|
fi
|
||||||
|
|
||||||
|
git -c advice.detachedHead=false clone --no-checkout "${REFERENCE[@]}" \
|
||||||
|
-b "$TAG" https://go.googlesource.com/go.git "$TMPDIR"
|
||||||
|
|
||||||
|
# Simplify the history graph by removing the dev.boringcrypto branches, whose
|
||||||
|
# merges end up empty after grafting anyway. This also fixes a weird quirk
|
||||||
|
# (maybe a git-filter-repo bug?) where only one file from an old path,
|
||||||
|
# src/crypto/ed25519/internal/edwards25519/const.go, would still exist in the
|
||||||
|
# filtered repo.
|
||||||
|
git -C "$TMPDIR" replace --graft f771edd7f9 99f1bf54eb
|
||||||
|
git -C "$TMPDIR" replace --graft 109c13b64f c2f96e686f
|
||||||
|
git -C "$TMPDIR" replace --graft aa4da4f189 912f075047
|
||||||
|
|
||||||
|
git -C "$TMPDIR" filter-repo --force \
|
||||||
|
--paths-from-file /dev/stdin \
|
||||||
|
--prune-empty always \
|
||||||
|
--prune-degenerate always \
|
||||||
|
--tag-callback 'tag.skip()' <<'EOF'
|
||||||
|
src/crypto/internal/fips140/edwards25519
|
||||||
|
src/crypto/internal/edwards25519
|
||||||
|
src/crypto/ed25519/internal/edwards25519
|
||||||
|
EOF
|
||||||
|
|
||||||
|
git fetch "$TMPDIR"
|
||||||
|
git update-ref "refs/heads/upstream/$TAG" FETCH_HEAD
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Fetched upstream history up to $TAG. Merge with:"
|
||||||
|
echo -e "\tgit merge --no-ff --no-commit --allow-unrelated-histories upstream/$TAG"
|
||||||
352
vendor/filippo.io/edwards25519/scalar.go
generated
vendored
Normal file
352
vendor/filippo.io/edwards25519/scalar.go
generated
vendored
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
// Copyright (c) 2016 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package edwards25519
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math/bits"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Scalar is an integer modulo
|
||||||
|
//
|
||||||
|
// l = 2^252 + 27742317777372353535851937790883648493
|
||||||
|
//
|
||||||
|
// which is the prime order of the edwards25519 group.
|
||||||
|
//
|
||||||
|
// This type works similarly to math/big.Int, and all arguments and
|
||||||
|
// receivers are allowed to alias.
|
||||||
|
//
|
||||||
|
// The zero value is a valid zero element.
|
||||||
|
type Scalar struct {
|
||||||
|
// s is the scalar in the Montgomery domain, in the format of the
|
||||||
|
// fiat-crypto implementation.
|
||||||
|
s fiatScalarMontgomeryDomainFieldElement
|
||||||
|
}
|
||||||
|
|
||||||
|
// The field implementation in scalar_fiat.go is generated by the fiat-crypto
|
||||||
|
// project (https://github.com/mit-plv/fiat-crypto) at version v0.0.9 (23d2dbc)
|
||||||
|
// from a formally verified model.
|
||||||
|
//
|
||||||
|
// fiat-crypto code comes under the following license.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2015-2020 The fiat-crypto Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// 1. Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY the fiat-crypto authors "AS IS"
|
||||||
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Berkeley Software Design,
|
||||||
|
// Inc. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
//
|
||||||
|
|
||||||
|
// NewScalar returns a new zero Scalar.
|
||||||
|
func NewScalar() *Scalar {
|
||||||
|
return &Scalar{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiplyAdd sets s = x * y + z mod l, and returns s. It is equivalent to
|
||||||
|
// using Multiply and then Add.
|
||||||
|
func (s *Scalar) MultiplyAdd(x, y, z *Scalar) *Scalar {
|
||||||
|
// Make a copy of z in case it aliases s.
|
||||||
|
zCopy := new(Scalar).Set(z)
|
||||||
|
return s.Multiply(x, y).Add(s, zCopy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sets s = x + y mod l, and returns s.
|
||||||
|
func (s *Scalar) Add(x, y *Scalar) *Scalar {
|
||||||
|
// s = 1 * x + y mod l
|
||||||
|
fiatScalarAdd(&s.s, &x.s, &y.s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract sets s = x - y mod l, and returns s.
|
||||||
|
func (s *Scalar) Subtract(x, y *Scalar) *Scalar {
|
||||||
|
// s = -1 * y + x mod l
|
||||||
|
fiatScalarSub(&s.s, &x.s, &y.s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negate sets s = -x mod l, and returns s.
|
||||||
|
func (s *Scalar) Negate(x *Scalar) *Scalar {
|
||||||
|
// s = -1 * x + 0 mod l
|
||||||
|
fiatScalarOpp(&s.s, &x.s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply sets s = x * y mod l, and returns s.
|
||||||
|
func (s *Scalar) Multiply(x, y *Scalar) *Scalar {
|
||||||
|
// s = x * y + 0 mod l
|
||||||
|
fiatScalarMul(&s.s, &x.s, &y.s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets s = x, and returns s.
|
||||||
|
func (s *Scalar) Set(x *Scalar) *Scalar {
|
||||||
|
*s = *x
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUniformBytes sets s = x mod l, where x is a 64-byte little-endian integer.
|
||||||
|
// If x is not of the right length, SetUniformBytes returns nil and an error,
|
||||||
|
// and the receiver is unchanged.
|
||||||
|
//
|
||||||
|
// SetUniformBytes can be used to set s to a uniformly distributed value given
|
||||||
|
// 64 uniformly distributed random bytes.
|
||||||
|
func (s *Scalar) SetUniformBytes(x []byte) (*Scalar, error) {
|
||||||
|
if len(x) != 64 {
|
||||||
|
return nil, errors.New("edwards25519: invalid SetUniformBytes input length")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a value x of 512 bits, but our fiatScalarFromBytes function
|
||||||
|
// expects an input lower than l, which is a little over 252 bits.
|
||||||
|
//
|
||||||
|
// Instead of writing a reduction function that operates on wider inputs, we
|
||||||
|
// can interpret x as the sum of three shorter values a, b, and c.
|
||||||
|
//
|
||||||
|
// x = a + b * 2^168 + c * 2^336 mod l
|
||||||
|
//
|
||||||
|
// We then precompute 2^168 and 2^336 modulo l, and perform the reduction
|
||||||
|
// with two multiplications and two additions.
|
||||||
|
|
||||||
|
s.setShortBytes(x[:21])
|
||||||
|
t := new(Scalar).setShortBytes(x[21:42])
|
||||||
|
s.Add(s, t.Multiply(t, scalarTwo168))
|
||||||
|
t.setShortBytes(x[42:])
|
||||||
|
s.Add(s, t.Multiply(t, scalarTwo336))
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scalarTwo168 and scalarTwo336 are 2^168 and 2^336 modulo l, encoded as a
|
||||||
|
// fiatScalarMontgomeryDomainFieldElement, which is a little-endian 4-limb value
|
||||||
|
// in the 2^256 Montgomery domain.
|
||||||
|
var scalarTwo168 = &Scalar{s: [4]uint64{0x5b8ab432eac74798, 0x38afddd6de59d5d7,
|
||||||
|
0xa2c131b399411b7c, 0x6329a7ed9ce5a30}}
|
||||||
|
var scalarTwo336 = &Scalar{s: [4]uint64{0xbd3d108e2b35ecc5, 0x5c3a3718bdf9c90b,
|
||||||
|
0x63aa97a331b4f2ee, 0x3d217f5be65cb5c}}
|
||||||
|
|
||||||
|
// setShortBytes sets s = x mod l, where x is a little-endian integer shorter
|
||||||
|
// than 32 bytes.
|
||||||
|
func (s *Scalar) setShortBytes(x []byte) *Scalar {
|
||||||
|
if len(x) >= 32 {
|
||||||
|
panic("edwards25519: internal error: setShortBytes called with a long string")
|
||||||
|
}
|
||||||
|
var buf [32]byte
|
||||||
|
copy(buf[:], x)
|
||||||
|
fiatScalarFromBytes((*[4]uint64)(&s.s), &buf)
|
||||||
|
fiatScalarToMontgomery(&s.s, (*fiatScalarNonMontgomeryDomainFieldElement)(&s.s))
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCanonicalBytes sets s = x, where x is a 32-byte little-endian encoding of
|
||||||
|
// s, and returns s. If x is not a canonical encoding of s, SetCanonicalBytes
|
||||||
|
// returns nil and an error, and the receiver is unchanged.
|
||||||
|
func (s *Scalar) SetCanonicalBytes(x []byte) (*Scalar, error) {
|
||||||
|
if len(x) != 32 {
|
||||||
|
return nil, errors.New("invalid scalar length")
|
||||||
|
}
|
||||||
|
if !isReduced(x) {
|
||||||
|
return nil, errors.New("invalid scalar encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
fiatScalarFromBytes((*[4]uint64)(&s.s), (*[32]byte)(x))
|
||||||
|
fiatScalarToMontgomery(&s.s, (*fiatScalarNonMontgomeryDomainFieldElement)(&s.s))
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scalarMinusOneBytes is l - 1 in little endian.
|
||||||
|
var scalarMinusOneBytes = [32]byte{236, 211, 245, 92, 26, 99, 18, 88, 214, 156, 247, 162, 222, 249, 222, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16}
|
||||||
|
|
||||||
|
// isReduced returns whether the given scalar in 32-byte little endian encoded
|
||||||
|
// form is reduced modulo l.
|
||||||
|
func isReduced(s []byte) bool {
|
||||||
|
if len(s) != 32 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
s0 := binary.LittleEndian.Uint64(s[:8])
|
||||||
|
s1 := binary.LittleEndian.Uint64(s[8:16])
|
||||||
|
s2 := binary.LittleEndian.Uint64(s[16:24])
|
||||||
|
s3 := binary.LittleEndian.Uint64(s[24:])
|
||||||
|
|
||||||
|
l0 := binary.LittleEndian.Uint64(scalarMinusOneBytes[:8])
|
||||||
|
l1 := binary.LittleEndian.Uint64(scalarMinusOneBytes[8:16])
|
||||||
|
l2 := binary.LittleEndian.Uint64(scalarMinusOneBytes[16:24])
|
||||||
|
l3 := binary.LittleEndian.Uint64(scalarMinusOneBytes[24:])
|
||||||
|
|
||||||
|
// Do a constant time subtraction chain scalarMinusOneBytes - s. If there is
|
||||||
|
// a borrow at the end, then s > scalarMinusOneBytes.
|
||||||
|
_, b := bits.Sub64(l0, s0, 0)
|
||||||
|
_, b = bits.Sub64(l1, s1, b)
|
||||||
|
_, b = bits.Sub64(l2, s2, b)
|
||||||
|
_, b = bits.Sub64(l3, s3, b)
|
||||||
|
return b == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBytesWithClamping applies the buffer pruning described in RFC 8032,
|
||||||
|
// Section 5.1.5 (also known as clamping) and sets s to the result. The input
|
||||||
|
// must be 32 bytes, and it is not modified. If x is not of the right length,
|
||||||
|
// SetBytesWithClamping returns nil and an error, and the receiver is unchanged.
|
||||||
|
//
|
||||||
|
// Note that since Scalar values are always reduced modulo the prime order of
|
||||||
|
// the curve, the resulting value will not preserve any of the cofactor-clearing
|
||||||
|
// properties that clamping is meant to provide. It will however work as
|
||||||
|
// expected as long as it is applied to points on the prime order subgroup, like
|
||||||
|
// in Ed25519. In fact, it is lost to history why RFC 8032 adopted the
|
||||||
|
// irrelevant RFC 7748 clamping, but it is now required for compatibility.
|
||||||
|
func (s *Scalar) SetBytesWithClamping(x []byte) (*Scalar, error) {
|
||||||
|
// The description above omits the purpose of the high bits of the clamping
|
||||||
|
// for brevity, but those are also lost to reductions, and are also
|
||||||
|
// irrelevant to edwards25519 as they protect against a specific
|
||||||
|
// implementation bug that was once observed in a generic Montgomery ladder.
|
||||||
|
if len(x) != 32 {
|
||||||
|
return nil, errors.New("edwards25519: invalid SetBytesWithClamping input length")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to use the wide reduction from SetUniformBytes, since clamping
|
||||||
|
// sets the 2^254 bit, making the value higher than the order.
|
||||||
|
var wideBytes [64]byte
|
||||||
|
copy(wideBytes[:], x[:])
|
||||||
|
wideBytes[0] &= 248
|
||||||
|
wideBytes[31] &= 63
|
||||||
|
wideBytes[31] |= 64
|
||||||
|
return s.SetUniformBytes(wideBytes[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the canonical 32-byte little-endian encoding of s.
|
||||||
|
func (s *Scalar) Bytes() []byte {
|
||||||
|
// This function is outlined to make the allocations inline in the caller
|
||||||
|
// rather than happen on the heap.
|
||||||
|
var encoded [32]byte
|
||||||
|
return s.bytes(&encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scalar) bytes(out *[32]byte) []byte {
|
||||||
|
var ss fiatScalarNonMontgomeryDomainFieldElement
|
||||||
|
fiatScalarFromMontgomery(&ss, &s.s)
|
||||||
|
fiatScalarToBytes(out, (*[4]uint64)(&ss))
|
||||||
|
return out[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns 1 if s and t are equal, and 0 otherwise.
|
||||||
|
func (s *Scalar) Equal(t *Scalar) int {
|
||||||
|
var diff fiatScalarMontgomeryDomainFieldElement
|
||||||
|
fiatScalarSub(&diff, &s.s, &t.s)
|
||||||
|
var nonzero uint64
|
||||||
|
fiatScalarNonzero(&nonzero, (*[4]uint64)(&diff))
|
||||||
|
nonzero |= nonzero >> 32
|
||||||
|
nonzero |= nonzero >> 16
|
||||||
|
nonzero |= nonzero >> 8
|
||||||
|
nonzero |= nonzero >> 4
|
||||||
|
nonzero |= nonzero >> 2
|
||||||
|
nonzero |= nonzero >> 1
|
||||||
|
return int(^nonzero) & 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// nonAdjacentForm computes a width-w non-adjacent form for this scalar.
|
||||||
|
//
|
||||||
|
// w must be between 2 and 8, or nonAdjacentForm will panic.
|
||||||
|
func (s *Scalar) nonAdjacentForm(w uint) [256]int8 {
|
||||||
|
// This implementation is adapted from the one
|
||||||
|
// in curve25519-dalek and is documented there:
|
||||||
|
// https://github.com/dalek-cryptography/curve25519-dalek/blob/f630041af28e9a405255f98a8a93adca18e4315b/src/scalar.rs#L800-L871
|
||||||
|
b := s.Bytes()
|
||||||
|
if b[31] > 127 {
|
||||||
|
panic("scalar has high bit set illegally")
|
||||||
|
}
|
||||||
|
if w < 2 {
|
||||||
|
panic("w must be at least 2 by the definition of NAF")
|
||||||
|
} else if w > 8 {
|
||||||
|
panic("NAF digits must fit in int8")
|
||||||
|
}
|
||||||
|
|
||||||
|
var naf [256]int8
|
||||||
|
var digits [5]uint64
|
||||||
|
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
digits[i] = binary.LittleEndian.Uint64(b[i*8:])
|
||||||
|
}
|
||||||
|
|
||||||
|
width := uint64(1 << w)
|
||||||
|
windowMask := uint64(width - 1)
|
||||||
|
|
||||||
|
pos := uint(0)
|
||||||
|
carry := uint64(0)
|
||||||
|
for pos < 256 {
|
||||||
|
indexU64 := pos / 64
|
||||||
|
indexBit := pos % 64
|
||||||
|
var bitBuf uint64
|
||||||
|
if indexBit < 64-w {
|
||||||
|
// This window's bits are contained in a single u64
|
||||||
|
bitBuf = digits[indexU64] >> indexBit
|
||||||
|
} else {
|
||||||
|
// Combine the current 64 bits with bits from the next 64
|
||||||
|
bitBuf = (digits[indexU64] >> indexBit) | (digits[1+indexU64] << (64 - indexBit))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add carry into the current window
|
||||||
|
window := carry + (bitBuf & windowMask)
|
||||||
|
|
||||||
|
if window&1 == 0 {
|
||||||
|
// If the window value is even, preserve the carry and continue.
|
||||||
|
// Why is the carry preserved?
|
||||||
|
// If carry == 0 and window & 1 == 0,
|
||||||
|
// then the next carry should be 0
|
||||||
|
// If carry == 1 and window & 1 == 0,
|
||||||
|
// then bit_buf & 1 == 1 so the next carry should be 1
|
||||||
|
pos += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if window < width/2 {
|
||||||
|
carry = 0
|
||||||
|
naf[pos] = int8(window)
|
||||||
|
} else {
|
||||||
|
carry = 1
|
||||||
|
naf[pos] = int8(window) - int8(width)
|
||||||
|
}
|
||||||
|
|
||||||
|
pos += w
|
||||||
|
}
|
||||||
|
return naf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scalar) signedRadix16() [64]int8 {
|
||||||
|
b := s.Bytes()
|
||||||
|
if b[31] > 127 {
|
||||||
|
panic("scalar has high bit set illegally")
|
||||||
|
}
|
||||||
|
|
||||||
|
var digits [64]int8
|
||||||
|
|
||||||
|
// Compute unsigned radix-16 digits:
|
||||||
|
for i := 0; i < 32; i++ {
|
||||||
|
digits[2*i] = int8(b[i] & 15)
|
||||||
|
digits[2*i+1] = int8((b[i] >> 4) & 15)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recenter coefficients:
|
||||||
|
for i := 0; i < 63; i++ {
|
||||||
|
carry := (digits[i] + 8) >> 4
|
||||||
|
digits[i] -= carry << 4
|
||||||
|
digits[i+1] += carry
|
||||||
|
}
|
||||||
|
|
||||||
|
return digits
|
||||||
|
}
|
||||||
1147
vendor/filippo.io/edwards25519/scalar_fiat.go
generated
vendored
Normal file
1147
vendor/filippo.io/edwards25519/scalar_fiat.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
214
vendor/filippo.io/edwards25519/scalarmult.go
generated
vendored
Normal file
214
vendor/filippo.io/edwards25519/scalarmult.go
generated
vendored
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
// Copyright (c) 2019 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package edwards25519
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// basepointTable is a set of 32 affineLookupTables, where table i is generated
|
||||||
|
// from 256i * basepoint. It is precomputed the first time it's used.
|
||||||
|
func basepointTable() *[32]affineLookupTable {
|
||||||
|
basepointTablePrecomp.initOnce.Do(func() {
|
||||||
|
p := NewGeneratorPoint()
|
||||||
|
for i := 0; i < 32; i++ {
|
||||||
|
basepointTablePrecomp.table[i].FromP3(p)
|
||||||
|
for j := 0; j < 8; j++ {
|
||||||
|
p.Add(p, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return &basepointTablePrecomp.table
|
||||||
|
}
|
||||||
|
|
||||||
|
var basepointTablePrecomp struct {
|
||||||
|
table [32]affineLookupTable
|
||||||
|
initOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScalarBaseMult sets v = x * B, where B is the canonical generator, and
|
||||||
|
// returns v.
|
||||||
|
//
|
||||||
|
// The scalar multiplication is done in constant time.
|
||||||
|
func (v *Point) ScalarBaseMult(x *Scalar) *Point {
|
||||||
|
basepointTable := basepointTable()
|
||||||
|
|
||||||
|
// Write x = sum(x_i * 16^i) so x*B = sum( B*x_i*16^i )
|
||||||
|
// as described in the Ed25519 paper
|
||||||
|
//
|
||||||
|
// Group even and odd coefficients
|
||||||
|
// x*B = x_0*16^0*B + x_2*16^2*B + ... + x_62*16^62*B
|
||||||
|
// + x_1*16^1*B + x_3*16^3*B + ... + x_63*16^63*B
|
||||||
|
// x*B = x_0*16^0*B + x_2*16^2*B + ... + x_62*16^62*B
|
||||||
|
// + 16*( x_1*16^0*B + x_3*16^2*B + ... + x_63*16^62*B)
|
||||||
|
//
|
||||||
|
// We use a lookup table for each i to get x_i*16^(2*i)*B
|
||||||
|
// and do four doublings to multiply by 16.
|
||||||
|
digits := x.signedRadix16()
|
||||||
|
|
||||||
|
multiple := &affineCached{}
|
||||||
|
tmp1 := &projP1xP1{}
|
||||||
|
tmp2 := &projP2{}
|
||||||
|
|
||||||
|
// Accumulate the odd components first
|
||||||
|
v.Set(NewIdentityPoint())
|
||||||
|
for i := 1; i < 64; i += 2 {
|
||||||
|
basepointTable[i/2].SelectInto(multiple, digits[i])
|
||||||
|
tmp1.AddAffine(v, multiple)
|
||||||
|
v.fromP1xP1(tmp1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply by 16
|
||||||
|
tmp2.FromP3(v) // tmp2 = v in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 2*v in P1xP1 coords
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = 2*v in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 4*v in P1xP1 coords
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = 4*v in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 8*v in P1xP1 coords
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = 8*v in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 16*v in P1xP1 coords
|
||||||
|
v.fromP1xP1(tmp1) // now v = 16*(odd components)
|
||||||
|
|
||||||
|
// Accumulate the even components
|
||||||
|
for i := 0; i < 64; i += 2 {
|
||||||
|
basepointTable[i/2].SelectInto(multiple, digits[i])
|
||||||
|
tmp1.AddAffine(v, multiple)
|
||||||
|
v.fromP1xP1(tmp1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScalarMult sets v = x * q, and returns v.
|
||||||
|
//
|
||||||
|
// The scalar multiplication is done in constant time.
|
||||||
|
func (v *Point) ScalarMult(x *Scalar, q *Point) *Point {
|
||||||
|
checkInitialized(q)
|
||||||
|
|
||||||
|
var table projLookupTable
|
||||||
|
table.FromP3(q)
|
||||||
|
|
||||||
|
// Write x = sum(x_i * 16^i)
|
||||||
|
// so x*Q = sum( Q*x_i*16^i )
|
||||||
|
// = Q*x_0 + 16*(Q*x_1 + 16*( ... + Q*x_63) ... )
|
||||||
|
// <------compute inside out---------
|
||||||
|
//
|
||||||
|
// We use the lookup table to get the x_i*Q values
|
||||||
|
// and do four doublings to compute 16*Q
|
||||||
|
digits := x.signedRadix16()
|
||||||
|
|
||||||
|
// Unwrap first loop iteration to save computing 16*identity
|
||||||
|
multiple := &projCached{}
|
||||||
|
tmp1 := &projP1xP1{}
|
||||||
|
tmp2 := &projP2{}
|
||||||
|
table.SelectInto(multiple, digits[63])
|
||||||
|
|
||||||
|
v.Set(NewIdentityPoint())
|
||||||
|
tmp1.Add(v, multiple) // tmp1 = x_63*Q in P1xP1 coords
|
||||||
|
for i := 62; i >= 0; i-- {
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = (prev) in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 2*(prev) in P1xP1 coords
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = 2*(prev) in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 4*(prev) in P1xP1 coords
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = 4*(prev) in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 8*(prev) in P1xP1 coords
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = 8*(prev) in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 16*(prev) in P1xP1 coords
|
||||||
|
v.fromP1xP1(tmp1) // v = 16*(prev) in P3 coords
|
||||||
|
table.SelectInto(multiple, digits[i])
|
||||||
|
tmp1.Add(v, multiple) // tmp1 = x_i*Q + 16*(prev) in P1xP1 coords
|
||||||
|
}
|
||||||
|
v.fromP1xP1(tmp1)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// basepointNafTable is the nafLookupTable8 for the basepoint.
|
||||||
|
// It is precomputed the first time it's used.
|
||||||
|
func basepointNafTable() *nafLookupTable8 {
|
||||||
|
basepointNafTablePrecomp.initOnce.Do(func() {
|
||||||
|
basepointNafTablePrecomp.table.FromP3(NewGeneratorPoint())
|
||||||
|
})
|
||||||
|
return &basepointNafTablePrecomp.table
|
||||||
|
}
|
||||||
|
|
||||||
|
var basepointNafTablePrecomp struct {
|
||||||
|
table nafLookupTable8
|
||||||
|
initOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// VarTimeDoubleScalarBaseMult sets v = a * A + b * B, where B is the canonical
|
||||||
|
// generator, and returns v.
|
||||||
|
//
|
||||||
|
// Execution time depends on the inputs.
|
||||||
|
func (v *Point) VarTimeDoubleScalarBaseMult(a *Scalar, A *Point, b *Scalar) *Point {
|
||||||
|
checkInitialized(A)
|
||||||
|
|
||||||
|
// Similarly to the single variable-base approach, we compute
|
||||||
|
// digits and use them with a lookup table. However, because
|
||||||
|
// we are allowed to do variable-time operations, we don't
|
||||||
|
// need constant-time lookups or constant-time digit
|
||||||
|
// computations.
|
||||||
|
//
|
||||||
|
// So we use a non-adjacent form of some width w instead of
|
||||||
|
// radix 16. This is like a binary representation (one digit
|
||||||
|
// for each binary place) but we allow the digits to grow in
|
||||||
|
// magnitude up to 2^{w-1} so that the nonzero digits are as
|
||||||
|
// sparse as possible. Intuitively, this "condenses" the
|
||||||
|
// "mass" of the scalar onto sparse coefficients (meaning
|
||||||
|
// fewer additions).
|
||||||
|
|
||||||
|
basepointNafTable := basepointNafTable()
|
||||||
|
var aTable nafLookupTable5
|
||||||
|
aTable.FromP3(A)
|
||||||
|
// Because the basepoint is fixed, we can use a wider NAF
|
||||||
|
// corresponding to a bigger table.
|
||||||
|
aNaf := a.nonAdjacentForm(5)
|
||||||
|
bNaf := b.nonAdjacentForm(8)
|
||||||
|
|
||||||
|
// Find the first nonzero coefficient.
|
||||||
|
i := 255
|
||||||
|
for j := i; j >= 0; j-- {
|
||||||
|
if aNaf[j] != 0 || bNaf[j] != 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
multA := &projCached{}
|
||||||
|
multB := &affineCached{}
|
||||||
|
tmp1 := &projP1xP1{}
|
||||||
|
tmp2 := &projP2{}
|
||||||
|
tmp2.Zero()
|
||||||
|
|
||||||
|
// Move from high to low bits, doubling the accumulator
|
||||||
|
// at each iteration and checking whether there is a nonzero
|
||||||
|
// coefficient to look up a multiple of.
|
||||||
|
for ; i >= 0; i-- {
|
||||||
|
tmp1.Double(tmp2)
|
||||||
|
|
||||||
|
// Only update v if we have a nonzero coeff to add in.
|
||||||
|
if aNaf[i] > 0 {
|
||||||
|
v.fromP1xP1(tmp1)
|
||||||
|
aTable.SelectInto(multA, aNaf[i])
|
||||||
|
tmp1.Add(v, multA)
|
||||||
|
} else if aNaf[i] < 0 {
|
||||||
|
v.fromP1xP1(tmp1)
|
||||||
|
aTable.SelectInto(multA, -aNaf[i])
|
||||||
|
tmp1.Sub(v, multA)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bNaf[i] > 0 {
|
||||||
|
v.fromP1xP1(tmp1)
|
||||||
|
basepointNafTable.SelectInto(multB, bNaf[i])
|
||||||
|
tmp1.AddAffine(v, multB)
|
||||||
|
} else if bNaf[i] < 0 {
|
||||||
|
v.fromP1xP1(tmp1)
|
||||||
|
basepointNafTable.SelectInto(multB, -bNaf[i])
|
||||||
|
tmp1.SubAffine(v, multB)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp2.FromP1xP1(tmp1)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.fromP2(tmp2)
|
||||||
|
return v
|
||||||
|
}
|
||||||
127
vendor/filippo.io/edwards25519/tables.go
generated
vendored
Normal file
127
vendor/filippo.io/edwards25519/tables.go
generated
vendored
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// Copyright (c) 2019 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package edwards25519
|
||||||
|
|
||||||
|
import "crypto/subtle"
|
||||||
|
|
||||||
|
// A dynamic lookup table for variable-base, constant-time scalar muls.
|
||||||
|
type projLookupTable struct {
|
||||||
|
points [8]projCached
|
||||||
|
}
|
||||||
|
|
||||||
|
// A precomputed lookup table for fixed-base, constant-time scalar muls.
|
||||||
|
type affineLookupTable struct {
|
||||||
|
points [8]affineCached
|
||||||
|
}
|
||||||
|
|
||||||
|
// A dynamic lookup table for variable-base, variable-time scalar muls.
|
||||||
|
type nafLookupTable5 struct {
|
||||||
|
points [8]projCached
|
||||||
|
}
|
||||||
|
|
||||||
|
// A precomputed lookup table for fixed-base, variable-time scalar muls.
|
||||||
|
type nafLookupTable8 struct {
|
||||||
|
points [64]affineCached
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructors.
|
||||||
|
|
||||||
|
// Builds a lookup table at runtime. Fast.
|
||||||
|
func (v *projLookupTable) FromP3(q *Point) {
|
||||||
|
// Goal: v.points[i] = (i+1)*Q, i.e., Q, 2Q, ..., 8Q
|
||||||
|
// This allows lookup of -8Q, ..., -Q, 0, Q, ..., 8Q
|
||||||
|
v.points[0].FromP3(q)
|
||||||
|
tmpP3 := Point{}
|
||||||
|
tmpP1xP1 := projP1xP1{}
|
||||||
|
for i := 0; i < 7; i++ {
|
||||||
|
// Compute (i+1)*Q as Q + i*Q and convert to a projCached
|
||||||
|
// This is needlessly complicated because the API has explicit
|
||||||
|
// receivers instead of creating stack objects and relying on RVO
|
||||||
|
v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.Add(q, &v.points[i])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is not optimised for speed; fixed-base tables should be precomputed.
|
||||||
|
func (v *affineLookupTable) FromP3(q *Point) {
|
||||||
|
// Goal: v.points[i] = (i+1)*Q, i.e., Q, 2Q, ..., 8Q
|
||||||
|
// This allows lookup of -8Q, ..., -Q, 0, Q, ..., 8Q
|
||||||
|
v.points[0].FromP3(q)
|
||||||
|
tmpP3 := Point{}
|
||||||
|
tmpP1xP1 := projP1xP1{}
|
||||||
|
for i := 0; i < 7; i++ {
|
||||||
|
// Compute (i+1)*Q as Q + i*Q and convert to affineCached
|
||||||
|
v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.AddAffine(q, &v.points[i])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds a lookup table at runtime. Fast.
|
||||||
|
func (v *nafLookupTable5) FromP3(q *Point) {
|
||||||
|
// Goal: v.points[i] = (2*i+1)*Q, i.e., Q, 3Q, 5Q, ..., 15Q
|
||||||
|
// This allows lookup of -15Q, ..., -3Q, -Q, 0, Q, 3Q, ..., 15Q
|
||||||
|
v.points[0].FromP3(q)
|
||||||
|
q2 := Point{}
|
||||||
|
q2.Add(q, q)
|
||||||
|
tmpP3 := Point{}
|
||||||
|
tmpP1xP1 := projP1xP1{}
|
||||||
|
for i := 0; i < 7; i++ {
|
||||||
|
v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.Add(&q2, &v.points[i])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is not optimised for speed; fixed-base tables should be precomputed.
|
||||||
|
func (v *nafLookupTable8) FromP3(q *Point) {
|
||||||
|
v.points[0].FromP3(q)
|
||||||
|
q2 := Point{}
|
||||||
|
q2.Add(q, q)
|
||||||
|
tmpP3 := Point{}
|
||||||
|
tmpP1xP1 := projP1xP1{}
|
||||||
|
for i := 0; i < 63; i++ {
|
||||||
|
v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.AddAffine(&q2, &v.points[i])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selectors.
|
||||||
|
|
||||||
|
// Set dest to x*Q, where -8 <= x <= 8, in constant time.
|
||||||
|
func (v *projLookupTable) SelectInto(dest *projCached, x int8) {
|
||||||
|
// Compute xabs = |x|
|
||||||
|
xmask := x >> 7
|
||||||
|
xabs := uint8((x + xmask) ^ xmask)
|
||||||
|
|
||||||
|
dest.Zero()
|
||||||
|
for j := 1; j <= 8; j++ {
|
||||||
|
// Set dest = j*Q if |x| = j
|
||||||
|
cond := subtle.ConstantTimeByteEq(xabs, uint8(j))
|
||||||
|
dest.Select(&v.points[j-1], dest, cond)
|
||||||
|
}
|
||||||
|
// Now dest = |x|*Q, conditionally negate to get x*Q
|
||||||
|
dest.CondNeg(int(xmask & 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set dest to x*Q, where -8 <= x <= 8, in constant time.
|
||||||
|
func (v *affineLookupTable) SelectInto(dest *affineCached, x int8) {
|
||||||
|
// Compute xabs = |x|
|
||||||
|
xmask := x >> 7
|
||||||
|
xabs := uint8((x + xmask) ^ xmask)
|
||||||
|
|
||||||
|
dest.Zero()
|
||||||
|
for j := 1; j <= 8; j++ {
|
||||||
|
// Set dest = j*Q if |x| = j
|
||||||
|
cond := subtle.ConstantTimeByteEq(xabs, uint8(j))
|
||||||
|
dest.Select(&v.points[j-1], dest, cond)
|
||||||
|
}
|
||||||
|
// Now dest = |x|*Q, conditionally negate to get x*Q
|
||||||
|
dest.CondNeg(int(xmask & 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given odd x with 0 < x < 2^4, return x*Q (in variable time).
|
||||||
|
func (v *nafLookupTable5) SelectInto(dest *projCached, x int8) {
|
||||||
|
*dest = v.points[x/2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given odd x with 0 < x < 2^7, return x*Q (in variable time).
|
||||||
|
func (v *nafLookupTable8) SelectInto(dest *affineCached, x int8) {
|
||||||
|
*dest = v.points[x/2]
|
||||||
|
}
|
||||||
1
vendor/github.com/PuerkitoBio/goquery/.gitattributes
generated
vendored
Normal file
1
vendor/github.com/PuerkitoBio/goquery/.gitattributes
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
testdata/* linguist-vendored
|
||||||
16
vendor/github.com/PuerkitoBio/goquery/.gitignore
generated
vendored
Normal file
16
vendor/github.com/PuerkitoBio/goquery/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# editor temporary files
|
||||||
|
*.sublime-*
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
#*.*#
|
||||||
|
tags
|
||||||
|
|
||||||
|
# direnv config
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# test binaries
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# coverage and profilte outputs
|
||||||
|
*.out
|
||||||
|
|
||||||
12
vendor/github.com/PuerkitoBio/goquery/LICENSE
generated
vendored
Normal file
12
vendor/github.com/PuerkitoBio/goquery/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Copyright (c) 2012-2021, Martin Angers & Contributors
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
218
vendor/github.com/PuerkitoBio/goquery/README.md
generated
vendored
Normal file
218
vendor/github.com/PuerkitoBio/goquery/README.md
generated
vendored
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
# goquery - a little like that j-thing, only in Go
|
||||||
|
|
||||||
|
[](https://github.com/PuerkitoBio/goquery/actions)
|
||||||
|
[](https://pkg.go.dev/github.com/PuerkitoBio/goquery)
|
||||||
|
[](https://sourcegraph.com/github.com/PuerkitoBio/goquery?badge)
|
||||||
|
|
||||||
|
goquery brings a syntax and a set of features similar to [jQuery][] to the [Go language][go]. It is based on Go's [net/html package][html] and the CSS Selector library [cascadia][]. Since the net/html parser returns nodes, and not a full-featured DOM tree, jQuery's stateful manipulation functions (like height(), css(), detach()) have been left off.
|
||||||
|
|
||||||
|
Also, because the net/html parser requires UTF-8 encoding, so does goquery: it is the caller's responsibility to ensure that the source document provides UTF-8 encoded HTML. See the [wiki][] for various options to do this.
|
||||||
|
|
||||||
|
Syntax-wise, it is as close as possible to jQuery, with the same function names when possible, and that warm and fuzzy chainable interface. jQuery being the ultra-popular library that it is, I felt that writing a similar HTML-manipulating library was better to follow its API than to start anew (in the same spirit as Go's `fmt` package), even though some of its methods are less than intuitive (looking at you, [index()][index]...).
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
* [Installation](#installation)
|
||||||
|
* [Changelog](#changelog)
|
||||||
|
* [API](#api)
|
||||||
|
* [Examples](#examples)
|
||||||
|
* [Related Projects](#related-projects)
|
||||||
|
* [Support](#support)
|
||||||
|
* [License](#license)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Required Go version:
|
||||||
|
|
||||||
|
* Starting with version `v1.12.0` of goquery, Go 1.25+ is required due to its dependencies.
|
||||||
|
* Starting with version `v1.11.0` of goquery, Go 1.24+ is required due to its dependencies.
|
||||||
|
* Starting with version `v1.10.0` of goquery, Go 1.23+ is required due to the use of function-based iterators.
|
||||||
|
* For `v1.9.0` of goquery, Go 1.18+ is required due to the use of generics.
|
||||||
|
* For previous goquery versions, a Go version of 1.1+ was required because of the `net/html` dependency.
|
||||||
|
|
||||||
|
Ongoing goquery development is tested on the latest 2 versions of Go.
|
||||||
|
|
||||||
|
$ go get github.com/PuerkitoBio/goquery
|
||||||
|
|
||||||
|
(optional) To run unit tests:
|
||||||
|
|
||||||
|
$ cd $GOPATH/src/github.com/PuerkitoBio/goquery
|
||||||
|
$ go test
|
||||||
|
|
||||||
|
(optional) To run benchmarks (warning: it runs for a few minutes):
|
||||||
|
|
||||||
|
$ cd $GOPATH/src/github.com/PuerkitoBio/goquery
|
||||||
|
$ go test -bench=".*"
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
**Note that goquery's API is now stable, and will not break.**
|
||||||
|
|
||||||
|
* **2026-03-15 (v1.12.0)** : Update `go.mod` dependencies, add go1.26 to the test matrix, **goquery now requires Go version 1.25+**.
|
||||||
|
* **2025-11-16 (v1.11.0)** : Update `go.mod` dependencies, add go1.25 to the test matrix, **goquery now requires Go version 1.24+**.
|
||||||
|
* **2025-04-11 (v1.10.3)** : Update `go.mod` dependencies, small optimization (thanks [@myxzlpltk](https://github.com/myxzlpltk)).
|
||||||
|
* **2025-02-13 (v1.10.2)** : Update `go.mod` dependencies, add go1.24 to the test matrix.
|
||||||
|
* **2024-12-26 (v1.10.1)** : Update `go.mod` dependencies.
|
||||||
|
* **2024-09-06 (v1.10.0)** : Add `EachIter` which provides an iterator that can be used in `for..range` loops on the `*Selection` object. **goquery now requires Go version 1.23+** (thanks [@amikai](https://github.com/amikai)).
|
||||||
|
* **2024-09-06 (v1.9.3)** : Update `go.mod` dependencies.
|
||||||
|
* **2024-04-29 (v1.9.2)** : Update `go.mod` dependencies.
|
||||||
|
* **2024-02-29 (v1.9.1)** : Improve allocation and performance of the `Map` function and `Selection.Map` method, better document the cascadia differences (thanks [@jwilsson](https://github.com/jwilsson)).
|
||||||
|
* **2024-02-22 (v1.9.0)** : Add a generic `Map` function, **goquery now requires Go version 1.18+** (thanks [@Fesaa](https://github.com/Fesaa)).
|
||||||
|
* **2023-02-18 (v1.8.1)** : Update `go.mod` dependencies, update CI workflow.
|
||||||
|
* **2021-10-25 (v1.8.0)** : Add `Render` function to render a `Selection` to an `io.Writer` (thanks [@anthonygedeon](https://github.com/anthonygedeon)).
|
||||||
|
* **2021-07-11 (v1.7.1)** : Update go.mod dependencies and add dependabot config (thanks [@jauderho](https://github.com/jauderho)).
|
||||||
|
* **2021-06-14 (v1.7.0)** : Add `Single` and `SingleMatcher` functions to optimize first-match selection (thanks [@gdollardollar](https://github.com/gdollardollar)).
|
||||||
|
* **2021-01-11 (v1.6.1)** : Fix panic when calling `{Prepend,Append,Set}Html` on a `Selection` that contains non-Element nodes.
|
||||||
|
* **2020-10-08 (v1.6.0)** : Parse html in context of the container node for all functions that deal with html strings (`AfterHtml`, `AppendHtml`, etc.). Thanks to [@thiemok][thiemok] and [@davidjwilkins][djw] for their work on this.
|
||||||
|
* **2020-02-04 (v1.5.1)** : Update module dependencies.
|
||||||
|
* **2018-11-15 (v1.5.0)** : Go module support (thanks @Zaba505).
|
||||||
|
* **2018-06-07 (v1.4.1)** : Add `NewDocumentFromReader` examples.
|
||||||
|
* **2018-03-24 (v1.4.0)** : Deprecate `NewDocument(url)` and `NewDocumentFromResponse(response)`.
|
||||||
|
* **2018-01-28 (v1.3.0)** : Add `ToEnd` constant to `Slice` until the end of the selection (thanks to @davidjwilkins for raising the issue).
|
||||||
|
* **2018-01-11 (v1.2.0)** : Add `AddBack*` and deprecate `AndSelf` (thanks to @davidjwilkins).
|
||||||
|
* **2017-02-12 (v1.1.0)** : Add `SetHtml` and `SetText` (thanks to @glebtv).
|
||||||
|
* **2016-12-29 (v1.0.2)** : Optimize allocations for `Selection.Text` (thanks to @radovskyb).
|
||||||
|
* **2016-08-28 (v1.0.1)** : Optimize performance for large documents.
|
||||||
|
* **2016-07-27 (v1.0.0)** : Tag version 1.0.0.
|
||||||
|
* **2016-06-15** : Invalid selector strings internally compile to a `Matcher` implementation that never matches any node (instead of a panic). So for example, `doc.Find("~")` returns an empty `*Selection` object.
|
||||||
|
* **2016-02-02** : Add `NodeName` utility function similar to the DOM's `nodeName` property. It returns the tag name of the first element in a selection, and other relevant values of non-element nodes (see [doc][] for details). Add `OuterHtml` utility function similar to the DOM's `outerHTML` property (named `OuterHtml` in small caps for consistency with the existing `Html` method on the `Selection`).
|
||||||
|
* **2015-04-20** : Add `AttrOr` helper method to return the attribute's value or a default value if absent. Thanks to [piotrkowalczuk][piotr].
|
||||||
|
* **2015-02-04** : Add more manipulation functions - Prepend* - thanks again to [Andrew Stone][thatguystone].
|
||||||
|
* **2014-11-28** : Add more manipulation functions - ReplaceWith*, Wrap* and Unwrap - thanks again to [Andrew Stone][thatguystone].
|
||||||
|
* **2014-11-07** : Add manipulation functions (thanks to [Andrew Stone][thatguystone]) and `*Matcher` functions, that receive compiled cascadia selectors instead of selector strings, thus avoiding potential panics thrown by goquery via `cascadia.MustCompile` calls. This results in better performance (selectors can be compiled once and reused) and more idiomatic error handling (you can handle cascadia's compilation errors, instead of recovering from panics, which had been bugging me for a long time). Note that the actual type expected is a `Matcher` interface, that `cascadia.Selector` implements. Other matcher implementations could be used.
|
||||||
|
* **2014-11-06** : Change import paths of net/html to golang.org/x/net/html (see https://groups.google.com/forum/#!topic/golang-nuts/eD8dh3T9yyA). Make sure to update your code to use the new import path too when you call goquery with `html.Node`s.
|
||||||
|
* **v0.3.2** : Add `NewDocumentFromReader()` (thanks jweir) which allows creating a goquery document from an io.Reader.
|
||||||
|
* **v0.3.1** : Add `NewDocumentFromResponse()` (thanks assassingj) which allows creating a goquery document from an http response.
|
||||||
|
* **v0.3.0** : Add `EachWithBreak()` which allows to break out of an `Each()` loop by returning false. This function was added instead of changing the existing `Each()` to avoid breaking compatibility.
|
||||||
|
* **v0.2.1** : Make go-getable, now that [go.net/html is Go1.0-compatible][gonet] (thanks to @matrixik for pointing this out).
|
||||||
|
* **v0.2.0** : Add support for negative indices in Slice(). **BREAKING CHANGE** `Document.Root` is removed, `Document` is now a `Selection` itself (a selection of one, the root element, just like `Document.Root` was before). Add jQuery's Closest() method.
|
||||||
|
* **v0.1.1** : Add benchmarks to use as baseline for refactorings, refactor Next...() and Prev...() methods to use the new html package's linked list features (Next/PrevSibling, FirstChild). Good performance boost (40+% in some cases).
|
||||||
|
* **v0.1.0** : Initial release.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
goquery exposes two structs, `Document` and `Selection`, and the `Matcher` interface. Unlike jQuery, which is loaded as part of a DOM document, and thus acts on its containing document, goquery doesn't know which HTML document to act upon. So it needs to be told, and that's what the `Document` type is for. It holds the root document node as the initial Selection value to manipulate.
|
||||||
|
|
||||||
|
jQuery often has many variants for the same function (no argument, a selector string argument, a jQuery object argument, a DOM element argument, ...). Instead of exposing the same features in goquery as a single method with variadic empty interface arguments, statically-typed signatures are used following this naming convention:
|
||||||
|
|
||||||
|
* When the jQuery equivalent can be called with no argument, it has the same name as jQuery for the no argument signature (e.g.: `Prev()`), and the version with a selector string argument is called `XxxFiltered()` (e.g.: `PrevFiltered()`)
|
||||||
|
* When the jQuery equivalent **requires** one argument, the same name as jQuery is used for the selector string version (e.g.: `Is()`)
|
||||||
|
* The signatures accepting a jQuery object as argument are defined in goquery as `XxxSelection()` and take a `*Selection` object as argument (e.g.: `FilterSelection()`)
|
||||||
|
* The signatures accepting a DOM element as argument in jQuery are defined in goquery as `XxxNodes()` and take a variadic argument of type `*html.Node` (e.g.: `FilterNodes()`)
|
||||||
|
* The signatures accepting a function as argument in jQuery are defined in goquery as `XxxFunction()` and take a function as argument (e.g.: `FilterFunction()`)
|
||||||
|
* The goquery methods that can be called with a selector string have a corresponding version that take a `Matcher` interface and are defined as `XxxMatcher()` (e.g.: `IsMatcher()`)
|
||||||
|
|
||||||
|
Utility functions that are not in jQuery but are useful in Go are implemented as functions (that take a `*Selection` as parameter), to avoid a potential naming clash on the `*Selection`'s methods (reserved for jQuery-equivalent behaviour).
|
||||||
|
|
||||||
|
The complete [package reference documentation can be found here][doc].
|
||||||
|
|
||||||
|
Please note that Cascadia's selectors do not necessarily match all supported selectors of jQuery (Sizzle). See the [cascadia project][cascadia] for details. Also, the selectors work more like the DOM's `querySelectorAll`, than jQuery's matchers - they have no concept of contextual matching (for some concrete examples of what that means, see [this ticket](https://github.com/andybalholm/cascadia/issues/61)). In practice, it doesn't matter very often but it's something worth mentioning. Invalid selector strings compile to a `Matcher` that fails to match any node. Behaviour of the various functions that take a selector string as argument follows from that fact, e.g. (where `~` is an invalid selector string):
|
||||||
|
|
||||||
|
* `Find("~")` returns an empty selection because the selector string doesn't match anything.
|
||||||
|
* `Add("~")` returns a new selection that holds the same nodes as the original selection, because it didn't add any node (selector string didn't match anything).
|
||||||
|
* `ParentsFiltered("~")` returns an empty selection because the selector string doesn't match anything.
|
||||||
|
* `ParentsUntil("~")` returns all parents of the selection because the selector string didn't match any element to stop before the top element.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See some tips and tricks in the [wiki][].
|
||||||
|
|
||||||
|
Adapted from example_test.go:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleScrape() {
|
||||||
|
// Request the HTML page.
|
||||||
|
res, err := http.Get("http://metalsucks.net")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the HTML document
|
||||||
|
doc, err := goquery.NewDocumentFromReader(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the review items
|
||||||
|
doc.Find(".left-content article .post-title").Each(func(i int, s *goquery.Selection) {
|
||||||
|
// For each item found, get the title
|
||||||
|
title := s.Find("a").Text()
|
||||||
|
fmt.Printf("Review %d: %s\n", i, title)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ExampleScrape()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Projects
|
||||||
|
|
||||||
|
- [Goq][goq], an HTML deserialization and scraping library based on goquery and struct tags.
|
||||||
|
- [andybalholm/cascadia][cascadia], the CSS selector library used by goquery.
|
||||||
|
- [suntong/cascadia][cascadiacli], a command-line interface to the cascadia CSS selector library, useful to test selectors.
|
||||||
|
- [gocolly/colly](https://github.com/gocolly/colly), a lightning fast and elegant Scraping Framework
|
||||||
|
- [gnulnx/goperf](https://github.com/gnulnx/goperf), a website performance test tool that also fetches static assets.
|
||||||
|
- [MontFerret/ferret](https://github.com/MontFerret/ferret), declarative web scraping.
|
||||||
|
- [tacusci/berrycms](https://github.com/tacusci/berrycms), a modern simple to use CMS with easy to write plugins
|
||||||
|
- [Dataflow kit](https://github.com/slotix/dataflowkit), Web Scraping framework for Gophers.
|
||||||
|
- [Geziyor](https://github.com/geziyor/geziyor), a fast web crawling & scraping framework for Go. Supports JS rendering.
|
||||||
|
- [Pagser](https://github.com/foolin/pagser), a simple, easy, extensible, configurable HTML parser to struct based on goquery and struct tags.
|
||||||
|
- [stitcherd](https://github.com/vhodges/stitcherd), A server for doing server side includes using css selectors and DOM updates.
|
||||||
|
- [goskyr](https://github.com/jakopako/goskyr), an easily configurable command-line scraper written in Go.
|
||||||
|
- [goGetJS](https://github.com/davemolk/goGetJS), a tool for extracting, searching, and saving JavaScript files (with optional headless browser).
|
||||||
|
- [fitter](https://github.com/PxyUp/fitter), a tool for selecting values from JSON, XML, HTML and XPath formatted pages.
|
||||||
|
- [seltabl](github.com/conneroisu/seltabl), an orm-like package and supporting language server for extracting values from HTML
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
There are a number of ways you can support the project:
|
||||||
|
|
||||||
|
* Use it, star it, build something with it, spread the word!
|
||||||
|
- If you do build something open-source or otherwise publicly-visible, let me know so I can add it to the [Related Projects](#related-projects) section!
|
||||||
|
* Raise issues to improve the project (note: doc typos and clarifications are issues too!)
|
||||||
|
- Please search existing issues before opening a new one - it may have already been addressed.
|
||||||
|
* Pull requests: please discuss new code in an issue first, unless the fix is really trivial.
|
||||||
|
- Make sure new code is tested.
|
||||||
|
- Be mindful of existing code - PRs that break existing code have a high probability of being declined, unless it fixes a serious issue.
|
||||||
|
* Sponsor the developer
|
||||||
|
- See the Github Sponsor button at the top of the repo on github
|
||||||
|
- or via BuyMeACoffee.com, below
|
||||||
|
|
||||||
|
<a href="https://www.buymeacoffee.com/mna" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The [BSD 3-Clause license][bsd], the same as the [Go language][golic]. Cascadia's license is [here][caslic].
|
||||||
|
|
||||||
|
[jquery]: https://jquery.com/
|
||||||
|
[go]: https://go.dev/
|
||||||
|
[cascadia]: https://github.com/andybalholm/cascadia
|
||||||
|
[cascadiacli]: https://github.com/suntong/cascadia
|
||||||
|
[bsd]: https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
[golic]: https://go.dev/LICENSE
|
||||||
|
[caslic]: https://github.com/andybalholm/cascadia/blob/master/LICENSE
|
||||||
|
[doc]: https://pkg.go.dev/github.com/PuerkitoBio/goquery
|
||||||
|
[index]: https://api.jquery.com/index/
|
||||||
|
[gonet]: https://github.com/golang/net/
|
||||||
|
[html]: https://pkg.go.dev/golang.org/x/net/html
|
||||||
|
[wiki]: https://github.com/PuerkitoBio/goquery/wiki/Tips-and-tricks
|
||||||
|
[thatguystone]: https://github.com/thatguystone
|
||||||
|
[piotr]: https://github.com/piotrkowalczuk
|
||||||
|
[goq]: https://github.com/andrewstuart/goq
|
||||||
|
[thiemok]: https://github.com/thiemok
|
||||||
|
[djw]: https://github.com/davidjwilkins
|
||||||
124
vendor/github.com/PuerkitoBio/goquery/array.go
generated
vendored
Normal file
124
vendor/github.com/PuerkitoBio/goquery/array.go
generated
vendored
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package goquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxUint = ^uint(0)
|
||||||
|
maxInt = int(maxUint >> 1)
|
||||||
|
|
||||||
|
// ToEnd is a special index value that can be used as end index in a call
|
||||||
|
// to Slice so that all elements are selected until the end of the Selection.
|
||||||
|
// It is equivalent to passing (*Selection).Length().
|
||||||
|
ToEnd = maxInt
|
||||||
|
)
|
||||||
|
|
||||||
|
// First reduces the set of matched elements to the first in the set.
|
||||||
|
// It returns a new Selection object, and an empty Selection object if the
|
||||||
|
// the selection is empty.
|
||||||
|
func (s *Selection) First() *Selection {
|
||||||
|
return s.Eq(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last reduces the set of matched elements to the last in the set.
|
||||||
|
// It returns a new Selection object, and an empty Selection object if
|
||||||
|
// the selection is empty.
|
||||||
|
func (s *Selection) Last() *Selection {
|
||||||
|
return s.Eq(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eq reduces the set of matched elements to the one at the specified index.
|
||||||
|
// If a negative index is given, it counts backwards starting at the end of the
|
||||||
|
// set. It returns a new Selection object, and an empty Selection object if the
|
||||||
|
// index is invalid.
|
||||||
|
func (s *Selection) Eq(index int) *Selection {
|
||||||
|
if index < 0 {
|
||||||
|
index += len(s.Nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if index >= len(s.Nodes) || index < 0 {
|
||||||
|
return newEmptySelection(s.document)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Slice(index, index+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice reduces the set of matched elements to a subset specified by a range
|
||||||
|
// of indices. The start index is 0-based and indicates the index of the first
|
||||||
|
// element to select. The end index is 0-based and indicates the index at which
|
||||||
|
// the elements stop being selected (the end index is not selected).
|
||||||
|
//
|
||||||
|
// The indices may be negative, in which case they represent an offset from the
|
||||||
|
// end of the selection.
|
||||||
|
//
|
||||||
|
// The special value ToEnd may be specified as end index, in which case all elements
|
||||||
|
// until the end are selected. This works both for a positive and negative start
|
||||||
|
// index.
|
||||||
|
func (s *Selection) Slice(start, end int) *Selection {
|
||||||
|
if start < 0 {
|
||||||
|
start += len(s.Nodes)
|
||||||
|
}
|
||||||
|
if end == ToEnd {
|
||||||
|
end = len(s.Nodes)
|
||||||
|
} else if end < 0 {
|
||||||
|
end += len(s.Nodes)
|
||||||
|
}
|
||||||
|
return pushStack(s, s.Nodes[start:end])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves the underlying node at the specified index.
|
||||||
|
// Get without parameter is not implemented, since the node array is available
|
||||||
|
// on the Selection object.
|
||||||
|
func (s *Selection) Get(index int) *html.Node {
|
||||||
|
if index < 0 {
|
||||||
|
index += len(s.Nodes) // Negative index gets from the end
|
||||||
|
}
|
||||||
|
return s.Nodes[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index returns the position of the first element within the Selection object
|
||||||
|
// relative to its sibling elements.
|
||||||
|
func (s *Selection) Index() int {
|
||||||
|
if len(s.Nodes) > 0 {
|
||||||
|
return newSingleSelection(s.Nodes[0], s.document).PrevAll().Length()
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexSelector returns the position of the first element within the
|
||||||
|
// Selection object relative to the elements matched by the selector, or -1 if
|
||||||
|
// not found.
|
||||||
|
func (s *Selection) IndexSelector(selector string) int {
|
||||||
|
if len(s.Nodes) > 0 {
|
||||||
|
sel := s.document.Find(selector)
|
||||||
|
return indexInSlice(sel.Nodes, s.Nodes[0])
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexMatcher returns the position of the first element within the
|
||||||
|
// Selection object relative to the elements matched by the matcher, or -1 if
|
||||||
|
// not found.
|
||||||
|
func (s *Selection) IndexMatcher(m Matcher) int {
|
||||||
|
if len(s.Nodes) > 0 {
|
||||||
|
sel := s.document.FindMatcher(m)
|
||||||
|
return indexInSlice(sel.Nodes, s.Nodes[0])
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexOfNode returns the position of the specified node within the Selection
|
||||||
|
// object, or -1 if not found.
|
||||||
|
func (s *Selection) IndexOfNode(node *html.Node) int {
|
||||||
|
return indexInSlice(s.Nodes, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexOfSelection returns the position of the first node in the specified
|
||||||
|
// Selection object within this Selection object, or -1 if not found.
|
||||||
|
func (s *Selection) IndexOfSelection(sel *Selection) int {
|
||||||
|
if sel != nil && len(sel.Nodes) > 0 {
|
||||||
|
return indexInSlice(s.Nodes, sel.Nodes[0])
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
123
vendor/github.com/PuerkitoBio/goquery/doc.go
generated
vendored
Normal file
123
vendor/github.com/PuerkitoBio/goquery/doc.go
generated
vendored
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// Copyright (c) 2012-2016, Martin Angers & Contributors
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
// are permitted provided that the following conditions are met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer.
|
||||||
|
// * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer in the documentation and/or
|
||||||
|
// other materials provided with the distribution.
|
||||||
|
// * Neither the name of the author nor the names of its contributors may be used to
|
||||||
|
// endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
|
||||||
|
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||||
|
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||||
|
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
|
||||||
|
// WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package goquery implements features similar to jQuery, including the chainable
|
||||||
|
syntax, to manipulate and query an HTML document.
|
||||||
|
|
||||||
|
It brings a syntax and a set of features similar to jQuery to the Go language.
|
||||||
|
It is based on Go's net/html package and the CSS Selector library cascadia.
|
||||||
|
Since the net/html parser returns nodes, and not a full-featured DOM
|
||||||
|
tree, jQuery's stateful manipulation functions (like height(), css(), detach())
|
||||||
|
have been left off.
|
||||||
|
|
||||||
|
Also, because the net/html parser requires UTF-8 encoding, so does goquery: it is
|
||||||
|
the caller's responsibility to ensure that the source document provides UTF-8 encoded HTML.
|
||||||
|
See the repository's wiki for various options on how to do this.
|
||||||
|
|
||||||
|
Syntax-wise, it is as close as possible to jQuery, with the same method names when
|
||||||
|
possible, and that warm and fuzzy chainable interface. jQuery being the
|
||||||
|
ultra-popular library that it is, writing a similar HTML-manipulating
|
||||||
|
library was better to follow its API than to start anew (in the same spirit as
|
||||||
|
Go's fmt package), even though some of its methods are less than intuitive (looking
|
||||||
|
at you, index()...).
|
||||||
|
|
||||||
|
It is hosted on GitHub, along with additional documentation in the README.md
|
||||||
|
file: https://github.com/puerkitobio/goquery
|
||||||
|
|
||||||
|
Please note that because of the net/html dependency, goquery requires Go1.1+.
|
||||||
|
|
||||||
|
The various methods are split into files based on the category of behavior.
|
||||||
|
The three dots (...) indicate that various "overloads" are available.
|
||||||
|
|
||||||
|
* array.go : array-like positional manipulation of the selection.
|
||||||
|
- Eq()
|
||||||
|
- First()
|
||||||
|
- Get()
|
||||||
|
- Index...()
|
||||||
|
- Last()
|
||||||
|
- Slice()
|
||||||
|
|
||||||
|
* expand.go : methods that expand or augment the selection's set.
|
||||||
|
- Add...()
|
||||||
|
- AndSelf()
|
||||||
|
- Union(), which is an alias for AddSelection()
|
||||||
|
|
||||||
|
* filter.go : filtering methods, that reduce the selection's set.
|
||||||
|
- End()
|
||||||
|
- Filter...()
|
||||||
|
- Has...()
|
||||||
|
- Intersection(), which is an alias of FilterSelection()
|
||||||
|
- Not...()
|
||||||
|
|
||||||
|
* iteration.go : methods to loop over the selection's nodes.
|
||||||
|
- Each()
|
||||||
|
- EachWithBreak()
|
||||||
|
- Map()
|
||||||
|
|
||||||
|
* manipulation.go : methods for modifying the document
|
||||||
|
- After...()
|
||||||
|
- Append...()
|
||||||
|
- Before...()
|
||||||
|
- Clone()
|
||||||
|
- Empty()
|
||||||
|
- Prepend...()
|
||||||
|
- Remove...()
|
||||||
|
- ReplaceWith...()
|
||||||
|
- Unwrap()
|
||||||
|
- Wrap...()
|
||||||
|
- WrapAll...()
|
||||||
|
- WrapInner...()
|
||||||
|
|
||||||
|
* property.go : methods that inspect and get the node's properties values.
|
||||||
|
- Attr*(), RemoveAttr(), SetAttr()
|
||||||
|
- AddClass(), HasClass(), RemoveClass(), ToggleClass()
|
||||||
|
- Html()
|
||||||
|
- Length()
|
||||||
|
- Size(), which is an alias for Length()
|
||||||
|
- Text()
|
||||||
|
|
||||||
|
* query.go : methods that query, or reflect, a node's identity.
|
||||||
|
- Contains()
|
||||||
|
- Is...()
|
||||||
|
|
||||||
|
* traversal.go : methods to traverse the HTML document tree.
|
||||||
|
- Children...()
|
||||||
|
- Contents()
|
||||||
|
- Find...()
|
||||||
|
- Next...()
|
||||||
|
- Parent[s]...()
|
||||||
|
- Prev...()
|
||||||
|
- Siblings...()
|
||||||
|
|
||||||
|
* type.go : definition of the types exposed by goquery.
|
||||||
|
- Document
|
||||||
|
- Selection
|
||||||
|
- Matcher
|
||||||
|
|
||||||
|
* utilities.go : definition of helper functions (and not methods on a *Selection)
|
||||||
|
that are not part of jQuery, but are useful to goquery.
|
||||||
|
- NodeName
|
||||||
|
- OuterHtml
|
||||||
|
*/
|
||||||
|
package goquery
|
||||||
70
vendor/github.com/PuerkitoBio/goquery/expand.go
generated
vendored
Normal file
70
vendor/github.com/PuerkitoBio/goquery/expand.go
generated
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package goquery
|
||||||
|
|
||||||
|
import "golang.org/x/net/html"
|
||||||
|
|
||||||
|
// Add adds the selector string's matching nodes to those in the current
|
||||||
|
// selection and returns a new Selection object.
|
||||||
|
// The selector string is run in the context of the document of the current
|
||||||
|
// Selection object.
|
||||||
|
func (s *Selection) Add(selector string) *Selection {
|
||||||
|
return s.AddNodes(findWithMatcher([]*html.Node{s.document.rootNode}, compileMatcher(selector))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddMatcher adds the matcher's matching nodes to those in the current
|
||||||
|
// selection and returns a new Selection object.
|
||||||
|
// The matcher is run in the context of the document of the current
|
||||||
|
// Selection object.
|
||||||
|
func (s *Selection) AddMatcher(m Matcher) *Selection {
|
||||||
|
return s.AddNodes(findWithMatcher([]*html.Node{s.document.rootNode}, m)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSelection adds the specified Selection object's nodes to those in the
|
||||||
|
// current selection and returns a new Selection object.
|
||||||
|
func (s *Selection) AddSelection(sel *Selection) *Selection {
|
||||||
|
if sel == nil {
|
||||||
|
return s.AddNodes()
|
||||||
|
}
|
||||||
|
return s.AddNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union is an alias for AddSelection.
|
||||||
|
func (s *Selection) Union(sel *Selection) *Selection {
|
||||||
|
return s.AddSelection(sel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNodes adds the specified nodes to those in the
|
||||||
|
// current selection and returns a new Selection object.
|
||||||
|
func (s *Selection) AddNodes(nodes ...*html.Node) *Selection {
|
||||||
|
return pushStack(s, appendWithoutDuplicates(s.Nodes, nodes, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndSelf adds the previous set of elements on the stack to the current set.
|
||||||
|
// It returns a new Selection object containing the current Selection combined
|
||||||
|
// with the previous one.
|
||||||
|
// Deprecated: This function has been deprecated and is now an alias for AddBack().
|
||||||
|
func (s *Selection) AndSelf() *Selection {
|
||||||
|
return s.AddBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBack adds the previous set of elements on the stack to the current set.
|
||||||
|
// It returns a new Selection object containing the current Selection combined
|
||||||
|
// with the previous one.
|
||||||
|
func (s *Selection) AddBack() *Selection {
|
||||||
|
return s.AddSelection(s.prevSel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBackFiltered reduces the previous set of elements on the stack to those that
|
||||||
|
// match the selector string, and adds them to the current set.
|
||||||
|
// It returns a new Selection object containing the current Selection combined
|
||||||
|
// with the filtered previous one
|
||||||
|
func (s *Selection) AddBackFiltered(selector string) *Selection {
|
||||||
|
return s.AddSelection(s.prevSel.Filter(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBackMatcher reduces the previous set of elements on the stack to those that match
|
||||||
|
// the matcher, and adds them to the current set.
|
||||||
|
// It returns a new Selection object containing the current Selection combined
|
||||||
|
// with the filtered previous one
|
||||||
|
func (s *Selection) AddBackMatcher(m Matcher) *Selection {
|
||||||
|
return s.AddSelection(s.prevSel.FilterMatcher(m))
|
||||||
|
}
|
||||||
163
vendor/github.com/PuerkitoBio/goquery/filter.go
generated
vendored
Normal file
163
vendor/github.com/PuerkitoBio/goquery/filter.go
generated
vendored
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package goquery
|
||||||
|
|
||||||
|
import "golang.org/x/net/html"
|
||||||
|
|
||||||
|
// Filter reduces the set of matched elements to those that match the selector string.
|
||||||
|
// It returns a new Selection object for this subset of matching elements.
|
||||||
|
func (s *Selection) Filter(selector string) *Selection {
|
||||||
|
return s.FilterMatcher(compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterMatcher reduces the set of matched elements to those that match
|
||||||
|
// the given matcher. It returns a new Selection object for this subset
|
||||||
|
// of matching elements.
|
||||||
|
func (s *Selection) FilterMatcher(m Matcher) *Selection {
|
||||||
|
return pushStack(s, winnow(s, m, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not removes elements from the Selection that match the selector string.
|
||||||
|
// It returns a new Selection object with the matching elements removed.
|
||||||
|
func (s *Selection) Not(selector string) *Selection {
|
||||||
|
return s.NotMatcher(compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotMatcher removes elements from the Selection that match the given matcher.
|
||||||
|
// It returns a new Selection object with the matching elements removed.
|
||||||
|
func (s *Selection) NotMatcher(m Matcher) *Selection {
|
||||||
|
return pushStack(s, winnow(s, m, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterFunction reduces the set of matched elements to those that pass the function's test.
|
||||||
|
// It returns a new Selection object for this subset of elements.
|
||||||
|
func (s *Selection) FilterFunction(f func(int, *Selection) bool) *Selection {
|
||||||
|
return pushStack(s, winnowFunction(s, f, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotFunction removes elements from the Selection that pass the function's test.
|
||||||
|
// It returns a new Selection object with the matching elements removed.
|
||||||
|
func (s *Selection) NotFunction(f func(int, *Selection) bool) *Selection {
|
||||||
|
return pushStack(s, winnowFunction(s, f, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterNodes reduces the set of matched elements to those that match the specified nodes.
|
||||||
|
// It returns a new Selection object for this subset of elements.
|
||||||
|
func (s *Selection) FilterNodes(nodes ...*html.Node) *Selection {
|
||||||
|
return pushStack(s, winnowNodes(s, nodes, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotNodes removes elements from the Selection that match the specified nodes.
|
||||||
|
// It returns a new Selection object with the matching elements removed.
|
||||||
|
func (s *Selection) NotNodes(nodes ...*html.Node) *Selection {
|
||||||
|
return pushStack(s, winnowNodes(s, nodes, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterSelection reduces the set of matched elements to those that match a
|
||||||
|
// node in the specified Selection object.
|
||||||
|
// It returns a new Selection object for this subset of elements.
|
||||||
|
func (s *Selection) FilterSelection(sel *Selection) *Selection {
|
||||||
|
if sel == nil {
|
||||||
|
return pushStack(s, winnowNodes(s, nil, true))
|
||||||
|
}
|
||||||
|
return pushStack(s, winnowNodes(s, sel.Nodes, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotSelection removes elements from the Selection that match a node in the specified
|
||||||
|
// Selection object. It returns a new Selection object with the matching elements removed.
|
||||||
|
func (s *Selection) NotSelection(sel *Selection) *Selection {
|
||||||
|
if sel == nil {
|
||||||
|
return pushStack(s, winnowNodes(s, nil, false))
|
||||||
|
}
|
||||||
|
return pushStack(s, winnowNodes(s, sel.Nodes, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intersection is an alias for FilterSelection.
|
||||||
|
func (s *Selection) Intersection(sel *Selection) *Selection {
|
||||||
|
return s.FilterSelection(sel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has reduces the set of matched elements to those that have a descendant
|
||||||
|
// that matches the selector.
|
||||||
|
// It returns a new Selection object with the matching elements.
|
||||||
|
func (s *Selection) Has(selector string) *Selection {
|
||||||
|
return s.HasSelection(s.document.Find(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasMatcher reduces the set of matched elements to those that have a descendant
|
||||||
|
// that matches the matcher.
|
||||||
|
// It returns a new Selection object with the matching elements.
|
||||||
|
func (s *Selection) HasMatcher(m Matcher) *Selection {
|
||||||
|
return s.HasSelection(s.document.FindMatcher(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNodes reduces the set of matched elements to those that have a
|
||||||
|
// descendant that matches one of the nodes.
|
||||||
|
// It returns a new Selection object with the matching elements.
|
||||||
|
func (s *Selection) HasNodes(nodes ...*html.Node) *Selection {
|
||||||
|
return s.FilterFunction(func(_ int, sel *Selection) bool {
|
||||||
|
// Add all nodes that contain one of the specified nodes
|
||||||
|
for _, n := range nodes {
|
||||||
|
if sel.Contains(n) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasSelection reduces the set of matched elements to those that have a
|
||||||
|
// descendant that matches one of the nodes of the specified Selection object.
|
||||||
|
// It returns a new Selection object with the matching elements.
|
||||||
|
func (s *Selection) HasSelection(sel *Selection) *Selection {
|
||||||
|
if sel == nil {
|
||||||
|
return s.HasNodes()
|
||||||
|
}
|
||||||
|
return s.HasNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// End ends the most recent filtering operation in the current chain and
|
||||||
|
// returns the set of matched elements to its previous state.
|
||||||
|
func (s *Selection) End() *Selection {
|
||||||
|
if s.prevSel != nil {
|
||||||
|
return s.prevSel
|
||||||
|
}
|
||||||
|
return newEmptySelection(s.document)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter based on the matcher, and the indicator to keep (Filter) or
|
||||||
|
// to get rid of (Not) the matching elements.
|
||||||
|
func winnow(sel *Selection, m Matcher, keep bool) []*html.Node {
|
||||||
|
// Optimize if keep is requested
|
||||||
|
if keep {
|
||||||
|
return m.Filter(sel.Nodes)
|
||||||
|
}
|
||||||
|
// Use grep
|
||||||
|
return grep(sel, func(i int, s *Selection) bool {
|
||||||
|
return !m.Match(s.Get(0))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter based on an array of nodes, and the indicator to keep (Filter) or
|
||||||
|
// to get rid of (Not) the matching elements.
|
||||||
|
func winnowNodes(sel *Selection, nodes []*html.Node, keep bool) []*html.Node {
|
||||||
|
if len(nodes)+len(sel.Nodes) < minNodesForSet {
|
||||||
|
return grep(sel, func(i int, s *Selection) bool {
|
||||||
|
return isInSlice(nodes, s.Get(0)) == keep
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
set := make(map[*html.Node]bool)
|
||||||
|
for _, n := range nodes {
|
||||||
|
set[n] = true
|
||||||
|
}
|
||||||
|
return grep(sel, func(i int, s *Selection) bool {
|
||||||
|
return set[s.Get(0)] == keep
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter based on a function test, and the indicator to keep (Filter) or
|
||||||
|
// to get rid of (Not) the matching elements.
|
||||||
|
func winnowFunction(sel *Selection, f func(int, *Selection) bool, keep bool) []*html.Node {
|
||||||
|
return grep(sel, func(i int, s *Selection) bool {
|
||||||
|
return f(i, s) == keep
|
||||||
|
})
|
||||||
|
}
|
||||||
61
vendor/github.com/PuerkitoBio/goquery/iteration.go
generated
vendored
Normal file
61
vendor/github.com/PuerkitoBio/goquery/iteration.go
generated
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package goquery
|
||||||
|
|
||||||
|
import "iter"
|
||||||
|
|
||||||
|
// Each iterates over a Selection object, executing a function for each
|
||||||
|
// matched element. It returns the current Selection object. The function
|
||||||
|
// f is called for each element in the selection with the index of the
|
||||||
|
// element in that selection starting at 0, and a *Selection that contains
|
||||||
|
// only that element.
|
||||||
|
func (s *Selection) Each(f func(int, *Selection)) *Selection {
|
||||||
|
for i, n := range s.Nodes {
|
||||||
|
f(i, newSingleSelection(n, s.document))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// EachIter returns an iterator that yields the Selection object in order.
|
||||||
|
// The implementation is similar to Each, but it returns an iterator instead.
|
||||||
|
func (s *Selection) EachIter() iter.Seq2[int, *Selection] {
|
||||||
|
return func(yield func(int, *Selection) bool) {
|
||||||
|
for i, n := range s.Nodes {
|
||||||
|
if !yield(i, newSingleSelection(n, s.document)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EachWithBreak iterates over a Selection object, executing a function for each
|
||||||
|
// matched element. It is identical to Each except that it is possible to break
|
||||||
|
// out of the loop by returning false in the callback function. It returns the
|
||||||
|
// current Selection object.
|
||||||
|
func (s *Selection) EachWithBreak(f func(int, *Selection) bool) *Selection {
|
||||||
|
for i, n := range s.Nodes {
|
||||||
|
if !f(i, newSingleSelection(n, s.document)) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map passes each element in the current matched set through a function,
|
||||||
|
// producing a slice of string holding the returned values. The function
|
||||||
|
// f is called for each element in the selection with the index of the
|
||||||
|
// element in that selection starting at 0, and a *Selection that contains
|
||||||
|
// only that element.
|
||||||
|
func (s *Selection) Map(f func(int, *Selection) string) (result []string) {
|
||||||
|
return Map(s, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map is the generic version of Selection.Map, allowing any type to be
|
||||||
|
// returned.
|
||||||
|
func Map[E any](s *Selection, f func(int, *Selection) E) (result []E) {
|
||||||
|
result = make([]E, len(s.Nodes))
|
||||||
|
|
||||||
|
for i, n := range s.Nodes {
|
||||||
|
result[i] = f(i, newSingleSelection(n, s.document))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
679
vendor/github.com/PuerkitoBio/goquery/manipulation.go
generated
vendored
Normal file
679
vendor/github.com/PuerkitoBio/goquery/manipulation.go
generated
vendored
Normal file
@@ -0,0 +1,679 @@
|
|||||||
|
package goquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// After applies the selector from the root document and inserts the matched elements
|
||||||
|
// after the elements in the set of matched elements.
|
||||||
|
//
|
||||||
|
// If one of the matched elements in the selection is not currently in the
|
||||||
|
// document, it's impossible to insert nodes after it, so it will be ignored.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) After(selector string) *Selection {
|
||||||
|
return s.AfterMatcher(compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterMatcher applies the matcher from the root document and inserts the matched elements
|
||||||
|
// after the elements in the set of matched elements.
|
||||||
|
//
|
||||||
|
// If one of the matched elements in the selection is not currently in the
|
||||||
|
// document, it's impossible to insert nodes after it, so it will be ignored.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) AfterMatcher(m Matcher) *Selection {
|
||||||
|
return s.AfterNodes(m.MatchAll(s.document.rootNode)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterSelection inserts the elements in the selection after each element in the set of matched
|
||||||
|
// elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) AfterSelection(sel *Selection) *Selection {
|
||||||
|
return s.AfterNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterHtml parses the html and inserts it after the set of matched elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) AfterHtml(htmlStr string) *Selection {
|
||||||
|
return s.eachNodeHtml(htmlStr, true, func(node *html.Node, nodes []*html.Node) {
|
||||||
|
nextSibling := node.NextSibling
|
||||||
|
for _, n := range nodes {
|
||||||
|
if node.Parent != nil {
|
||||||
|
node.Parent.InsertBefore(n, nextSibling)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterNodes inserts the nodes after each element in the set of matched elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) AfterNodes(ns ...*html.Node) *Selection {
|
||||||
|
return s.manipulateNodes(ns, true, func(sn *html.Node, n *html.Node) {
|
||||||
|
if sn.Parent != nil {
|
||||||
|
sn.Parent.InsertBefore(n, sn.NextSibling)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append appends the elements specified by the selector to the end of each element
|
||||||
|
// in the set of matched elements, following those rules:
|
||||||
|
//
|
||||||
|
// 1) The selector is applied to the root document.
|
||||||
|
//
|
||||||
|
// 2) Elements that are part of the document will be moved to the new location.
|
||||||
|
//
|
||||||
|
// 3) If there are multiple locations to append to, cloned nodes will be
|
||||||
|
// appended to all target locations except the last one, which will be moved
|
||||||
|
// as noted in (2).
|
||||||
|
func (s *Selection) Append(selector string) *Selection {
|
||||||
|
return s.AppendMatcher(compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendMatcher appends the elements specified by the matcher to the end of each element
|
||||||
|
// in the set of matched elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) AppendMatcher(m Matcher) *Selection {
|
||||||
|
return s.AppendNodes(m.MatchAll(s.document.rootNode)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendSelection appends the elements in the selection to the end of each element
|
||||||
|
// in the set of matched elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) AppendSelection(sel *Selection) *Selection {
|
||||||
|
return s.AppendNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendHtml parses the html and appends it to the set of matched elements.
|
||||||
|
func (s *Selection) AppendHtml(htmlStr string) *Selection {
|
||||||
|
return s.eachNodeHtml(htmlStr, false, func(node *html.Node, nodes []*html.Node) {
|
||||||
|
for _, n := range nodes {
|
||||||
|
node.AppendChild(n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendNodes appends the specified nodes to each node in the set of matched elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) AppendNodes(ns ...*html.Node) *Selection {
|
||||||
|
return s.manipulateNodes(ns, false, func(sn *html.Node, n *html.Node) {
|
||||||
|
sn.AppendChild(n)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before inserts the matched elements before each element in the set of matched elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) Before(selector string) *Selection {
|
||||||
|
return s.BeforeMatcher(compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeMatcher inserts the matched elements before each element in the set of matched elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) BeforeMatcher(m Matcher) *Selection {
|
||||||
|
return s.BeforeNodes(m.MatchAll(s.document.rootNode)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeSelection inserts the elements in the selection before each element in the set of matched
|
||||||
|
// elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) BeforeSelection(sel *Selection) *Selection {
|
||||||
|
return s.BeforeNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeHtml parses the html and inserts it before the set of matched elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) BeforeHtml(htmlStr string) *Selection {
|
||||||
|
return s.eachNodeHtml(htmlStr, true, func(node *html.Node, nodes []*html.Node) {
|
||||||
|
for _, n := range nodes {
|
||||||
|
if node.Parent != nil {
|
||||||
|
node.Parent.InsertBefore(n, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeNodes inserts the nodes before each element in the set of matched elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) BeforeNodes(ns ...*html.Node) *Selection {
|
||||||
|
return s.manipulateNodes(ns, false, func(sn *html.Node, n *html.Node) {
|
||||||
|
if sn.Parent != nil {
|
||||||
|
sn.Parent.InsertBefore(n, sn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone creates a deep copy of the set of matched nodes. The new nodes will not be
|
||||||
|
// attached to the document.
|
||||||
|
func (s *Selection) Clone() *Selection {
|
||||||
|
ns := newEmptySelection(s.document)
|
||||||
|
ns.Nodes = cloneNodes(s.Nodes)
|
||||||
|
return ns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty removes all children nodes from the set of matched elements.
|
||||||
|
// It returns the children nodes in a new Selection.
|
||||||
|
func (s *Selection) Empty() *Selection {
|
||||||
|
var nodes []*html.Node
|
||||||
|
|
||||||
|
for _, n := range s.Nodes {
|
||||||
|
for c := n.FirstChild; c != nil; c = n.FirstChild {
|
||||||
|
n.RemoveChild(c)
|
||||||
|
nodes = append(nodes, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pushStack(s, nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepend prepends the elements specified by the selector to each element in
|
||||||
|
// the set of matched elements, following the same rules as Append.
|
||||||
|
func (s *Selection) Prepend(selector string) *Selection {
|
||||||
|
return s.PrependMatcher(compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrependMatcher prepends the elements specified by the matcher to each
|
||||||
|
// element in the set of matched elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) PrependMatcher(m Matcher) *Selection {
|
||||||
|
return s.PrependNodes(m.MatchAll(s.document.rootNode)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrependSelection prepends the elements in the selection to each element in
|
||||||
|
// the set of matched elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) PrependSelection(sel *Selection) *Selection {
|
||||||
|
return s.PrependNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrependHtml parses the html and prepends it to the set of matched elements.
|
||||||
|
func (s *Selection) PrependHtml(htmlStr string) *Selection {
|
||||||
|
return s.eachNodeHtml(htmlStr, false, func(node *html.Node, nodes []*html.Node) {
|
||||||
|
firstChild := node.FirstChild
|
||||||
|
for _, n := range nodes {
|
||||||
|
node.InsertBefore(n, firstChild)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrependNodes prepends the specified nodes to each node in the set of
|
||||||
|
// matched elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) PrependNodes(ns ...*html.Node) *Selection {
|
||||||
|
return s.manipulateNodes(ns, true, func(sn *html.Node, n *html.Node) {
|
||||||
|
// sn.FirstChild may be nil, in which case this functions like
|
||||||
|
// sn.AppendChild()
|
||||||
|
sn.InsertBefore(n, sn.FirstChild)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes the set of matched elements from the document.
|
||||||
|
// It returns the same selection, now consisting of nodes not in the document.
|
||||||
|
func (s *Selection) Remove() *Selection {
|
||||||
|
for _, n := range s.Nodes {
|
||||||
|
if n.Parent != nil {
|
||||||
|
n.Parent.RemoveChild(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveFiltered removes from the current set of matched elements those that
|
||||||
|
// match the selector filter. It returns the Selection of removed nodes.
|
||||||
|
//
|
||||||
|
// For example if the selection s contains "<h1>", "<h2>" and "<h3>"
|
||||||
|
// and s.RemoveFiltered("h2") is called, only the "<h2>" node is removed
|
||||||
|
// (and returned), while "<h1>" and "<h3>" are kept in the document.
|
||||||
|
func (s *Selection) RemoveFiltered(selector string) *Selection {
|
||||||
|
return s.RemoveMatcher(compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveMatcher removes from the current set of matched elements those that
|
||||||
|
// match the Matcher filter. It returns the Selection of removed nodes.
|
||||||
|
// See RemoveFiltered for additional information.
|
||||||
|
func (s *Selection) RemoveMatcher(m Matcher) *Selection {
|
||||||
|
return s.FilterMatcher(m).Remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceWith replaces each element in the set of matched elements with the
|
||||||
|
// nodes matched by the given selector.
|
||||||
|
// It returns the removed elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) ReplaceWith(selector string) *Selection {
|
||||||
|
return s.ReplaceWithMatcher(compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceWithMatcher replaces each element in the set of matched elements with
|
||||||
|
// the nodes matched by the given Matcher.
|
||||||
|
// It returns the removed elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) ReplaceWithMatcher(m Matcher) *Selection {
|
||||||
|
return s.ReplaceWithNodes(m.MatchAll(s.document.rootNode)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceWithSelection replaces each element in the set of matched elements with
|
||||||
|
// the nodes from the given Selection.
|
||||||
|
// It returns the removed elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) ReplaceWithSelection(sel *Selection) *Selection {
|
||||||
|
return s.ReplaceWithNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceWithHtml replaces each element in the set of matched elements with
|
||||||
|
// the parsed HTML.
|
||||||
|
// It returns the removed elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) ReplaceWithHtml(htmlStr string) *Selection {
|
||||||
|
s.eachNodeHtml(htmlStr, true, func(node *html.Node, nodes []*html.Node) {
|
||||||
|
nextSibling := node.NextSibling
|
||||||
|
for _, n := range nodes {
|
||||||
|
if node.Parent != nil {
|
||||||
|
node.Parent.InsertBefore(n, nextSibling)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return s.Remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceWithNodes replaces each element in the set of matched elements with
|
||||||
|
// the given nodes.
|
||||||
|
// It returns the removed elements.
|
||||||
|
//
|
||||||
|
// This follows the same rules as Selection.Append.
|
||||||
|
func (s *Selection) ReplaceWithNodes(ns ...*html.Node) *Selection {
|
||||||
|
s.AfterNodes(ns...)
|
||||||
|
return s.Remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHtml sets the html content of each element in the selection to
|
||||||
|
// specified html string.
|
||||||
|
func (s *Selection) SetHtml(htmlStr string) *Selection {
|
||||||
|
for _, context := range s.Nodes {
|
||||||
|
for c := context.FirstChild; c != nil; c = context.FirstChild {
|
||||||
|
context.RemoveChild(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.eachNodeHtml(htmlStr, false, func(node *html.Node, nodes []*html.Node) {
|
||||||
|
for _, n := range nodes {
|
||||||
|
node.AppendChild(n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetText sets the content of each element in the selection to specified content.
|
||||||
|
// The provided text string is escaped.
|
||||||
|
func (s *Selection) SetText(text string) *Selection {
|
||||||
|
return s.SetHtml(html.EscapeString(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap removes the parents of the set of matched elements, leaving the matched
|
||||||
|
// elements (and their siblings, if any) in their place.
|
||||||
|
// It returns the original selection.
|
||||||
|
func (s *Selection) Unwrap() *Selection {
|
||||||
|
s.Parent().Each(func(i int, ss *Selection) {
|
||||||
|
// For some reason, jquery allows unwrap to remove the <head> element, so
|
||||||
|
// allowing it here too. Same for <html>. Why it allows those elements to
|
||||||
|
// be unwrapped while not allowing body is a mystery to me.
|
||||||
|
if ss.Nodes[0].Data != "body" {
|
||||||
|
ss.ReplaceWithSelection(ss.Contents())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap wraps each element in the set of matched elements inside the first
|
||||||
|
// element matched by the given selector. The matched child is cloned before
|
||||||
|
// being inserted into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) Wrap(selector string) *Selection {
|
||||||
|
return s.WrapMatcher(compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapMatcher wraps each element in the set of matched elements inside the
|
||||||
|
// first element matched by the given matcher. The matched child is cloned
|
||||||
|
// before being inserted into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapMatcher(m Matcher) *Selection {
|
||||||
|
return s.wrapNodes(m.MatchAll(s.document.rootNode)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapSelection wraps each element in the set of matched elements inside the
|
||||||
|
// first element in the given Selection. The element is cloned before being
|
||||||
|
// inserted into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapSelection(sel *Selection) *Selection {
|
||||||
|
return s.wrapNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapHtml wraps each element in the set of matched elements inside the inner-
|
||||||
|
// most child of the given HTML.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapHtml(htmlStr string) *Selection {
|
||||||
|
nodesMap := make(map[string][]*html.Node)
|
||||||
|
for _, context := range s.Nodes {
|
||||||
|
var parent *html.Node
|
||||||
|
if context.Parent != nil {
|
||||||
|
parent = context.Parent
|
||||||
|
} else {
|
||||||
|
parent = &html.Node{Type: html.ElementNode}
|
||||||
|
}
|
||||||
|
nodes, found := nodesMap[nodeName(parent)]
|
||||||
|
if !found {
|
||||||
|
nodes = parseHtmlWithContext(htmlStr, parent)
|
||||||
|
nodesMap[nodeName(parent)] = nodes
|
||||||
|
}
|
||||||
|
newSingleSelection(context, s.document).wrapAllNodes(cloneNodes(nodes)...)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapNode wraps each element in the set of matched elements inside the inner-
|
||||||
|
// most child of the given node. The given node is copied before being inserted
|
||||||
|
// into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapNode(n *html.Node) *Selection {
|
||||||
|
return s.wrapNodes(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Selection) wrapNodes(ns ...*html.Node) *Selection {
|
||||||
|
s.Each(func(i int, ss *Selection) {
|
||||||
|
ss.wrapAllNodes(ns...)
|
||||||
|
})
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapAll wraps a single HTML structure, matched by the given selector, around
|
||||||
|
// all elements in the set of matched elements. The matched child is cloned
|
||||||
|
// before being inserted into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapAll(selector string) *Selection {
|
||||||
|
return s.WrapAllMatcher(compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapAllMatcher wraps a single HTML structure, matched by the given Matcher,
|
||||||
|
// around all elements in the set of matched elements. The matched child is
|
||||||
|
// cloned before being inserted into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapAllMatcher(m Matcher) *Selection {
|
||||||
|
return s.wrapAllNodes(m.MatchAll(s.document.rootNode)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapAllSelection wraps a single HTML structure, the first node of the given
|
||||||
|
// Selection, around all elements in the set of matched elements. The matched
|
||||||
|
// child is cloned before being inserted into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapAllSelection(sel *Selection) *Selection {
|
||||||
|
return s.wrapAllNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapAllHtml wraps the given HTML structure around all elements in the set of
|
||||||
|
// matched elements. The matched child is cloned before being inserted into the
|
||||||
|
// document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapAllHtml(htmlStr string) *Selection {
|
||||||
|
var context *html.Node
|
||||||
|
var nodes []*html.Node
|
||||||
|
if len(s.Nodes) > 0 {
|
||||||
|
context = s.Nodes[0]
|
||||||
|
if context.Parent != nil {
|
||||||
|
nodes = parseHtmlWithContext(htmlStr, context)
|
||||||
|
} else {
|
||||||
|
nodes = parseHtml(htmlStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.wrapAllNodes(nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Selection) wrapAllNodes(ns ...*html.Node) *Selection {
|
||||||
|
if len(ns) > 0 {
|
||||||
|
return s.WrapAllNode(ns[0])
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapAllNode wraps the given node around the first element in the Selection,
|
||||||
|
// making all other nodes in the Selection children of the given node. The node
|
||||||
|
// is cloned before being inserted into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapAllNode(n *html.Node) *Selection {
|
||||||
|
if s.Size() == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap := cloneNode(n)
|
||||||
|
|
||||||
|
first := s.Nodes[0]
|
||||||
|
if first.Parent != nil {
|
||||||
|
first.Parent.InsertBefore(wrap, first)
|
||||||
|
first.Parent.RemoveChild(first)
|
||||||
|
}
|
||||||
|
|
||||||
|
for c := getFirstChildEl(wrap); c != nil; c = getFirstChildEl(wrap) {
|
||||||
|
wrap = c
|
||||||
|
}
|
||||||
|
|
||||||
|
newSingleSelection(wrap, s.document).AppendSelection(s)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapInner wraps an HTML structure, matched by the given selector, around the
|
||||||
|
// content of element in the set of matched elements. The matched child is
|
||||||
|
// cloned before being inserted into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapInner(selector string) *Selection {
|
||||||
|
return s.WrapInnerMatcher(compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapInnerMatcher wraps an HTML structure, matched by the given selector,
|
||||||
|
// around the content of element in the set of matched elements. The matched
|
||||||
|
// child is cloned before being inserted into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapInnerMatcher(m Matcher) *Selection {
|
||||||
|
return s.wrapInnerNodes(m.MatchAll(s.document.rootNode)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapInnerSelection wraps an HTML structure, matched by the given selector,
|
||||||
|
// around the content of element in the set of matched elements. The matched
|
||||||
|
// child is cloned before being inserted into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapInnerSelection(sel *Selection) *Selection {
|
||||||
|
return s.wrapInnerNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapInnerHtml wraps an HTML structure, matched by the given selector, around
|
||||||
|
// the content of element in the set of matched elements. The matched child is
|
||||||
|
// cloned before being inserted into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapInnerHtml(htmlStr string) *Selection {
|
||||||
|
nodesMap := make(map[string][]*html.Node)
|
||||||
|
for _, context := range s.Nodes {
|
||||||
|
nodes, found := nodesMap[nodeName(context)]
|
||||||
|
if !found {
|
||||||
|
nodes = parseHtmlWithContext(htmlStr, context)
|
||||||
|
nodesMap[nodeName(context)] = nodes
|
||||||
|
}
|
||||||
|
newSingleSelection(context, s.document).wrapInnerNodes(cloneNodes(nodes)...)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapInnerNode wraps an HTML structure, matched by the given selector, around
|
||||||
|
// the content of element in the set of matched elements. The matched child is
|
||||||
|
// cloned before being inserted into the document.
|
||||||
|
//
|
||||||
|
// It returns the original set of elements.
|
||||||
|
func (s *Selection) WrapInnerNode(n *html.Node) *Selection {
|
||||||
|
return s.wrapInnerNodes(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Selection) wrapInnerNodes(ns ...*html.Node) *Selection {
|
||||||
|
if len(ns) == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Each(func(i int, s *Selection) {
|
||||||
|
contents := s.Contents()
|
||||||
|
|
||||||
|
if contents.Size() > 0 {
|
||||||
|
contents.wrapAllNodes(ns...)
|
||||||
|
} else {
|
||||||
|
s.AppendNodes(cloneNode(ns[0]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHtml(h string) []*html.Node {
|
||||||
|
// Errors are only returned when the io.Reader returns any error besides
|
||||||
|
// EOF, but strings.Reader never will
|
||||||
|
nodes, err := html.ParseFragment(strings.NewReader(h), &html.Node{Type: html.ElementNode})
|
||||||
|
if err != nil {
|
||||||
|
panic("goquery: failed to parse HTML: " + err.Error())
|
||||||
|
}
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHtmlWithContext(h string, context *html.Node) []*html.Node {
|
||||||
|
// Errors are only returned when the io.Reader returns any error besides
|
||||||
|
// EOF, but strings.Reader never will
|
||||||
|
nodes, err := html.ParseFragment(strings.NewReader(h), context)
|
||||||
|
if err != nil {
|
||||||
|
panic("goquery: failed to parse HTML: " + err.Error())
|
||||||
|
}
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first child that is an ElementNode
|
||||||
|
func getFirstChildEl(n *html.Node) *html.Node {
|
||||||
|
c := n.FirstChild
|
||||||
|
for c != nil && c.Type != html.ElementNode {
|
||||||
|
c = c.NextSibling
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep copy a slice of nodes.
|
||||||
|
func cloneNodes(ns []*html.Node) []*html.Node {
|
||||||
|
cns := make([]*html.Node, 0, len(ns))
|
||||||
|
|
||||||
|
for _, n := range ns {
|
||||||
|
cns = append(cns, cloneNode(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep copy a node. The new node has clones of all the original node's
|
||||||
|
// children but none of its parents or siblings.
|
||||||
|
func cloneNode(n *html.Node) *html.Node {
|
||||||
|
nn := &html.Node{
|
||||||
|
Type: n.Type,
|
||||||
|
DataAtom: n.DataAtom,
|
||||||
|
Data: n.Data,
|
||||||
|
Attr: make([]html.Attribute, len(n.Attr)),
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(nn.Attr, n.Attr)
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
nn.AppendChild(cloneNode(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Selection) manipulateNodes(ns []*html.Node, reverse bool,
|
||||||
|
f func(sn *html.Node, n *html.Node)) *Selection {
|
||||||
|
|
||||||
|
lasti := s.Size() - 1
|
||||||
|
|
||||||
|
// net.Html doesn't provide document fragments for insertion, so to get
|
||||||
|
// things in the correct order with After() and Prepend(), the callback
|
||||||
|
// needs to be called on the reverse of the nodes.
|
||||||
|
if reverse {
|
||||||
|
for i, j := 0, len(ns)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
ns[i], ns[j] = ns[j], ns[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, sn := range s.Nodes {
|
||||||
|
for _, n := range ns {
|
||||||
|
if i != lasti {
|
||||||
|
f(sn, cloneNode(n))
|
||||||
|
} else {
|
||||||
|
if n.Parent != nil {
|
||||||
|
n.Parent.RemoveChild(n)
|
||||||
|
}
|
||||||
|
f(sn, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// eachNodeHtml parses the given html string and inserts the resulting nodes in the dom with the mergeFn.
|
||||||
|
// The parsed nodes are inserted for each element of the selection.
|
||||||
|
// isParent can be used to indicate that the elements of the selection should be treated as the parent for the parsed html.
|
||||||
|
// A cache is used to avoid parsing the html multiple times should the elements of the selection result in the same context.
|
||||||
|
func (s *Selection) eachNodeHtml(htmlStr string, isParent bool, mergeFn func(n *html.Node, nodes []*html.Node)) *Selection {
|
||||||
|
// cache to avoid parsing the html for the same context multiple times
|
||||||
|
nodeCache := make(map[string][]*html.Node)
|
||||||
|
var context *html.Node
|
||||||
|
for _, n := range s.Nodes {
|
||||||
|
if isParent {
|
||||||
|
context = n.Parent
|
||||||
|
} else {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
context = n
|
||||||
|
}
|
||||||
|
if context != nil {
|
||||||
|
nodes, found := nodeCache[nodeName(context)]
|
||||||
|
if !found {
|
||||||
|
nodes = parseHtmlWithContext(htmlStr, context)
|
||||||
|
nodeCache[nodeName(context)] = nodes
|
||||||
|
}
|
||||||
|
mergeFn(n, cloneNodes(nodes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
275
vendor/github.com/PuerkitoBio/goquery/property.go
generated
vendored
Normal file
275
vendor/github.com/PuerkitoBio/goquery/property.go
generated
vendored
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
package goquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rxClassTrim = regexp.MustCompile("[\t\r\n]")
|
||||||
|
|
||||||
|
// Attr gets the specified attribute's value for the first element in the
|
||||||
|
// Selection. To get the value for each element individually, use a looping
|
||||||
|
// construct such as Each or Map method.
|
||||||
|
func (s *Selection) Attr(attrName string) (val string, exists bool) {
|
||||||
|
if len(s.Nodes) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return getAttributeValue(attrName, s.Nodes[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttrOr works like Attr but returns default value if attribute is not present.
|
||||||
|
func (s *Selection) AttrOr(attrName, defaultValue string) string {
|
||||||
|
if len(s.Nodes) == 0 {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
val, exists := getAttributeValue(attrName, s.Nodes[0])
|
||||||
|
if !exists {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAttr removes the named attribute from each element in the set of matched elements.
|
||||||
|
func (s *Selection) RemoveAttr(attrName string) *Selection {
|
||||||
|
for _, n := range s.Nodes {
|
||||||
|
removeAttr(n, attrName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAttr sets the given attribute on each element in the set of matched elements.
|
||||||
|
func (s *Selection) SetAttr(attrName, val string) *Selection {
|
||||||
|
for _, n := range s.Nodes {
|
||||||
|
attr := getAttributePtr(attrName, n)
|
||||||
|
if attr == nil {
|
||||||
|
n.Attr = append(n.Attr, html.Attribute{Key: attrName, Val: val})
|
||||||
|
} else {
|
||||||
|
attr.Val = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text gets the combined text contents of each element in the set of matched
|
||||||
|
// elements, including their descendants.
|
||||||
|
func (s *Selection) Text() string {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
// Slightly optimized vs calling Each: no single selection object created
|
||||||
|
var f func(*html.Node)
|
||||||
|
f = func(n *html.Node) {
|
||||||
|
if n.Type == html.TextNode {
|
||||||
|
// Keep newlines and spaces, like jQuery
|
||||||
|
builder.WriteString(n.Data)
|
||||||
|
}
|
||||||
|
if n.FirstChild != nil {
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
f(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, n := range s.Nodes {
|
||||||
|
f(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size is an alias for Length.
|
||||||
|
func (s *Selection) Size() int {
|
||||||
|
return s.Length()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length returns the number of elements in the Selection object.
|
||||||
|
func (s *Selection) Length() int {
|
||||||
|
return len(s.Nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Html gets the HTML contents of the first element in the set of matched
|
||||||
|
// elements. It includes text and comment nodes.
|
||||||
|
func (s *Selection) Html() (ret string, e error) {
|
||||||
|
// Since there is no .innerHtml, the HTML content must be re-created from
|
||||||
|
// the nodes using html.Render.
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
if len(s.Nodes) > 0 {
|
||||||
|
for c := s.Nodes[0].FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
e = html.Render(&builder, c)
|
||||||
|
if e != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret = builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddClass adds the given class(es) to each element in the set of matched elements.
|
||||||
|
// Multiple class names can be specified, separated by a space or via multiple arguments.
|
||||||
|
func (s *Selection) AddClass(class ...string) *Selection {
|
||||||
|
classStr := strings.TrimSpace(strings.Join(class, " "))
|
||||||
|
|
||||||
|
if classStr == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
tcls := getClassesSlice(classStr)
|
||||||
|
for _, n := range s.Nodes {
|
||||||
|
curClasses, attr := getClassesAndAttr(n, true)
|
||||||
|
for _, newClass := range tcls {
|
||||||
|
if !strings.Contains(curClasses, " "+newClass+" ") {
|
||||||
|
curClasses += newClass + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setClasses(n, attr, curClasses)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasClass determines whether any of the matched elements are assigned the
|
||||||
|
// given class.
|
||||||
|
func (s *Selection) HasClass(class string) bool {
|
||||||
|
class = " " + class + " "
|
||||||
|
for _, n := range s.Nodes {
|
||||||
|
classes, _ := getClassesAndAttr(n, false)
|
||||||
|
if strings.Contains(classes, class) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveClass removes the given class(es) from each element in the set of matched elements.
|
||||||
|
// Multiple class names can be specified, separated by a space or via multiple arguments.
|
||||||
|
// If no class name is provided, all classes are removed.
|
||||||
|
func (s *Selection) RemoveClass(class ...string) *Selection {
|
||||||
|
var rclasses []string
|
||||||
|
|
||||||
|
classStr := strings.TrimSpace(strings.Join(class, " "))
|
||||||
|
remove := classStr == ""
|
||||||
|
|
||||||
|
if !remove {
|
||||||
|
rclasses = getClassesSlice(classStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range s.Nodes {
|
||||||
|
if remove {
|
||||||
|
removeAttr(n, "class")
|
||||||
|
} else {
|
||||||
|
classes, attr := getClassesAndAttr(n, true)
|
||||||
|
for _, rcl := range rclasses {
|
||||||
|
classes = strings.ReplaceAll(classes, " "+rcl+" ", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
setClasses(n, attr, classes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToggleClass adds or removes the given class(es) for each element in the set of matched elements.
|
||||||
|
// Multiple class names can be specified, separated by a space or via multiple arguments.
|
||||||
|
func (s *Selection) ToggleClass(class ...string) *Selection {
|
||||||
|
classStr := strings.TrimSpace(strings.Join(class, " "))
|
||||||
|
|
||||||
|
if classStr == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
tcls := getClassesSlice(classStr)
|
||||||
|
|
||||||
|
for _, n := range s.Nodes {
|
||||||
|
classes, attr := getClassesAndAttr(n, true)
|
||||||
|
for _, tcl := range tcls {
|
||||||
|
spaceAroundTcl := " " + tcl + " "
|
||||||
|
if strings.Contains(classes, spaceAroundTcl) {
|
||||||
|
classes = strings.ReplaceAll(classes, spaceAroundTcl, " ")
|
||||||
|
} else {
|
||||||
|
classes += tcl + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setClasses(n, attr, classes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAttributePtr(attrName string, n *html.Node) *html.Attribute {
|
||||||
|
if n == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, a := range n.Attr {
|
||||||
|
if a.Key == attrName {
|
||||||
|
return &n.Attr[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private function to get the specified attribute's value from a node.
|
||||||
|
func getAttributeValue(attrName string, n *html.Node) (val string, exists bool) {
|
||||||
|
if a := getAttributePtr(attrName, n); a != nil {
|
||||||
|
val = a.Val
|
||||||
|
exists = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and normalize the "class" attribute from the node.
|
||||||
|
func getClassesAndAttr(n *html.Node, create bool) (classes string, attr *html.Attribute) {
|
||||||
|
// Applies only to element nodes
|
||||||
|
if n.Type == html.ElementNode {
|
||||||
|
attr = getAttributePtr("class", n)
|
||||||
|
if attr == nil && create {
|
||||||
|
n.Attr = append(n.Attr, html.Attribute{
|
||||||
|
Key: "class",
|
||||||
|
Val: "",
|
||||||
|
})
|
||||||
|
attr = &n.Attr[len(n.Attr)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr == nil {
|
||||||
|
classes = " "
|
||||||
|
} else {
|
||||||
|
classes = rxClassTrim.ReplaceAllString(" "+attr.Val+" ", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClassesSlice(classes string) []string {
|
||||||
|
return strings.Split(rxClassTrim.ReplaceAllString(" "+classes+" ", " "), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeAttr(n *html.Node, attrName string) {
|
||||||
|
for i, a := range n.Attr {
|
||||||
|
if a.Key == attrName {
|
||||||
|
n.Attr[i], n.Attr[len(n.Attr)-1], n.Attr =
|
||||||
|
n.Attr[len(n.Attr)-1], html.Attribute{}, n.Attr[:len(n.Attr)-1]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setClasses(n *html.Node, attr *html.Attribute, classes string) {
|
||||||
|
classes = strings.TrimSpace(classes)
|
||||||
|
if classes == "" {
|
||||||
|
removeAttr(n, "class")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attr.Val = classes
|
||||||
|
}
|
||||||
49
vendor/github.com/PuerkitoBio/goquery/query.go
generated
vendored
Normal file
49
vendor/github.com/PuerkitoBio/goquery/query.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package goquery
|
||||||
|
|
||||||
|
import "golang.org/x/net/html"
|
||||||
|
|
||||||
|
// Is checks the current matched set of elements against a selector and
|
||||||
|
// returns true if at least one of these elements matches.
|
||||||
|
func (s *Selection) Is(selector string) bool {
|
||||||
|
return s.IsMatcher(compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMatcher checks the current matched set of elements against a matcher and
|
||||||
|
// returns true if at least one of these elements matches.
|
||||||
|
func (s *Selection) IsMatcher(m Matcher) bool {
|
||||||
|
if len(s.Nodes) > 0 {
|
||||||
|
if len(s.Nodes) == 1 {
|
||||||
|
return m.Match(s.Nodes[0])
|
||||||
|
}
|
||||||
|
return len(m.Filter(s.Nodes)) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFunction checks the current matched set of elements against a predicate and
|
||||||
|
// returns true if at least one of these elements matches.
|
||||||
|
func (s *Selection) IsFunction(f func(int, *Selection) bool) bool {
|
||||||
|
return s.FilterFunction(f).Length() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSelection checks the current matched set of elements against a Selection object
|
||||||
|
// and returns true if at least one of these elements matches.
|
||||||
|
func (s *Selection) IsSelection(sel *Selection) bool {
|
||||||
|
return s.FilterSelection(sel).Length() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNodes checks the current matched set of elements against the specified nodes
|
||||||
|
// and returns true if at least one of these elements matches.
|
||||||
|
func (s *Selection) IsNodes(nodes ...*html.Node) bool {
|
||||||
|
return s.FilterNodes(nodes...).Length() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains returns true if the specified Node is within,
|
||||||
|
// at any depth, one of the nodes in the Selection object.
|
||||||
|
// It is NOT inclusive, to behave like jQuery's implementation, and
|
||||||
|
// unlike Javascript's .contains, so if the contained
|
||||||
|
// node is itself in the selection, it returns false.
|
||||||
|
func (s *Selection) Contains(n *html.Node) bool {
|
||||||
|
return sliceContains(s.Nodes, n)
|
||||||
|
}
|
||||||
704
vendor/github.com/PuerkitoBio/goquery/traversal.go
generated
vendored
Normal file
704
vendor/github.com/PuerkitoBio/goquery/traversal.go
generated
vendored
Normal file
@@ -0,0 +1,704 @@
|
|||||||
|
package goquery
|
||||||
|
|
||||||
|
import "golang.org/x/net/html"
|
||||||
|
|
||||||
|
type siblingType int
|
||||||
|
|
||||||
|
// Sibling type, used internally when iterating over children at the same
|
||||||
|
// level (siblings) to specify which nodes are requested.
|
||||||
|
const (
|
||||||
|
siblingPrevUntil siblingType = iota - 3
|
||||||
|
siblingPrevAll
|
||||||
|
siblingPrev
|
||||||
|
siblingAll
|
||||||
|
siblingNext
|
||||||
|
siblingNextAll
|
||||||
|
siblingNextUntil
|
||||||
|
siblingAllIncludingNonElements
|
||||||
|
)
|
||||||
|
|
||||||
|
// Find gets the descendants of each element in the current set of matched
|
||||||
|
// elements, filtered by a selector. It returns a new Selection object
|
||||||
|
// containing these matched elements.
|
||||||
|
//
|
||||||
|
// Note that as for all methods accepting a selector string, the selector is
|
||||||
|
// compiled and applied by the cascadia package and inherits its behavior and
|
||||||
|
// constraints regarding supported selectors. See the note on cascadia in
|
||||||
|
// the goquery documentation here:
|
||||||
|
// https://github.com/PuerkitoBio/goquery?tab=readme-ov-file#api
|
||||||
|
func (s *Selection) Find(selector string) *Selection {
|
||||||
|
return pushStack(s, findWithMatcher(s.Nodes, compileMatcher(selector)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindMatcher gets the descendants of each element in the current set of matched
|
||||||
|
// elements, filtered by the matcher. It returns a new Selection object
|
||||||
|
// containing these matched elements.
|
||||||
|
func (s *Selection) FindMatcher(m Matcher) *Selection {
|
||||||
|
return pushStack(s, findWithMatcher(s.Nodes, m))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindSelection gets the descendants of each element in the current
|
||||||
|
// Selection, filtered by a Selection. It returns a new Selection object
|
||||||
|
// containing these matched elements.
|
||||||
|
func (s *Selection) FindSelection(sel *Selection) *Selection {
|
||||||
|
if sel == nil {
|
||||||
|
return pushStack(s, nil)
|
||||||
|
}
|
||||||
|
return s.FindNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindNodes gets the descendants of each element in the current
|
||||||
|
// Selection, filtered by some nodes. It returns a new Selection object
|
||||||
|
// containing these matched elements.
|
||||||
|
func (s *Selection) FindNodes(nodes ...*html.Node) *Selection {
|
||||||
|
return pushStack(s, mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
|
||||||
|
if sliceContains(s.Nodes, n) {
|
||||||
|
return []*html.Node{n}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contents gets the children of each element in the Selection,
|
||||||
|
// including text and comment nodes. It returns a new Selection object
|
||||||
|
// containing these elements.
|
||||||
|
func (s *Selection) Contents() *Selection {
|
||||||
|
return pushStack(s, getChildrenNodes(s.Nodes, siblingAllIncludingNonElements))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentsFiltered gets the children of each element in the Selection,
|
||||||
|
// filtered by the specified selector. It returns a new Selection
|
||||||
|
// object containing these elements. Since selectors only act on Element nodes,
|
||||||
|
// this function is an alias to ChildrenFiltered unless the selector is empty,
|
||||||
|
// in which case it is an alias to Contents.
|
||||||
|
func (s *Selection) ContentsFiltered(selector string) *Selection {
|
||||||
|
if selector != "" {
|
||||||
|
return s.ChildrenFiltered(selector)
|
||||||
|
}
|
||||||
|
return s.Contents()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentsMatcher gets the children of each element in the Selection,
|
||||||
|
// filtered by the specified matcher. It returns a new Selection
|
||||||
|
// object containing these elements. Since matchers only act on Element nodes,
|
||||||
|
// this function is an alias to ChildrenMatcher.
|
||||||
|
func (s *Selection) ContentsMatcher(m Matcher) *Selection {
|
||||||
|
return s.ChildrenMatcher(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Children gets the child elements of each element in the Selection.
|
||||||
|
// It returns a new Selection object containing these elements.
|
||||||
|
func (s *Selection) Children() *Selection {
|
||||||
|
return pushStack(s, getChildrenNodes(s.Nodes, siblingAll))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChildrenFiltered gets the child elements of each element in the Selection,
|
||||||
|
// filtered by the specified selector. It returns a new
|
||||||
|
// Selection object containing these elements.
|
||||||
|
func (s *Selection) ChildrenFiltered(selector string) *Selection {
|
||||||
|
return filterAndPush(s, getChildrenNodes(s.Nodes, siblingAll), compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChildrenMatcher gets the child elements of each element in the Selection,
|
||||||
|
// filtered by the specified matcher. It returns a new
|
||||||
|
// Selection object containing these elements.
|
||||||
|
func (s *Selection) ChildrenMatcher(m Matcher) *Selection {
|
||||||
|
return filterAndPush(s, getChildrenNodes(s.Nodes, siblingAll), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent gets the parent of each element in the Selection. It returns a
|
||||||
|
// new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) Parent() *Selection {
|
||||||
|
return pushStack(s, getParentNodes(s.Nodes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentFiltered gets the parent of each element in the Selection filtered by a
|
||||||
|
// selector. It returns a new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) ParentFiltered(selector string) *Selection {
|
||||||
|
return filterAndPush(s, getParentNodes(s.Nodes), compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentMatcher gets the parent of each element in the Selection filtered by a
|
||||||
|
// matcher. It returns a new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) ParentMatcher(m Matcher) *Selection {
|
||||||
|
return filterAndPush(s, getParentNodes(s.Nodes), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closest gets the first element that matches the selector by testing the
|
||||||
|
// element itself and traversing up through its ancestors in the DOM tree.
|
||||||
|
func (s *Selection) Closest(selector string) *Selection {
|
||||||
|
cs := compileMatcher(selector)
|
||||||
|
return s.ClosestMatcher(cs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClosestMatcher gets the first element that matches the matcher by testing the
|
||||||
|
// element itself and traversing up through its ancestors in the DOM tree.
|
||||||
|
func (s *Selection) ClosestMatcher(m Matcher) *Selection {
|
||||||
|
return pushStack(s, mapNodes(s.Nodes, func(i int, n *html.Node) []*html.Node {
|
||||||
|
// For each node in the selection, test the node itself, then each parent
|
||||||
|
// until a match is found.
|
||||||
|
for ; n != nil; n = n.Parent {
|
||||||
|
if m.Match(n) {
|
||||||
|
return []*html.Node{n}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClosestNodes gets the first element that matches one of the nodes by testing the
|
||||||
|
// element itself and traversing up through its ancestors in the DOM tree.
|
||||||
|
func (s *Selection) ClosestNodes(nodes ...*html.Node) *Selection {
|
||||||
|
set := make(map[*html.Node]bool)
|
||||||
|
for _, n := range nodes {
|
||||||
|
set[n] = true
|
||||||
|
}
|
||||||
|
return pushStack(s, mapNodes(s.Nodes, func(i int, n *html.Node) []*html.Node {
|
||||||
|
// For each node in the selection, test the node itself, then each parent
|
||||||
|
// until a match is found.
|
||||||
|
for ; n != nil; n = n.Parent {
|
||||||
|
if set[n] {
|
||||||
|
return []*html.Node{n}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClosestSelection gets the first element that matches one of the nodes in the
|
||||||
|
// Selection by testing the element itself and traversing up through its ancestors
|
||||||
|
// in the DOM tree.
|
||||||
|
func (s *Selection) ClosestSelection(sel *Selection) *Selection {
|
||||||
|
if sel == nil {
|
||||||
|
return pushStack(s, nil)
|
||||||
|
}
|
||||||
|
return s.ClosestNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parents gets the ancestors of each element in the current Selection. It
|
||||||
|
// returns a new Selection object with the matched elements.
|
||||||
|
func (s *Selection) Parents() *Selection {
|
||||||
|
return pushStack(s, getParentsNodes(s.Nodes, nil, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentsFiltered gets the ancestors of each element in the current
|
||||||
|
// Selection. It returns a new Selection object with the matched elements.
|
||||||
|
func (s *Selection) ParentsFiltered(selector string) *Selection {
|
||||||
|
return filterAndPush(s, getParentsNodes(s.Nodes, nil, nil), compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentsMatcher gets the ancestors of each element in the current
|
||||||
|
// Selection. It returns a new Selection object with the matched elements.
|
||||||
|
func (s *Selection) ParentsMatcher(m Matcher) *Selection {
|
||||||
|
return filterAndPush(s, getParentsNodes(s.Nodes, nil, nil), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentsUntil gets the ancestors of each element in the Selection, up to but
|
||||||
|
// not including the element matched by the selector. It returns a new Selection
|
||||||
|
// object containing the matched elements.
|
||||||
|
func (s *Selection) ParentsUntil(selector string) *Selection {
|
||||||
|
return pushStack(s, getParentsNodes(s.Nodes, compileMatcher(selector), nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentsUntilMatcher gets the ancestors of each element in the Selection, up to but
|
||||||
|
// not including the element matched by the matcher. It returns a new Selection
|
||||||
|
// object containing the matched elements.
|
||||||
|
func (s *Selection) ParentsUntilMatcher(m Matcher) *Selection {
|
||||||
|
return pushStack(s, getParentsNodes(s.Nodes, m, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentsUntilSelection gets the ancestors of each element in the Selection,
|
||||||
|
// up to but not including the elements in the specified Selection. It returns a
|
||||||
|
// new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) ParentsUntilSelection(sel *Selection) *Selection {
|
||||||
|
if sel == nil {
|
||||||
|
return s.Parents()
|
||||||
|
}
|
||||||
|
return s.ParentsUntilNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentsUntilNodes gets the ancestors of each element in the Selection,
|
||||||
|
// up to but not including the specified nodes. It returns a
|
||||||
|
// new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) ParentsUntilNodes(nodes ...*html.Node) *Selection {
|
||||||
|
return pushStack(s, getParentsNodes(s.Nodes, nil, nodes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentsFilteredUntil is like ParentsUntil, with the option to filter the
|
||||||
|
// results based on a selector string. It returns a new Selection
|
||||||
|
// object containing the matched elements.
|
||||||
|
func (s *Selection) ParentsFilteredUntil(filterSelector, untilSelector string) *Selection {
|
||||||
|
return filterAndPush(s, getParentsNodes(s.Nodes, compileMatcher(untilSelector), nil), compileMatcher(filterSelector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentsFilteredUntilMatcher is like ParentsUntilMatcher, with the option to filter the
|
||||||
|
// results based on a matcher. It returns a new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) ParentsFilteredUntilMatcher(filter, until Matcher) *Selection {
|
||||||
|
return filterAndPush(s, getParentsNodes(s.Nodes, until, nil), filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentsFilteredUntilSelection is like ParentsUntilSelection, with the
|
||||||
|
// option to filter the results based on a selector string. It returns a new
|
||||||
|
// Selection object containing the matched elements.
|
||||||
|
func (s *Selection) ParentsFilteredUntilSelection(filterSelector string, sel *Selection) *Selection {
|
||||||
|
return s.ParentsMatcherUntilSelection(compileMatcher(filterSelector), sel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentsMatcherUntilSelection is like ParentsUntilSelection, with the
|
||||||
|
// option to filter the results based on a matcher. It returns a new
|
||||||
|
// Selection object containing the matched elements.
|
||||||
|
func (s *Selection) ParentsMatcherUntilSelection(filter Matcher, sel *Selection) *Selection {
|
||||||
|
if sel == nil {
|
||||||
|
return s.ParentsMatcher(filter)
|
||||||
|
}
|
||||||
|
return s.ParentsMatcherUntilNodes(filter, sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentsFilteredUntilNodes is like ParentsUntilNodes, with the
|
||||||
|
// option to filter the results based on a selector string. It returns a new
|
||||||
|
// Selection object containing the matched elements.
|
||||||
|
func (s *Selection) ParentsFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection {
|
||||||
|
return filterAndPush(s, getParentsNodes(s.Nodes, nil, nodes), compileMatcher(filterSelector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentsMatcherUntilNodes is like ParentsUntilNodes, with the
|
||||||
|
// option to filter the results based on a matcher. It returns a new
|
||||||
|
// Selection object containing the matched elements.
|
||||||
|
func (s *Selection) ParentsMatcherUntilNodes(filter Matcher, nodes ...*html.Node) *Selection {
|
||||||
|
return filterAndPush(s, getParentsNodes(s.Nodes, nil, nodes), filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Siblings gets the siblings of each element in the Selection. It returns
|
||||||
|
// a new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) Siblings() *Selection {
|
||||||
|
return pushStack(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SiblingsFiltered gets the siblings of each element in the Selection
|
||||||
|
// filtered by a selector. It returns a new Selection object containing the
|
||||||
|
// matched elements.
|
||||||
|
func (s *Selection) SiblingsFiltered(selector string) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil), compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SiblingsMatcher gets the siblings of each element in the Selection
|
||||||
|
// filtered by a matcher. It returns a new Selection object containing the
|
||||||
|
// matched elements.
|
||||||
|
func (s *Selection) SiblingsMatcher(m Matcher) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next gets the immediately following sibling of each element in the
|
||||||
|
// Selection. It returns a new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) Next() *Selection {
|
||||||
|
return pushStack(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextFiltered gets the immediately following sibling of each element in the
|
||||||
|
// Selection filtered by a selector. It returns a new Selection object
|
||||||
|
// containing the matched elements.
|
||||||
|
func (s *Selection) NextFiltered(selector string) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil), compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextMatcher gets the immediately following sibling of each element in the
|
||||||
|
// Selection filtered by a matcher. It returns a new Selection object
|
||||||
|
// containing the matched elements.
|
||||||
|
func (s *Selection) NextMatcher(m Matcher) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextAll gets all the following siblings of each element in the
|
||||||
|
// Selection. It returns a new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) NextAll() *Selection {
|
||||||
|
return pushStack(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextAllFiltered gets all the following siblings of each element in the
|
||||||
|
// Selection filtered by a selector. It returns a new Selection object
|
||||||
|
// containing the matched elements.
|
||||||
|
func (s *Selection) NextAllFiltered(selector string) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil), compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextAllMatcher gets all the following siblings of each element in the
|
||||||
|
// Selection filtered by a matcher. It returns a new Selection object
|
||||||
|
// containing the matched elements.
|
||||||
|
func (s *Selection) NextAllMatcher(m Matcher) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prev gets the immediately preceding sibling of each element in the
|
||||||
|
// Selection. It returns a new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) Prev() *Selection {
|
||||||
|
return pushStack(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevFiltered gets the immediately preceding sibling of each element in the
|
||||||
|
// Selection filtered by a selector. It returns a new Selection object
|
||||||
|
// containing the matched elements.
|
||||||
|
func (s *Selection) PrevFiltered(selector string) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil), compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevMatcher gets the immediately preceding sibling of each element in the
|
||||||
|
// Selection filtered by a matcher. It returns a new Selection object
|
||||||
|
// containing the matched elements.
|
||||||
|
func (s *Selection) PrevMatcher(m Matcher) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevAll gets all the preceding siblings of each element in the
|
||||||
|
// Selection. It returns a new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) PrevAll() *Selection {
|
||||||
|
return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevAllFiltered gets all the preceding siblings of each element in the
|
||||||
|
// Selection filtered by a selector. It returns a new Selection object
|
||||||
|
// containing the matched elements.
|
||||||
|
func (s *Selection) PrevAllFiltered(selector string) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil), compileMatcher(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevAllMatcher gets all the preceding siblings of each element in the
|
||||||
|
// Selection filtered by a matcher. It returns a new Selection object
|
||||||
|
// containing the matched elements.
|
||||||
|
func (s *Selection) PrevAllMatcher(m Matcher) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextUntil gets all following siblings of each element up to but not
|
||||||
|
// including the element matched by the selector. It returns a new Selection
|
||||||
|
// object containing the matched elements.
|
||||||
|
func (s *Selection) NextUntil(selector string) *Selection {
|
||||||
|
return pushStack(s, getSiblingNodes(s.Nodes, siblingNextUntil,
|
||||||
|
compileMatcher(selector), nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextUntilMatcher gets all following siblings of each element up to but not
|
||||||
|
// including the element matched by the matcher. It returns a new Selection
|
||||||
|
// object containing the matched elements.
|
||||||
|
func (s *Selection) NextUntilMatcher(m Matcher) *Selection {
|
||||||
|
return pushStack(s, getSiblingNodes(s.Nodes, siblingNextUntil,
|
||||||
|
m, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextUntilSelection gets all following siblings of each element up to but not
|
||||||
|
// including the element matched by the Selection. It returns a new Selection
|
||||||
|
// object containing the matched elements.
|
||||||
|
func (s *Selection) NextUntilSelection(sel *Selection) *Selection {
|
||||||
|
if sel == nil {
|
||||||
|
return s.NextAll()
|
||||||
|
}
|
||||||
|
return s.NextUntilNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextUntilNodes gets all following siblings of each element up to but not
|
||||||
|
// including the element matched by the nodes. It returns a new Selection
|
||||||
|
// object containing the matched elements.
|
||||||
|
func (s *Selection) NextUntilNodes(nodes ...*html.Node) *Selection {
|
||||||
|
return pushStack(s, getSiblingNodes(s.Nodes, siblingNextUntil,
|
||||||
|
nil, nodes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevUntil gets all preceding siblings of each element up to but not
|
||||||
|
// including the element matched by the selector. It returns a new Selection
|
||||||
|
// object containing the matched elements.
|
||||||
|
func (s *Selection) PrevUntil(selector string) *Selection {
|
||||||
|
return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
|
||||||
|
compileMatcher(selector), nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevUntilMatcher gets all preceding siblings of each element up to but not
|
||||||
|
// including the element matched by the matcher. It returns a new Selection
|
||||||
|
// object containing the matched elements.
|
||||||
|
func (s *Selection) PrevUntilMatcher(m Matcher) *Selection {
|
||||||
|
return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
|
||||||
|
m, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevUntilSelection gets all preceding siblings of each element up to but not
|
||||||
|
// including the element matched by the Selection. It returns a new Selection
|
||||||
|
// object containing the matched elements.
|
||||||
|
func (s *Selection) PrevUntilSelection(sel *Selection) *Selection {
|
||||||
|
if sel == nil {
|
||||||
|
return s.PrevAll()
|
||||||
|
}
|
||||||
|
return s.PrevUntilNodes(sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevUntilNodes gets all preceding siblings of each element up to but not
|
||||||
|
// including the element matched by the nodes. It returns a new Selection
|
||||||
|
// object containing the matched elements.
|
||||||
|
func (s *Selection) PrevUntilNodes(nodes ...*html.Node) *Selection {
|
||||||
|
return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
|
||||||
|
nil, nodes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextFilteredUntil is like NextUntil, with the option to filter
|
||||||
|
// the results based on a selector string.
|
||||||
|
// It returns a new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) NextFilteredUntil(filterSelector, untilSelector string) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
|
||||||
|
compileMatcher(untilSelector), nil), compileMatcher(filterSelector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextFilteredUntilMatcher is like NextUntilMatcher, with the option to filter
|
||||||
|
// the results based on a matcher.
|
||||||
|
// It returns a new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) NextFilteredUntilMatcher(filter, until Matcher) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
|
||||||
|
until, nil), filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextFilteredUntilSelection is like NextUntilSelection, with the
|
||||||
|
// option to filter the results based on a selector string. It returns a new
|
||||||
|
// Selection object containing the matched elements.
|
||||||
|
func (s *Selection) NextFilteredUntilSelection(filterSelector string, sel *Selection) *Selection {
|
||||||
|
return s.NextMatcherUntilSelection(compileMatcher(filterSelector), sel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextMatcherUntilSelection is like NextUntilSelection, with the
|
||||||
|
// option to filter the results based on a matcher. It returns a new
|
||||||
|
// Selection object containing the matched elements.
|
||||||
|
func (s *Selection) NextMatcherUntilSelection(filter Matcher, sel *Selection) *Selection {
|
||||||
|
if sel == nil {
|
||||||
|
return s.NextMatcher(filter)
|
||||||
|
}
|
||||||
|
return s.NextMatcherUntilNodes(filter, sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextFilteredUntilNodes is like NextUntilNodes, with the
|
||||||
|
// option to filter the results based on a selector string. It returns a new
|
||||||
|
// Selection object containing the matched elements.
|
||||||
|
func (s *Selection) NextFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
|
||||||
|
nil, nodes), compileMatcher(filterSelector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextMatcherUntilNodes is like NextUntilNodes, with the
|
||||||
|
// option to filter the results based on a matcher. It returns a new
|
||||||
|
// Selection object containing the matched elements.
|
||||||
|
func (s *Selection) NextMatcherUntilNodes(filter Matcher, nodes ...*html.Node) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
|
||||||
|
nil, nodes), filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevFilteredUntil is like PrevUntil, with the option to filter
|
||||||
|
// the results based on a selector string.
|
||||||
|
// It returns a new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) PrevFilteredUntil(filterSelector, untilSelector string) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
|
||||||
|
compileMatcher(untilSelector), nil), compileMatcher(filterSelector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevFilteredUntilMatcher is like PrevUntilMatcher, with the option to filter
|
||||||
|
// the results based on a matcher.
|
||||||
|
// It returns a new Selection object containing the matched elements.
|
||||||
|
func (s *Selection) PrevFilteredUntilMatcher(filter, until Matcher) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
|
||||||
|
until, nil), filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevFilteredUntilSelection is like PrevUntilSelection, with the
|
||||||
|
// option to filter the results based on a selector string. It returns a new
|
||||||
|
// Selection object containing the matched elements.
|
||||||
|
func (s *Selection) PrevFilteredUntilSelection(filterSelector string, sel *Selection) *Selection {
|
||||||
|
return s.PrevMatcherUntilSelection(compileMatcher(filterSelector), sel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevMatcherUntilSelection is like PrevUntilSelection, with the
|
||||||
|
// option to filter the results based on a matcher. It returns a new
|
||||||
|
// Selection object containing the matched elements.
|
||||||
|
func (s *Selection) PrevMatcherUntilSelection(filter Matcher, sel *Selection) *Selection {
|
||||||
|
if sel == nil {
|
||||||
|
return s.PrevMatcher(filter)
|
||||||
|
}
|
||||||
|
return s.PrevMatcherUntilNodes(filter, sel.Nodes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevFilteredUntilNodes is like PrevUntilNodes, with the
|
||||||
|
// option to filter the results based on a selector string. It returns a new
|
||||||
|
// Selection object containing the matched elements.
|
||||||
|
func (s *Selection) PrevFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
|
||||||
|
nil, nodes), compileMatcher(filterSelector))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevMatcherUntilNodes is like PrevUntilNodes, with the
|
||||||
|
// option to filter the results based on a matcher. It returns a new
|
||||||
|
// Selection object containing the matched elements.
|
||||||
|
func (s *Selection) PrevMatcherUntilNodes(filter Matcher, nodes ...*html.Node) *Selection {
|
||||||
|
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
|
||||||
|
nil, nodes), filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter and push filters the nodes based on a matcher, and pushes the results
|
||||||
|
// on the stack, with the srcSel as previous selection.
|
||||||
|
func filterAndPush(srcSel *Selection, nodes []*html.Node, m Matcher) *Selection {
|
||||||
|
// Create a temporary Selection with the specified nodes to filter using winnow
|
||||||
|
sel := &Selection{nodes, srcSel.document, nil}
|
||||||
|
// Filter based on matcher and push on stack
|
||||||
|
return pushStack(srcSel, winnow(sel, m, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal implementation of Find that return raw nodes.
|
||||||
|
func findWithMatcher(nodes []*html.Node, m Matcher) []*html.Node {
|
||||||
|
// Map nodes to find the matches within the children of each node
|
||||||
|
return mapNodes(nodes, func(i int, n *html.Node) (result []*html.Node) {
|
||||||
|
// Go down one level, becausejQuery's Find selects only within descendants
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
if c.Type == html.ElementNode {
|
||||||
|
result = append(result, m.MatchAll(c)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal implementation to get all parent nodes, stopping at the specified
|
||||||
|
// node (or nil if no stop).
|
||||||
|
func getParentsNodes(nodes []*html.Node, stopm Matcher, stopNodes []*html.Node) []*html.Node {
|
||||||
|
return mapNodes(nodes, func(i int, n *html.Node) (result []*html.Node) {
|
||||||
|
for p := n.Parent; p != nil; p = p.Parent {
|
||||||
|
sel := newSingleSelection(p, nil)
|
||||||
|
if stopm != nil {
|
||||||
|
if sel.IsMatcher(stopm) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if len(stopNodes) > 0 {
|
||||||
|
if sel.IsNodes(stopNodes...) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.Type == html.ElementNode {
|
||||||
|
result = append(result, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal implementation of sibling nodes that return a raw slice of matches.
|
||||||
|
func getSiblingNodes(nodes []*html.Node, st siblingType, untilm Matcher, untilNodes []*html.Node) []*html.Node {
|
||||||
|
var f func(*html.Node) bool
|
||||||
|
|
||||||
|
// If the requested siblings are ...Until, create the test function to
|
||||||
|
// determine if the until condition is reached (returns true if it is)
|
||||||
|
if st == siblingNextUntil || st == siblingPrevUntil {
|
||||||
|
f = func(n *html.Node) bool {
|
||||||
|
if untilm != nil {
|
||||||
|
// Matcher-based condition
|
||||||
|
sel := newSingleSelection(n, nil)
|
||||||
|
return sel.IsMatcher(untilm)
|
||||||
|
} else if len(untilNodes) > 0 {
|
||||||
|
// Nodes-based condition
|
||||||
|
sel := newSingleSelection(n, nil)
|
||||||
|
return sel.IsNodes(untilNodes...)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
|
||||||
|
return getChildrenWithSiblingType(n.Parent, st, n, f)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the children nodes of each node in the specified slice of nodes,
|
||||||
|
// based on the sibling type request.
|
||||||
|
func getChildrenNodes(nodes []*html.Node, st siblingType) []*html.Node {
|
||||||
|
return mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
|
||||||
|
return getChildrenWithSiblingType(n, st, nil, nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the children of the specified parent, based on the requested sibling
|
||||||
|
// type, skipping a specified node if required.
|
||||||
|
func getChildrenWithSiblingType(parent *html.Node, st siblingType, skipNode *html.Node,
|
||||||
|
untilFunc func(*html.Node) bool) (result []*html.Node) {
|
||||||
|
|
||||||
|
// Create the iterator function
|
||||||
|
var iter = func(cur *html.Node) (ret *html.Node) {
|
||||||
|
// Based on the sibling type requested, iterate the right way
|
||||||
|
for {
|
||||||
|
switch st {
|
||||||
|
case siblingAll, siblingAllIncludingNonElements:
|
||||||
|
if cur == nil {
|
||||||
|
// First iteration, start with first child of parent
|
||||||
|
// Skip node if required
|
||||||
|
if ret = parent.FirstChild; ret == skipNode && skipNode != nil {
|
||||||
|
ret = skipNode.NextSibling
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Skip node if required
|
||||||
|
if ret = cur.NextSibling; ret == skipNode && skipNode != nil {
|
||||||
|
ret = skipNode.NextSibling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case siblingPrev, siblingPrevAll, siblingPrevUntil:
|
||||||
|
if cur == nil {
|
||||||
|
// Start with previous sibling of the skip node
|
||||||
|
ret = skipNode.PrevSibling
|
||||||
|
} else {
|
||||||
|
ret = cur.PrevSibling
|
||||||
|
}
|
||||||
|
case siblingNext, siblingNextAll, siblingNextUntil:
|
||||||
|
if cur == nil {
|
||||||
|
// Start with next sibling of the skip node
|
||||||
|
ret = skipNode.NextSibling
|
||||||
|
} else {
|
||||||
|
ret = cur.NextSibling
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("Invalid sibling type.")
|
||||||
|
}
|
||||||
|
if ret == nil || ret.Type == html.ElementNode || st == siblingAllIncludingNonElements {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Not a valid node, try again from this one
|
||||||
|
cur = ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for c := iter(nil); c != nil; c = iter(c) {
|
||||||
|
// If this is an ...Until case, test before append (returns true
|
||||||
|
// if the until condition is reached)
|
||||||
|
if st == siblingNextUntil || st == siblingPrevUntil {
|
||||||
|
if untilFunc(c) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, c)
|
||||||
|
if st == siblingNext || st == siblingPrev {
|
||||||
|
// Only one node was requested (immediate next or previous), so exit
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal implementation of parent nodes that return a raw slice of Nodes.
|
||||||
|
func getParentNodes(nodes []*html.Node) []*html.Node {
|
||||||
|
return mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
|
||||||
|
if n.Parent != nil && n.Parent.Type == html.ElementNode {
|
||||||
|
return []*html.Node{n.Parent}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal map function used by many traversing methods. Takes the source nodes
|
||||||
|
// to iterate on and the mapping function that returns an array of nodes.
|
||||||
|
// Returns an array of nodes mapped by calling the callback function once for
|
||||||
|
// each node in the source nodes.
|
||||||
|
func mapNodes(nodes []*html.Node, f func(int, *html.Node) []*html.Node) (result []*html.Node) {
|
||||||
|
set := make(map[*html.Node]bool)
|
||||||
|
for i, n := range nodes {
|
||||||
|
if vals := f(i, n); len(vals) > 0 {
|
||||||
|
result = appendWithoutDuplicates(result, vals, set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
203
vendor/github.com/PuerkitoBio/goquery/type.go
generated
vendored
Normal file
203
vendor/github.com/PuerkitoBio/goquery/type.go
generated
vendored
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package goquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/andybalholm/cascadia"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Document represents an HTML document to be manipulated. Unlike jQuery, which
|
||||||
|
// is loaded as part of a DOM document, and thus acts upon its containing
|
||||||
|
// document, GoQuery doesn't know which HTML document to act upon. So it needs
|
||||||
|
// to be told, and that's what the Document class is for. It holds the root
|
||||||
|
// document node to manipulate, and can make selections on this document.
|
||||||
|
type Document struct {
|
||||||
|
*Selection
|
||||||
|
Url *url.URL
|
||||||
|
rootNode *html.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocumentFromNode is a Document constructor that takes a root html Node
|
||||||
|
// as argument.
|
||||||
|
func NewDocumentFromNode(root *html.Node) *Document {
|
||||||
|
return newDocument(root, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocument is a Document constructor that takes a string URL as argument.
|
||||||
|
// It loads the specified document, parses it, and stores the root Document
|
||||||
|
// node, ready to be manipulated.
|
||||||
|
//
|
||||||
|
// Deprecated: Use the net/http standard library package to make the request
|
||||||
|
// and validate the response before calling goquery.NewDocumentFromReader
|
||||||
|
// with the response's body.
|
||||||
|
func NewDocument(url string) (*Document, error) {
|
||||||
|
// Load the URL
|
||||||
|
res, e := http.Get(url)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
return NewDocumentFromResponse(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocumentFromReader returns a Document from an io.Reader.
|
||||||
|
// It returns an error as second value if the reader's data cannot be parsed
|
||||||
|
// as html. It does not check if the reader is also an io.Closer, the
|
||||||
|
// provided reader is never closed by this call. It is the responsibility
|
||||||
|
// of the caller to close it if required.
|
||||||
|
func NewDocumentFromReader(r io.Reader) (*Document, error) {
|
||||||
|
root, e := html.Parse(r)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
return newDocument(root, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocumentFromResponse is another Document constructor that takes an http response as argument.
|
||||||
|
// It loads the specified response's document, parses it, and stores the root Document
|
||||||
|
// node, ready to be manipulated. The response's body is closed on return.
|
||||||
|
//
|
||||||
|
// Deprecated: Use goquery.NewDocumentFromReader with the response's body.
|
||||||
|
func NewDocumentFromResponse(res *http.Response) (*Document, error) {
|
||||||
|
if res == nil {
|
||||||
|
return nil, errors.New("Response is nil")
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.Request == nil {
|
||||||
|
return nil, errors.New("Response.Request is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the HTML into nodes
|
||||||
|
root, e := html.Parse(res.Body)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and fill the document
|
||||||
|
return newDocument(root, res.Request.URL), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloneDocument creates a deep-clone of a document.
|
||||||
|
func CloneDocument(doc *Document) *Document {
|
||||||
|
return newDocument(cloneNode(doc.rootNode), doc.Url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private constructor, make sure all fields are correctly filled.
|
||||||
|
func newDocument(root *html.Node, url *url.URL) *Document {
|
||||||
|
// Create and fill the document
|
||||||
|
d := &Document{nil, url, root}
|
||||||
|
d.Selection = newSingleSelection(root, d)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selection represents a collection of nodes matching some criteria. The
|
||||||
|
// initial Selection can be created by using Document.Find, and then
|
||||||
|
// manipulated using the jQuery-like chainable syntax and methods.
|
||||||
|
type Selection struct {
|
||||||
|
Nodes []*html.Node
|
||||||
|
document *Document
|
||||||
|
prevSel *Selection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper constructor to create an empty selection
|
||||||
|
func newEmptySelection(doc *Document) *Selection {
|
||||||
|
return &Selection{nil, doc, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper constructor to create a selection of only one node
|
||||||
|
func newSingleSelection(node *html.Node, doc *Document) *Selection {
|
||||||
|
return &Selection{[]*html.Node{node}, doc, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matcher is an interface that defines the methods to match
|
||||||
|
// HTML nodes against a compiled selector string. Cascadia's
|
||||||
|
// Selector implements this interface.
|
||||||
|
type Matcher interface {
|
||||||
|
Match(*html.Node) bool
|
||||||
|
MatchAll(*html.Node) []*html.Node
|
||||||
|
Filter([]*html.Node) []*html.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single compiles a selector string to a Matcher that stops after the first
|
||||||
|
// match is found.
|
||||||
|
//
|
||||||
|
// By default, Selection.Find and other functions that accept a selector string
|
||||||
|
// to select nodes will use all matches corresponding to that selector. By
|
||||||
|
// using the Matcher returned by Single, at most the first match will be
|
||||||
|
// selected.
|
||||||
|
//
|
||||||
|
// For example, those two statements are semantically equivalent:
|
||||||
|
//
|
||||||
|
// sel1 := doc.Find("a").First()
|
||||||
|
// sel2 := doc.FindMatcher(goquery.Single("a"))
|
||||||
|
//
|
||||||
|
// The one using Single is optimized to be potentially much faster on large
|
||||||
|
// documents.
|
||||||
|
//
|
||||||
|
// Only the behaviour of the MatchAll method of the Matcher interface is
|
||||||
|
// altered compared to standard Matchers. This means that the single-selection
|
||||||
|
// property of the Matcher only applies for Selection methods where the Matcher
|
||||||
|
// is used to select nodes, not to filter or check if a node matches the
|
||||||
|
// Matcher - in those cases, the behaviour of the Matcher is unchanged (e.g.
|
||||||
|
// FilterMatcher(Single("div")) will still result in a Selection with multiple
|
||||||
|
// "div"s if there were many "div"s in the Selection to begin with).
|
||||||
|
func Single(selector string) Matcher {
|
||||||
|
return singleMatcher{compileMatcher(selector)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SingleMatcher returns a Matcher matches the same nodes as m, but that stops
|
||||||
|
// after the first match is found.
|
||||||
|
//
|
||||||
|
// See the documentation of function Single for more details.
|
||||||
|
func SingleMatcher(m Matcher) Matcher {
|
||||||
|
if _, ok := m.(singleMatcher); ok {
|
||||||
|
// m is already a singleMatcher
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
return singleMatcher{m}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileMatcher compiles the selector string s and returns
|
||||||
|
// the corresponding Matcher. If s is an invalid selector string,
|
||||||
|
// it returns a Matcher that fails all matches.
|
||||||
|
func compileMatcher(s string) Matcher {
|
||||||
|
cs, err := cascadia.Compile(s)
|
||||||
|
if err != nil {
|
||||||
|
return invalidMatcher{}
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
type singleMatcher struct {
|
||||||
|
Matcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m singleMatcher) MatchAll(n *html.Node) []*html.Node {
|
||||||
|
// Optimized version - stops finding at the first match (cascadia-compiled
|
||||||
|
// matchers all use this code path).
|
||||||
|
if mm, ok := m.Matcher.(interface{ MatchFirst(*html.Node) *html.Node }); ok {
|
||||||
|
node := mm.MatchFirst(n)
|
||||||
|
if node == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []*html.Node{node}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback version, for e.g. test mocks that don't provide the MatchFirst
|
||||||
|
// method.
|
||||||
|
nodes := m.Matcher.MatchAll(n)
|
||||||
|
if len(nodes) > 0 {
|
||||||
|
return nodes[:1:1]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalidMatcher is a Matcher that always fails to match.
|
||||||
|
type invalidMatcher struct{}
|
||||||
|
|
||||||
|
func (invalidMatcher) Match(n *html.Node) bool { return false }
|
||||||
|
func (invalidMatcher) MatchAll(n *html.Node) []*html.Node { return nil }
|
||||||
|
func (invalidMatcher) Filter(ns []*html.Node) []*html.Node { return nil }
|
||||||
177
vendor/github.com/PuerkitoBio/goquery/utilities.go
generated
vendored
Normal file
177
vendor/github.com/PuerkitoBio/goquery/utilities.go
generated
vendored
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package goquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// used to determine if a set (map[*html.Node]bool) should be used
|
||||||
|
// instead of iterating over a slice. The set uses more memory and
|
||||||
|
// is slower than slice iteration for small N.
|
||||||
|
const minNodesForSet = 1000
|
||||||
|
|
||||||
|
var nodeNames = []string{
|
||||||
|
html.ErrorNode: "#error",
|
||||||
|
html.TextNode: "#text",
|
||||||
|
html.DocumentNode: "#document",
|
||||||
|
html.CommentNode: "#comment",
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeName returns the node name of the first element in the selection.
|
||||||
|
// It tries to behave in a similar way as the DOM's nodeName property
|
||||||
|
// (https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeName).
|
||||||
|
//
|
||||||
|
// Go's net/html package defines the following node types, listed with
|
||||||
|
// the corresponding returned value from this function:
|
||||||
|
//
|
||||||
|
// ErrorNode : #error
|
||||||
|
// TextNode : #text
|
||||||
|
// DocumentNode : #document
|
||||||
|
// ElementNode : the element's tag name
|
||||||
|
// CommentNode : #comment
|
||||||
|
// DoctypeNode : the name of the document type
|
||||||
|
func NodeName(s *Selection) string {
|
||||||
|
if s.Length() == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return nodeName(s.Get(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodeName returns the node name of the given html node.
|
||||||
|
// See NodeName for additional details on behaviour.
|
||||||
|
func nodeName(node *html.Node) string {
|
||||||
|
if node == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch node.Type {
|
||||||
|
case html.ElementNode, html.DoctypeNode:
|
||||||
|
return node.Data
|
||||||
|
default:
|
||||||
|
if int(node.Type) < len(nodeNames) {
|
||||||
|
return nodeNames[node.Type]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the HTML of the first item in the selection and writes it to
|
||||||
|
// the writer. It behaves the same as OuterHtml but writes to w instead of
|
||||||
|
// returning the string.
|
||||||
|
func Render(w io.Writer, s *Selection) error {
|
||||||
|
if s.Length() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
n := s.Get(0)
|
||||||
|
return html.Render(w, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OuterHtml returns the outer HTML rendering of the first item in
|
||||||
|
// the selection - that is, the HTML including the first element's
|
||||||
|
// tag and attributes.
|
||||||
|
//
|
||||||
|
// Unlike Html, this is a function and not a method on the Selection,
|
||||||
|
// because this is not a jQuery method (in javascript-land, this is
|
||||||
|
// a property provided by the DOM).
|
||||||
|
func OuterHtml(s *Selection) (string, error) {
|
||||||
|
var builder strings.Builder
|
||||||
|
if err := Render(&builder, s); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return builder.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through all container nodes to search for the target node.
|
||||||
|
func sliceContains(container []*html.Node, contained *html.Node) bool {
|
||||||
|
for _, n := range container {
|
||||||
|
if nodeContains(n, contained) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the contained node is within the container node.
|
||||||
|
func nodeContains(container *html.Node, contained *html.Node) bool {
|
||||||
|
// Check if the parent of the contained node is the container node, traversing
|
||||||
|
// upward until the top is reached, or the container is found.
|
||||||
|
for contained = contained.Parent; contained != nil; contained = contained.Parent {
|
||||||
|
if container == contained {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the target node is in the slice of nodes.
|
||||||
|
func isInSlice(slice []*html.Node, node *html.Node) bool {
|
||||||
|
return indexInSlice(slice, node) > -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the index of the target node in the slice, or -1.
|
||||||
|
func indexInSlice(slice []*html.Node, node *html.Node) int {
|
||||||
|
if node != nil {
|
||||||
|
for i, n := range slice {
|
||||||
|
if n == node {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appends the new nodes to the target slice, making sure no duplicate is added.
|
||||||
|
// There is no check to the original state of the target slice, so it may still
|
||||||
|
// contain duplicates. The target slice is returned because append() may create
|
||||||
|
// a new underlying array. If targetSet is nil, a local set is created with the
|
||||||
|
// target if len(target) + len(nodes) is greater than minNodesForSet.
|
||||||
|
func appendWithoutDuplicates(target []*html.Node, nodes []*html.Node, targetSet map[*html.Node]bool) []*html.Node {
|
||||||
|
// if there are not that many nodes, don't use the map, faster to just use nested loops
|
||||||
|
// (unless a non-nil targetSet is passed, in which case the caller knows better).
|
||||||
|
if targetSet == nil && len(target)+len(nodes) < minNodesForSet {
|
||||||
|
for _, n := range nodes {
|
||||||
|
if !isInSlice(target, n) {
|
||||||
|
target = append(target, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
// if a targetSet is passed, then assume it is reliable, otherwise create one
|
||||||
|
// and initialize it with the current target contents.
|
||||||
|
if targetSet == nil {
|
||||||
|
targetSet = make(map[*html.Node]bool, len(target))
|
||||||
|
for _, n := range target {
|
||||||
|
targetSet[n] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, n := range nodes {
|
||||||
|
if !targetSet[n] {
|
||||||
|
target = append(target, n)
|
||||||
|
targetSet[n] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through a selection, returning only those nodes that pass the predicate
|
||||||
|
// function.
|
||||||
|
func grep(sel *Selection, predicate func(i int, s *Selection) bool) (result []*html.Node) {
|
||||||
|
for i, n := range sel.Nodes {
|
||||||
|
if predicate(i, newSingleSelection(n, sel.document)) {
|
||||||
|
result = append(result, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new Selection object based on the specified nodes, and keeps the
|
||||||
|
// source Selection object on the stack (linked list).
|
||||||
|
func pushStack(fromSel *Selection, nodes []*html.Node) *Selection {
|
||||||
|
result := &Selection{nodes, fromSel.document, fromSel}
|
||||||
|
return result
|
||||||
|
}
|
||||||
14
vendor/github.com/andybalholm/cascadia/.travis.yml
generated
vendored
Normal file
14
vendor/github.com/andybalholm/cascadia/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.3
|
||||||
|
- 1.4
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go get github.com/andybalholm/cascadia
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go test -v
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
email: false
|
||||||
24
vendor/github.com/andybalholm/cascadia/LICENSE
generated
vendored
Normal file
24
vendor/github.com/andybalholm/cascadia/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Copyright (c) 2011 Andy Balholm. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
144
vendor/github.com/andybalholm/cascadia/README.md
generated
vendored
Normal file
144
vendor/github.com/andybalholm/cascadia/README.md
generated
vendored
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# cascadia
|
||||||
|
|
||||||
|
[](https://travis-ci.org/andybalholm/cascadia)
|
||||||
|
|
||||||
|
The Cascadia package implements CSS selectors for use with the parse trees produced by the html package.
|
||||||
|
|
||||||
|
To test CSS selectors without writing Go code, check out [cascadia](https://github.com/suntong/cascadia) the command line tool, a thin wrapper around this package.
|
||||||
|
|
||||||
|
[Refer to godoc here](https://godoc.org/github.com/andybalholm/cascadia).
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
The following is an example of how you can use Cascadia.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/andybalholm/cascadia"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pricingHtml string = `
|
||||||
|
<div class="card mb-4 box-shadow">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="my-0 font-weight-normal">Free</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="card-title pricing-card-title">$0/mo</h1>
|
||||||
|
<ul class="list-unstyled mt-3 mb-4">
|
||||||
|
<li>10 users included</li>
|
||||||
|
<li>2 GB of storage</li>
|
||||||
|
<li><a href="https://example.com">See more</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4 box-shadow">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="my-0 font-weight-normal">Pro</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="card-title pricing-card-title">$15/mo</h1>
|
||||||
|
<ul class="list-unstyled mt-3 mb-4">
|
||||||
|
<li>20 users included</li>
|
||||||
|
<li>10 GB of storage</li>
|
||||||
|
<li><a href="https://example.com">See more</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4 box-shadow">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="my-0 font-weight-normal">Enterprise</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="card-title pricing-card-title">$29/mo</h1>
|
||||||
|
<ul class="list-unstyled mt-3 mb-4">
|
||||||
|
<li>30 users included</li>
|
||||||
|
<li>15 GB of storage</li>
|
||||||
|
<li><a>See more</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
func Query(n *html.Node, query string) *html.Node {
|
||||||
|
sel, err := cascadia.Parse(query)
|
||||||
|
if err != nil {
|
||||||
|
return &html.Node{}
|
||||||
|
}
|
||||||
|
return cascadia.Query(n, sel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func QueryAll(n *html.Node, query string) []*html.Node {
|
||||||
|
sel, err := cascadia.Parse(query)
|
||||||
|
if err != nil {
|
||||||
|
return []*html.Node{}
|
||||||
|
}
|
||||||
|
return cascadia.QueryAll(n, sel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AttrOr(n *html.Node, attrName, or string) string {
|
||||||
|
for _, a := range n.Attr {
|
||||||
|
if a.Key == attrName {
|
||||||
|
return a.Val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return or
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
doc, err := html.Parse(strings.NewReader(pricingHtml))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("List of pricing plans:\n\n")
|
||||||
|
for i, p := range QueryAll(doc, "div.card.mb-4.box-shadow") {
|
||||||
|
planName := Query(p, "h4").FirstChild.Data
|
||||||
|
price := Query(p, ".pricing-card-title").FirstChild.Data
|
||||||
|
usersIncluded := Query(p, "li:first-child").FirstChild.Data
|
||||||
|
storage := Query(p, "li:nth-child(2)").FirstChild.Data
|
||||||
|
detailsUrl := AttrOr(Query(p, "li:last-child a"), "href", "(No link available)")
|
||||||
|
fmt.Printf(
|
||||||
|
"Plan #%d\nName: %s\nPrice: %s\nUsers: %s\nStorage: %s\nDetails: %s\n\n",
|
||||||
|
i+1,
|
||||||
|
planName,
|
||||||
|
price,
|
||||||
|
usersIncluded,
|
||||||
|
storage,
|
||||||
|
detailsUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The output is:
|
||||||
|
```
|
||||||
|
List of pricing plans:
|
||||||
|
|
||||||
|
Plan #1
|
||||||
|
Name: Free
|
||||||
|
Price: $0/mo
|
||||||
|
Users: 10 users included
|
||||||
|
Storage: 2 GB of storage
|
||||||
|
Details: https://example.com
|
||||||
|
|
||||||
|
Plan #2
|
||||||
|
Name: Pro
|
||||||
|
Price: $15/mo
|
||||||
|
Users: 20 users included
|
||||||
|
Storage: 10 GB of storage
|
||||||
|
Details: https://example.com
|
||||||
|
|
||||||
|
Plan #3
|
||||||
|
Name: Enterprise
|
||||||
|
Price: $29/mo
|
||||||
|
Users: 30 users included
|
||||||
|
Storage: 15 GB of storage
|
||||||
|
Details: (No link available)
|
||||||
|
```
|
||||||
889
vendor/github.com/andybalholm/cascadia/parser.go
generated
vendored
Normal file
889
vendor/github.com/andybalholm/cascadia/parser.go
generated
vendored
Normal file
@@ -0,0 +1,889 @@
|
|||||||
|
// Package cascadia is an implementation of CSS selectors.
|
||||||
|
package cascadia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// a parser for CSS selectors
|
||||||
|
type parser struct {
|
||||||
|
s string // the source text
|
||||||
|
i int // the current position
|
||||||
|
|
||||||
|
// if `false`, parsing a pseudo-element
|
||||||
|
// returns an error.
|
||||||
|
acceptPseudoElements bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseEscape parses a backslash escape.
|
||||||
|
func (p *parser) parseEscape() (result string, err error) {
|
||||||
|
if len(p.s) < p.i+2 || p.s[p.i] != '\\' {
|
||||||
|
return "", errors.New("invalid escape sequence")
|
||||||
|
}
|
||||||
|
|
||||||
|
start := p.i + 1
|
||||||
|
c := p.s[start]
|
||||||
|
switch {
|
||||||
|
case c == '\r' || c == '\n' || c == '\f':
|
||||||
|
return "", errors.New("escaped line ending outside string")
|
||||||
|
case hexDigit(c):
|
||||||
|
// unicode escape (hex)
|
||||||
|
var i int
|
||||||
|
for i = start; i < start+6 && i < len(p.s) && hexDigit(p.s[i]); i++ {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
v, _ := strconv.ParseUint(p.s[start:i], 16, 64)
|
||||||
|
if len(p.s) > i {
|
||||||
|
switch p.s[i] {
|
||||||
|
case '\r':
|
||||||
|
i++
|
||||||
|
if len(p.s) > i && p.s[i] == '\n' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case ' ', '\t', '\n', '\f':
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.i = i
|
||||||
|
return string(rune(v)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the literal character after the backslash.
|
||||||
|
result = p.s[start : start+1]
|
||||||
|
p.i += 2
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toLowerASCII returns s with all ASCII capital letters lowercased.
|
||||||
|
func toLowerASCII(s string) string {
|
||||||
|
var b []byte
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if c := s[i]; 'A' <= c && c <= 'Z' {
|
||||||
|
if b == nil {
|
||||||
|
b = make([]byte, len(s))
|
||||||
|
copy(b, s)
|
||||||
|
}
|
||||||
|
b[i] = s[i] + ('a' - 'A')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b == nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexDigit(c byte) bool {
|
||||||
|
return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F'
|
||||||
|
}
|
||||||
|
|
||||||
|
// nameStart returns whether c can be the first character of an identifier
|
||||||
|
// (not counting an initial hyphen, or an escape sequence).
|
||||||
|
func nameStart(c byte) bool {
|
||||||
|
return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || c > 127
|
||||||
|
}
|
||||||
|
|
||||||
|
// nameChar returns whether c can be a character within an identifier
|
||||||
|
// (not counting an escape sequence).
|
||||||
|
func nameChar(c byte) bool {
|
||||||
|
return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || c > 127 ||
|
||||||
|
c == '-' || '0' <= c && c <= '9'
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIdentifier parses an identifier.
|
||||||
|
func (p *parser) parseIdentifier() (result string, err error) {
|
||||||
|
const prefix = '-'
|
||||||
|
var numPrefix int
|
||||||
|
|
||||||
|
for len(p.s) > p.i && p.s[p.i] == prefix {
|
||||||
|
p.i++
|
||||||
|
numPrefix++
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.s) <= p.i {
|
||||||
|
return "", errors.New("expected identifier, found EOF instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c := p.s[p.i]; !(nameStart(c) || c == '\\') {
|
||||||
|
return "", fmt.Errorf("expected identifier, found %c instead", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = p.parseName()
|
||||||
|
if numPrefix > 0 && err == nil {
|
||||||
|
result = strings.Repeat(string(prefix), numPrefix) + result
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseName parses a name (which is like an identifier, but doesn't have
|
||||||
|
// extra restrictions on the first character).
|
||||||
|
func (p *parser) parseName() (result string, err error) {
|
||||||
|
i := p.i
|
||||||
|
loop:
|
||||||
|
for i < len(p.s) {
|
||||||
|
c := p.s[i]
|
||||||
|
switch {
|
||||||
|
case nameChar(c):
|
||||||
|
start := i
|
||||||
|
for i < len(p.s) && nameChar(p.s[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
result += p.s[start:i]
|
||||||
|
case c == '\\':
|
||||||
|
p.i = i
|
||||||
|
val, err := p.parseEscape()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
i = p.i
|
||||||
|
result += val
|
||||||
|
default:
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result == "" {
|
||||||
|
return "", errors.New("expected name, found EOF instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.i = i
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseString parses a single- or double-quoted string.
|
||||||
|
func (p *parser) parseString() (result string, err error) {
|
||||||
|
i := p.i
|
||||||
|
if len(p.s) < i+2 {
|
||||||
|
return "", errors.New("expected string, found EOF instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
quote := p.s[i]
|
||||||
|
i++
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for i < len(p.s) {
|
||||||
|
switch p.s[i] {
|
||||||
|
case '\\':
|
||||||
|
if len(p.s) > i+1 {
|
||||||
|
switch c := p.s[i+1]; c {
|
||||||
|
case '\r':
|
||||||
|
if len(p.s) > i+2 && p.s[i+2] == '\n' {
|
||||||
|
i += 3
|
||||||
|
continue loop
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case '\n', '\f':
|
||||||
|
i += 2
|
||||||
|
continue loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.i = i
|
||||||
|
val, err := p.parseEscape()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
i = p.i
|
||||||
|
result += val
|
||||||
|
case quote:
|
||||||
|
break loop
|
||||||
|
case '\r', '\n', '\f':
|
||||||
|
return "", errors.New("unexpected end of line in string")
|
||||||
|
default:
|
||||||
|
start := i
|
||||||
|
for i < len(p.s) {
|
||||||
|
if c := p.s[i]; c == quote || c == '\\' || c == '\r' || c == '\n' || c == '\f' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
result += p.s[start:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i >= len(p.s) {
|
||||||
|
return "", errors.New("EOF in string")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume the final quote.
|
||||||
|
i++
|
||||||
|
|
||||||
|
p.i = i
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseRegex parses a regular expression; the end is defined by encountering an
|
||||||
|
// unmatched closing ')' or ']' which is not consumed
|
||||||
|
func (p *parser) parseRegex() (rx *regexp.Regexp, err error) {
|
||||||
|
i := p.i
|
||||||
|
if len(p.s) < i+2 {
|
||||||
|
return nil, errors.New("expected regular expression, found EOF instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
// number of open parens or brackets;
|
||||||
|
// when it becomes negative, finished parsing regex
|
||||||
|
open := 0
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for i < len(p.s) {
|
||||||
|
switch p.s[i] {
|
||||||
|
case '(', '[':
|
||||||
|
open++
|
||||||
|
case ')', ']':
|
||||||
|
open--
|
||||||
|
if open < 0 {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
if i >= len(p.s) {
|
||||||
|
return nil, errors.New("EOF in regular expression")
|
||||||
|
}
|
||||||
|
rx, err = regexp.Compile(p.s[p.i:i])
|
||||||
|
p.i = i
|
||||||
|
return rx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// skipWhitespace consumes whitespace characters and comments.
|
||||||
|
// It returns true if there was actually anything to skip.
|
||||||
|
func (p *parser) skipWhitespace() bool {
|
||||||
|
i := p.i
|
||||||
|
for i < len(p.s) {
|
||||||
|
switch p.s[i] {
|
||||||
|
case ' ', '\t', '\r', '\n', '\f':
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
case '/':
|
||||||
|
if strings.HasPrefix(p.s[i:], "/*") {
|
||||||
|
end := strings.Index(p.s[i+len("/*"):], "*/")
|
||||||
|
if end != -1 {
|
||||||
|
i += end + len("/**/")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if i > p.i {
|
||||||
|
p.i = i
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeParenthesis consumes an opening parenthesis and any following
|
||||||
|
// whitespace. It returns true if there was actually a parenthesis to skip.
|
||||||
|
func (p *parser) consumeParenthesis() bool {
|
||||||
|
if p.i < len(p.s) && p.s[p.i] == '(' {
|
||||||
|
p.i++
|
||||||
|
p.skipWhitespace()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeClosingParenthesis consumes a closing parenthesis and any preceding
|
||||||
|
// whitespace. It returns true if there was actually a parenthesis to skip.
|
||||||
|
func (p *parser) consumeClosingParenthesis() bool {
|
||||||
|
i := p.i
|
||||||
|
p.skipWhitespace()
|
||||||
|
if p.i < len(p.s) && p.s[p.i] == ')' {
|
||||||
|
p.i++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
p.i = i
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTypeSelector parses a type selector (one that matches by tag name).
|
||||||
|
func (p *parser) parseTypeSelector() (result tagSelector, err error) {
|
||||||
|
tag, err := p.parseIdentifier()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return tagSelector{tag: toLowerASCII(tag)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIDSelector parses a selector that matches by id attribute.
|
||||||
|
func (p *parser) parseIDSelector() (idSelector, error) {
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return idSelector{}, fmt.Errorf("expected id selector (#id), found EOF instead")
|
||||||
|
}
|
||||||
|
if p.s[p.i] != '#' {
|
||||||
|
return idSelector{}, fmt.Errorf("expected id selector (#id), found '%c' instead", p.s[p.i])
|
||||||
|
}
|
||||||
|
|
||||||
|
p.i++
|
||||||
|
id, err := p.parseName()
|
||||||
|
if err != nil {
|
||||||
|
return idSelector{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return idSelector{id: id}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseClassSelector parses a selector that matches by class attribute.
|
||||||
|
func (p *parser) parseClassSelector() (classSelector, error) {
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return classSelector{}, fmt.Errorf("expected class selector (.class), found EOF instead")
|
||||||
|
}
|
||||||
|
if p.s[p.i] != '.' {
|
||||||
|
return classSelector{}, fmt.Errorf("expected class selector (.class), found '%c' instead", p.s[p.i])
|
||||||
|
}
|
||||||
|
|
||||||
|
p.i++
|
||||||
|
class, err := p.parseIdentifier()
|
||||||
|
if err != nil {
|
||||||
|
return classSelector{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return classSelector{class: class}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAttributeSelector parses a selector that matches by attribute value.
|
||||||
|
func (p *parser) parseAttributeSelector() (attrSelector, error) {
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return attrSelector{}, fmt.Errorf("expected attribute selector ([attribute]), found EOF instead")
|
||||||
|
}
|
||||||
|
if p.s[p.i] != '[' {
|
||||||
|
return attrSelector{}, fmt.Errorf("expected attribute selector ([attribute]), found '%c' instead", p.s[p.i])
|
||||||
|
}
|
||||||
|
|
||||||
|
p.i++
|
||||||
|
p.skipWhitespace()
|
||||||
|
key, err := p.parseIdentifier()
|
||||||
|
if err != nil {
|
||||||
|
return attrSelector{}, err
|
||||||
|
}
|
||||||
|
key = toLowerASCII(key)
|
||||||
|
|
||||||
|
p.skipWhitespace()
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return attrSelector{}, errors.New("unexpected EOF in attribute selector")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.s[p.i] == ']' {
|
||||||
|
p.i++
|
||||||
|
return attrSelector{key: key, operation: ""}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.i+2 >= len(p.s) {
|
||||||
|
return attrSelector{}, errors.New("unexpected EOF in attribute selector")
|
||||||
|
}
|
||||||
|
|
||||||
|
op := p.s[p.i : p.i+2]
|
||||||
|
if op[0] == '=' {
|
||||||
|
op = "="
|
||||||
|
} else if op[1] != '=' {
|
||||||
|
return attrSelector{}, fmt.Errorf(`expected equality operator, found "%s" instead`, op)
|
||||||
|
}
|
||||||
|
p.i += len(op)
|
||||||
|
|
||||||
|
p.skipWhitespace()
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return attrSelector{}, errors.New("unexpected EOF in attribute selector")
|
||||||
|
}
|
||||||
|
var val string
|
||||||
|
var rx *regexp.Regexp
|
||||||
|
if op == "#=" {
|
||||||
|
rx, err = p.parseRegex()
|
||||||
|
} else {
|
||||||
|
switch p.s[p.i] {
|
||||||
|
case '\'', '"':
|
||||||
|
val, err = p.parseString()
|
||||||
|
default:
|
||||||
|
val, err = p.parseIdentifier()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return attrSelector{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.skipWhitespace()
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return attrSelector{}, errors.New("unexpected EOF in attribute selector")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the attribute contains an ignore case flag
|
||||||
|
ignoreCase := false
|
||||||
|
if p.s[p.i] == 'i' || p.s[p.i] == 'I' {
|
||||||
|
ignoreCase = true
|
||||||
|
p.i++
|
||||||
|
}
|
||||||
|
|
||||||
|
p.skipWhitespace()
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return attrSelector{}, errors.New("unexpected EOF in attribute selector")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.s[p.i] != ']' {
|
||||||
|
return attrSelector{}, fmt.Errorf("expected ']', found '%c' instead", p.s[p.i])
|
||||||
|
}
|
||||||
|
p.i++
|
||||||
|
|
||||||
|
switch op {
|
||||||
|
case "=", "!=", "~=", "|=", "^=", "$=", "*=", "#=":
|
||||||
|
return attrSelector{key: key, val: val, operation: op, regexp: rx, insensitive: ignoreCase}, nil
|
||||||
|
default:
|
||||||
|
return attrSelector{}, fmt.Errorf("attribute operator %q is not supported", op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errExpectedParenthesis = errors.New("expected '(' but didn't find it")
|
||||||
|
errExpectedClosingParenthesis = errors.New("expected ')' but didn't find it")
|
||||||
|
errUnmatchedParenthesis = errors.New("unmatched '('")
|
||||||
|
)
|
||||||
|
|
||||||
|
// parsePseudoclassSelector parses a pseudoclass selector like :not(p) or a pseudo-element
|
||||||
|
// For backwards compatibility, both ':' and '::' prefix are allowed for pseudo-elements.
|
||||||
|
// https://drafts.csswg.org/selectors-3/#pseudo-elements
|
||||||
|
// Returning a nil `Sel` (and a nil `error`) means we found a pseudo-element.
|
||||||
|
func (p *parser) parsePseudoclassSelector() (out Sel, pseudoElement string, err error) {
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return nil, "", fmt.Errorf("expected pseudoclass selector (:pseudoclass), found EOF instead")
|
||||||
|
}
|
||||||
|
if p.s[p.i] != ':' {
|
||||||
|
return nil, "", fmt.Errorf("expected attribute selector (:pseudoclass), found '%c' instead", p.s[p.i])
|
||||||
|
}
|
||||||
|
|
||||||
|
p.i++
|
||||||
|
var mustBePseudoElement bool
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return nil, "", fmt.Errorf("got empty pseudoclass (or pseudoelement)")
|
||||||
|
}
|
||||||
|
if p.s[p.i] == ':' { // we found a pseudo-element
|
||||||
|
mustBePseudoElement = true
|
||||||
|
p.i++
|
||||||
|
}
|
||||||
|
|
||||||
|
name, err := p.parseIdentifier()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name = toLowerASCII(name)
|
||||||
|
if mustBePseudoElement && (name != "after" && name != "backdrop" && name != "before" &&
|
||||||
|
name != "cue" && name != "first-letter" && name != "first-line" && name != "grammar-error" &&
|
||||||
|
name != "marker" && name != "placeholder" && name != "selection" && name != "spelling-error") {
|
||||||
|
return out, "", fmt.Errorf("unknown pseudoelement :%s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "not", "has", "haschild":
|
||||||
|
if !p.consumeParenthesis() {
|
||||||
|
return out, "", errExpectedParenthesis
|
||||||
|
}
|
||||||
|
sel, parseErr := p.parseSelectorGroup()
|
||||||
|
if parseErr != nil {
|
||||||
|
return out, "", parseErr
|
||||||
|
}
|
||||||
|
if !p.consumeClosingParenthesis() {
|
||||||
|
return out, "", errExpectedClosingParenthesis
|
||||||
|
}
|
||||||
|
|
||||||
|
out = relativePseudoClassSelector{name: name, match: sel}
|
||||||
|
|
||||||
|
case "contains", "containsown":
|
||||||
|
if !p.consumeParenthesis() {
|
||||||
|
return out, "", errExpectedParenthesis
|
||||||
|
}
|
||||||
|
if p.i == len(p.s) {
|
||||||
|
return out, "", errUnmatchedParenthesis
|
||||||
|
}
|
||||||
|
var val string
|
||||||
|
switch p.s[p.i] {
|
||||||
|
case '\'', '"':
|
||||||
|
val, err = p.parseString()
|
||||||
|
default:
|
||||||
|
val, err = p.parseIdentifier()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return out, "", err
|
||||||
|
}
|
||||||
|
val = strings.ToLower(val)
|
||||||
|
p.skipWhitespace()
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return out, "", errors.New("unexpected EOF in pseudo selector")
|
||||||
|
}
|
||||||
|
if !p.consumeClosingParenthesis() {
|
||||||
|
return out, "", errExpectedClosingParenthesis
|
||||||
|
}
|
||||||
|
|
||||||
|
out = containsPseudoClassSelector{own: name == "containsown", value: val}
|
||||||
|
|
||||||
|
case "matches", "matchesown":
|
||||||
|
if !p.consumeParenthesis() {
|
||||||
|
return out, "", errExpectedParenthesis
|
||||||
|
}
|
||||||
|
rx, err := p.parseRegex()
|
||||||
|
if err != nil {
|
||||||
|
return out, "", err
|
||||||
|
}
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return out, "", errors.New("unexpected EOF in pseudo selector")
|
||||||
|
}
|
||||||
|
if !p.consumeClosingParenthesis() {
|
||||||
|
return out, "", errExpectedClosingParenthesis
|
||||||
|
}
|
||||||
|
|
||||||
|
out = regexpPseudoClassSelector{own: name == "matchesown", regexp: rx}
|
||||||
|
|
||||||
|
case "nth-child", "nth-last-child", "nth-of-type", "nth-last-of-type":
|
||||||
|
if !p.consumeParenthesis() {
|
||||||
|
return out, "", errExpectedParenthesis
|
||||||
|
}
|
||||||
|
a, b, err := p.parseNth()
|
||||||
|
if err != nil {
|
||||||
|
return out, "", err
|
||||||
|
}
|
||||||
|
if !p.consumeClosingParenthesis() {
|
||||||
|
return out, "", errExpectedClosingParenthesis
|
||||||
|
}
|
||||||
|
last := name == "nth-last-child" || name == "nth-last-of-type"
|
||||||
|
ofType := name == "nth-of-type" || name == "nth-last-of-type"
|
||||||
|
out = nthPseudoClassSelector{a: a, b: b, last: last, ofType: ofType}
|
||||||
|
|
||||||
|
case "first-child":
|
||||||
|
out = nthPseudoClassSelector{a: 0, b: 1, ofType: false, last: false}
|
||||||
|
case "last-child":
|
||||||
|
out = nthPseudoClassSelector{a: 0, b: 1, ofType: false, last: true}
|
||||||
|
case "first-of-type":
|
||||||
|
out = nthPseudoClassSelector{a: 0, b: 1, ofType: true, last: false}
|
||||||
|
case "last-of-type":
|
||||||
|
out = nthPseudoClassSelector{a: 0, b: 1, ofType: true, last: true}
|
||||||
|
case "only-child":
|
||||||
|
out = onlyChildPseudoClassSelector{ofType: false}
|
||||||
|
case "only-of-type":
|
||||||
|
out = onlyChildPseudoClassSelector{ofType: true}
|
||||||
|
case "input":
|
||||||
|
out = inputPseudoClassSelector{}
|
||||||
|
case "empty":
|
||||||
|
out = emptyElementPseudoClassSelector{}
|
||||||
|
case "root":
|
||||||
|
out = rootPseudoClassSelector{}
|
||||||
|
case "link":
|
||||||
|
out = linkPseudoClassSelector{}
|
||||||
|
case "lang":
|
||||||
|
if !p.consumeParenthesis() {
|
||||||
|
return out, "", errExpectedParenthesis
|
||||||
|
}
|
||||||
|
if p.i == len(p.s) {
|
||||||
|
return out, "", errUnmatchedParenthesis
|
||||||
|
}
|
||||||
|
val, err := p.parseIdentifier()
|
||||||
|
if err != nil {
|
||||||
|
return out, "", err
|
||||||
|
}
|
||||||
|
val = strings.ToLower(val)
|
||||||
|
p.skipWhitespace()
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return out, "", errors.New("unexpected EOF in pseudo selector")
|
||||||
|
}
|
||||||
|
if !p.consumeClosingParenthesis() {
|
||||||
|
return out, "", errExpectedClosingParenthesis
|
||||||
|
}
|
||||||
|
out = langPseudoClassSelector{lang: val}
|
||||||
|
case "enabled":
|
||||||
|
out = enabledPseudoClassSelector{}
|
||||||
|
case "disabled":
|
||||||
|
out = disabledPseudoClassSelector{}
|
||||||
|
case "checked":
|
||||||
|
out = checkedPseudoClassSelector{}
|
||||||
|
case "visited", "hover", "active", "focus", "target":
|
||||||
|
// Not applicable in a static context: never match.
|
||||||
|
out = neverMatchSelector{value: ":" + name}
|
||||||
|
case "after", "backdrop", "before", "cue", "first-letter", "first-line", "grammar-error", "marker", "placeholder", "selection", "spelling-error":
|
||||||
|
return nil, name, nil
|
||||||
|
default:
|
||||||
|
return out, "", fmt.Errorf("unknown pseudoclass or pseudoelement :%s", name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInteger parses a decimal integer.
|
||||||
|
func (p *parser) parseInteger() (int, error) {
|
||||||
|
i := p.i
|
||||||
|
start := i
|
||||||
|
for i < len(p.s) && '0' <= p.s[i] && p.s[i] <= '9' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i == start {
|
||||||
|
return 0, errors.New("expected integer, but didn't find it")
|
||||||
|
}
|
||||||
|
p.i = i
|
||||||
|
|
||||||
|
val, err := strconv.Atoi(p.s[start:i])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNth parses the argument for :nth-child (normally of the form an+b).
|
||||||
|
func (p *parser) parseNth() (a, b int, err error) {
|
||||||
|
// initial state
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
goto eof
|
||||||
|
}
|
||||||
|
switch p.s[p.i] {
|
||||||
|
case '-':
|
||||||
|
p.i++
|
||||||
|
goto negativeA
|
||||||
|
case '+':
|
||||||
|
p.i++
|
||||||
|
goto positiveA
|
||||||
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||||
|
goto positiveA
|
||||||
|
case 'n', 'N':
|
||||||
|
a = 1
|
||||||
|
p.i++
|
||||||
|
goto readN
|
||||||
|
case 'o', 'O', 'e', 'E':
|
||||||
|
id, nameErr := p.parseName()
|
||||||
|
if nameErr != nil {
|
||||||
|
return 0, 0, nameErr
|
||||||
|
}
|
||||||
|
id = toLowerASCII(id)
|
||||||
|
if id == "odd" {
|
||||||
|
return 2, 1, nil
|
||||||
|
}
|
||||||
|
if id == "even" {
|
||||||
|
return 2, 0, nil
|
||||||
|
}
|
||||||
|
return 0, 0, fmt.Errorf("expected 'odd' or 'even', but found '%s' instead", id)
|
||||||
|
default:
|
||||||
|
goto invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
positiveA:
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
goto eof
|
||||||
|
}
|
||||||
|
switch p.s[p.i] {
|
||||||
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||||
|
a, err = p.parseInteger()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
goto readA
|
||||||
|
case 'n', 'N':
|
||||||
|
a = 1
|
||||||
|
p.i++
|
||||||
|
goto readN
|
||||||
|
default:
|
||||||
|
goto invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
negativeA:
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
goto eof
|
||||||
|
}
|
||||||
|
switch p.s[p.i] {
|
||||||
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||||
|
a, err = p.parseInteger()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
a = -a
|
||||||
|
goto readA
|
||||||
|
case 'n', 'N':
|
||||||
|
a = -1
|
||||||
|
p.i++
|
||||||
|
goto readN
|
||||||
|
default:
|
||||||
|
goto invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
readA:
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
goto eof
|
||||||
|
}
|
||||||
|
switch p.s[p.i] {
|
||||||
|
case 'n', 'N':
|
||||||
|
p.i++
|
||||||
|
goto readN
|
||||||
|
default:
|
||||||
|
// The number we read as a is actually b.
|
||||||
|
return 0, a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
readN:
|
||||||
|
p.skipWhitespace()
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
goto eof
|
||||||
|
}
|
||||||
|
switch p.s[p.i] {
|
||||||
|
case '+':
|
||||||
|
p.i++
|
||||||
|
p.skipWhitespace()
|
||||||
|
b, err = p.parseInteger()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return a, b, nil
|
||||||
|
case '-':
|
||||||
|
p.i++
|
||||||
|
p.skipWhitespace()
|
||||||
|
b, err = p.parseInteger()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return a, -b, nil
|
||||||
|
default:
|
||||||
|
return a, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
eof:
|
||||||
|
return 0, 0, errors.New("unexpected EOF while attempting to parse expression of form an+b")
|
||||||
|
|
||||||
|
invalid:
|
||||||
|
return 0, 0, errors.New("unexpected character while attempting to parse expression of form an+b")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSimpleSelectorSequence parses a selector sequence that applies to
|
||||||
|
// a single element.
|
||||||
|
func (p *parser) parseSimpleSelectorSequence() (Sel, error) {
|
||||||
|
var selectors []Sel
|
||||||
|
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return nil, errors.New("expected selector, found EOF instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p.s[p.i] {
|
||||||
|
case '*':
|
||||||
|
// It's the universal selector. Just skip over it, since it doesn't affect the meaning.
|
||||||
|
p.i++
|
||||||
|
if p.i+2 < len(p.s) && p.s[p.i:p.i+2] == "|*" { // other version of universal selector
|
||||||
|
p.i += 2
|
||||||
|
}
|
||||||
|
case '#', '.', '[', ':':
|
||||||
|
// There's no type selector. Wait to process the other till the main loop.
|
||||||
|
default:
|
||||||
|
r, err := p.parseTypeSelector()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
selectors = append(selectors, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pseudoElement string
|
||||||
|
loop:
|
||||||
|
for p.i < len(p.s) {
|
||||||
|
var (
|
||||||
|
ns Sel
|
||||||
|
newPseudoElement string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
switch p.s[p.i] {
|
||||||
|
case '#':
|
||||||
|
ns, err = p.parseIDSelector()
|
||||||
|
case '.':
|
||||||
|
ns, err = p.parseClassSelector()
|
||||||
|
case '[':
|
||||||
|
ns, err = p.parseAttributeSelector()
|
||||||
|
case ':':
|
||||||
|
ns, newPseudoElement, err = p.parsePseudoclassSelector()
|
||||||
|
default:
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// From https://drafts.csswg.org/selectors-3/#pseudo-elements :
|
||||||
|
// "Only one pseudo-element may appear per selector, and if present
|
||||||
|
// it must appear after the sequence of simple selectors that
|
||||||
|
// represents the subjects of the selector.""
|
||||||
|
if ns == nil { // we found a pseudo-element
|
||||||
|
if pseudoElement != "" {
|
||||||
|
return nil, fmt.Errorf("only one pseudo-element is accepted per selector, got %s and %s", pseudoElement, newPseudoElement)
|
||||||
|
}
|
||||||
|
if !p.acceptPseudoElements {
|
||||||
|
return nil, fmt.Errorf("pseudo-element %s found, but pseudo-elements support is disabled", newPseudoElement)
|
||||||
|
}
|
||||||
|
pseudoElement = newPseudoElement
|
||||||
|
} else {
|
||||||
|
if pseudoElement != "" {
|
||||||
|
return nil, fmt.Errorf("pseudo-element %s must be at the end of selector", pseudoElement)
|
||||||
|
}
|
||||||
|
selectors = append(selectors, ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if len(selectors) == 1 && pseudoElement == "" { // no need wrap the selectors in compoundSelector
|
||||||
|
return selectors[0], nil
|
||||||
|
}
|
||||||
|
return compoundSelector{selectors: selectors, pseudoElement: pseudoElement}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSelector parses a selector that may include combinators.
|
||||||
|
func (p *parser) parseSelector() (Sel, error) {
|
||||||
|
p.skipWhitespace()
|
||||||
|
result, err := p.parseSimpleSelectorSequence()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
var (
|
||||||
|
combinator byte
|
||||||
|
c Sel
|
||||||
|
)
|
||||||
|
if p.skipWhitespace() {
|
||||||
|
combinator = ' '
|
||||||
|
}
|
||||||
|
if p.i >= len(p.s) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p.s[p.i] {
|
||||||
|
case '+', '>', '~':
|
||||||
|
combinator = p.s[p.i]
|
||||||
|
p.i++
|
||||||
|
p.skipWhitespace()
|
||||||
|
case ',', ')':
|
||||||
|
// These characters can't begin a selector, but they can legally occur after one.
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if combinator == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = p.parseSimpleSelectorSequence()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result = combinedSelector{first: result, combinator: combinator, second: c}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSelectorGroup parses a group of selectors, separated by commas.
|
||||||
|
func (p *parser) parseSelectorGroup() (SelectorGroup, error) {
|
||||||
|
current, err := p.parseSelector()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := SelectorGroup{current}
|
||||||
|
|
||||||
|
for p.i < len(p.s) {
|
||||||
|
if p.s[p.i] != ',' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.i++
|
||||||
|
c, err := p.parseSelector()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result = append(result, c)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
458
vendor/github.com/andybalholm/cascadia/pseudo_classes.go
generated
vendored
Normal file
458
vendor/github.com/andybalholm/cascadia/pseudo_classes.go
generated
vendored
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
package cascadia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
"golang.org/x/net/html/atom"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This file implements the pseudo classes selectors,
|
||||||
|
// which share the implementation of PseudoElement() and Specificity()
|
||||||
|
|
||||||
|
type abstractPseudoClass struct{}
|
||||||
|
|
||||||
|
func (s abstractPseudoClass) Specificity() Specificity {
|
||||||
|
return Specificity{0, 1, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c abstractPseudoClass) PseudoElement() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type relativePseudoClassSelector struct {
|
||||||
|
name string // one of "not", "has", "haschild"
|
||||||
|
match SelectorGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s relativePseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch s.name {
|
||||||
|
case "not":
|
||||||
|
// matches elements that do not match a.
|
||||||
|
return !s.match.Match(n)
|
||||||
|
case "has":
|
||||||
|
// matches elements with any descendant that matches a.
|
||||||
|
return hasDescendantMatch(n, s.match)
|
||||||
|
case "haschild":
|
||||||
|
// matches elements with a child that matches a.
|
||||||
|
return hasChildMatch(n, s.match)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unsupported relative pseudo class selector : %s", s.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasChildMatch returns whether n has any child that matches a.
|
||||||
|
func hasChildMatch(n *html.Node, a Matcher) bool {
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
if a.Match(c) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasDescendantMatch performs a depth-first search of n's descendants,
|
||||||
|
// testing whether any of them match a. It returns true as soon as a match is
|
||||||
|
// found, or false if no match is found.
|
||||||
|
func hasDescendantMatch(n *html.Node, a Matcher) bool {
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
if a.Match(c) || (c.Type == html.ElementNode && hasDescendantMatch(c, a)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specificity returns the specificity of the most specific selectors
|
||||||
|
// in the pseudo-class arguments.
|
||||||
|
// See https://www.w3.org/TR/selectors/#specificity-rules
|
||||||
|
func (s relativePseudoClassSelector) Specificity() Specificity {
|
||||||
|
var max Specificity
|
||||||
|
for _, sel := range s.match {
|
||||||
|
newSpe := sel.Specificity()
|
||||||
|
if max.Less(newSpe) {
|
||||||
|
max = newSpe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c relativePseudoClassSelector) PseudoElement() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type containsPseudoClassSelector struct {
|
||||||
|
abstractPseudoClass
|
||||||
|
value string
|
||||||
|
own bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s containsPseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
var text string
|
||||||
|
if s.own {
|
||||||
|
// matches nodes that directly contain the given text
|
||||||
|
text = strings.ToLower(nodeOwnText(n))
|
||||||
|
} else {
|
||||||
|
// matches nodes that contain the given text.
|
||||||
|
text = strings.ToLower(nodeText(n))
|
||||||
|
}
|
||||||
|
return strings.Contains(text, s.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
type regexpPseudoClassSelector struct {
|
||||||
|
abstractPseudoClass
|
||||||
|
regexp *regexp.Regexp
|
||||||
|
own bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s regexpPseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
var text string
|
||||||
|
if s.own {
|
||||||
|
// matches nodes whose text directly matches the specified regular expression
|
||||||
|
text = nodeOwnText(n)
|
||||||
|
} else {
|
||||||
|
// matches nodes whose text matches the specified regular expression
|
||||||
|
text = nodeText(n)
|
||||||
|
}
|
||||||
|
return s.regexp.MatchString(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeNodeText writes the text contained in n and its descendants to b.
|
||||||
|
func writeNodeText(n *html.Node, b *bytes.Buffer) {
|
||||||
|
switch n.Type {
|
||||||
|
case html.TextNode:
|
||||||
|
b.WriteString(n.Data)
|
||||||
|
case html.ElementNode:
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
writeNodeText(c, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodeText returns the text contained in n and its descendants.
|
||||||
|
func nodeText(n *html.Node) string {
|
||||||
|
var b bytes.Buffer
|
||||||
|
writeNodeText(n, &b)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodeOwnText returns the contents of the text nodes that are direct
|
||||||
|
// children of n.
|
||||||
|
func nodeOwnText(n *html.Node) string {
|
||||||
|
var b bytes.Buffer
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
if c.Type == html.TextNode {
|
||||||
|
b.WriteString(c.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type nthPseudoClassSelector struct {
|
||||||
|
abstractPseudoClass
|
||||||
|
a, b int
|
||||||
|
last, ofType bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s nthPseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
if s.a == 0 {
|
||||||
|
if s.last {
|
||||||
|
return simpleNthLastChildMatch(s.b, s.ofType, n)
|
||||||
|
} else {
|
||||||
|
return simpleNthChildMatch(s.b, s.ofType, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nthChildMatch(s.a, s.b, s.last, s.ofType, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nthChildMatch implements :nth-child(an+b).
|
||||||
|
// If last is true, implements :nth-last-child instead.
|
||||||
|
// If ofType is true, implements :nth-of-type instead.
|
||||||
|
func nthChildMatch(a, b int, last, ofType bool, n *html.Node) bool {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := n.Parent
|
||||||
|
if parent == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
i := -1
|
||||||
|
count := 0
|
||||||
|
for c := parent.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
if (c.Type != html.ElementNode) || (ofType && c.Data != n.Data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
if c == n {
|
||||||
|
i = count
|
||||||
|
if !last {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == -1 {
|
||||||
|
// This shouldn't happen, since n should always be one of its parent's children.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if last {
|
||||||
|
i = count - i + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
i -= b
|
||||||
|
if a == 0 {
|
||||||
|
return i == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return i%a == 0 && i/a >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// simpleNthChildMatch implements :nth-child(b).
|
||||||
|
// If ofType is true, implements :nth-of-type instead.
|
||||||
|
func simpleNthChildMatch(b int, ofType bool, n *html.Node) bool {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := n.Parent
|
||||||
|
if parent == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for c := parent.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
if c.Type != html.ElementNode || (ofType && c.Data != n.Data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
if c == n {
|
||||||
|
return count == b
|
||||||
|
}
|
||||||
|
if count >= b {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// simpleNthLastChildMatch implements :nth-last-child(b).
|
||||||
|
// If ofType is true, implements :nth-last-of-type instead.
|
||||||
|
func simpleNthLastChildMatch(b int, ofType bool, n *html.Node) bool {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := n.Parent
|
||||||
|
if parent == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for c := parent.LastChild; c != nil; c = c.PrevSibling {
|
||||||
|
if c.Type != html.ElementNode || (ofType && c.Data != n.Data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
if c == n {
|
||||||
|
return count == b
|
||||||
|
}
|
||||||
|
if count >= b {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type onlyChildPseudoClassSelector struct {
|
||||||
|
abstractPseudoClass
|
||||||
|
ofType bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match implements :only-child.
|
||||||
|
// If `ofType` is true, it implements :only-of-type instead.
|
||||||
|
func (s onlyChildPseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := n.Parent
|
||||||
|
if parent == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for c := parent.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
if (c.Type != html.ElementNode) || (s.ofType && c.Data != n.Data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
if count > 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
type inputPseudoClassSelector struct {
|
||||||
|
abstractPseudoClass
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches input, select, textarea and button elements.
|
||||||
|
func (s inputPseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
return n.Type == html.ElementNode && (n.Data == "input" || n.Data == "select" || n.Data == "textarea" || n.Data == "button")
|
||||||
|
}
|
||||||
|
|
||||||
|
type emptyElementPseudoClassSelector struct {
|
||||||
|
abstractPseudoClass
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches empty elements.
|
||||||
|
func (s emptyElementPseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
switch c.Type {
|
||||||
|
case html.ElementNode:
|
||||||
|
return false
|
||||||
|
case html.TextNode:
|
||||||
|
if strings.TrimSpace(nodeText(c)) == "" {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type rootPseudoClassSelector struct {
|
||||||
|
abstractPseudoClass
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match implements :root
|
||||||
|
func (s rootPseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if n.Parent == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return n.Parent.Type == html.DocumentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAttr(n *html.Node, attr string) bool {
|
||||||
|
return matchAttribute(n, attr, func(string) bool { return true })
|
||||||
|
}
|
||||||
|
|
||||||
|
type linkPseudoClassSelector struct {
|
||||||
|
abstractPseudoClass
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match implements :link
|
||||||
|
func (s linkPseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
return (n.DataAtom == atom.A || n.DataAtom == atom.Area || n.DataAtom == atom.Link) && hasAttr(n, "href")
|
||||||
|
}
|
||||||
|
|
||||||
|
type langPseudoClassSelector struct {
|
||||||
|
abstractPseudoClass
|
||||||
|
lang string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s langPseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
own := matchAttribute(n, "lang", func(val string) bool {
|
||||||
|
return val == s.lang || strings.HasPrefix(val, s.lang+"-")
|
||||||
|
})
|
||||||
|
if n.Parent == nil {
|
||||||
|
return own
|
||||||
|
}
|
||||||
|
return own || s.Match(n.Parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
type enabledPseudoClassSelector struct {
|
||||||
|
abstractPseudoClass
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s enabledPseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch n.DataAtom {
|
||||||
|
case atom.A, atom.Area, atom.Link:
|
||||||
|
return hasAttr(n, "href")
|
||||||
|
case atom.Optgroup, atom.Menuitem, atom.Fieldset:
|
||||||
|
return !hasAttr(n, "disabled")
|
||||||
|
case atom.Button, atom.Input, atom.Select, atom.Textarea, atom.Option:
|
||||||
|
return !hasAttr(n, "disabled") && !inDisabledFieldset(n)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type disabledPseudoClassSelector struct {
|
||||||
|
abstractPseudoClass
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s disabledPseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch n.DataAtom {
|
||||||
|
case atom.Optgroup, atom.Menuitem, atom.Fieldset:
|
||||||
|
return hasAttr(n, "disabled")
|
||||||
|
case atom.Button, atom.Input, atom.Select, atom.Textarea, atom.Option:
|
||||||
|
return hasAttr(n, "disabled") || inDisabledFieldset(n)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasLegendInPreviousSiblings(n *html.Node) bool {
|
||||||
|
for s := n.PrevSibling; s != nil; s = s.PrevSibling {
|
||||||
|
if s.DataAtom == atom.Legend {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func inDisabledFieldset(n *html.Node) bool {
|
||||||
|
if n.Parent == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if n.Parent.DataAtom == atom.Fieldset && hasAttr(n.Parent, "disabled") &&
|
||||||
|
(n.DataAtom != atom.Legend || hasLegendInPreviousSiblings(n)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return inDisabledFieldset(n.Parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
type checkedPseudoClassSelector struct {
|
||||||
|
abstractPseudoClass
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s checkedPseudoClassSelector) Match(n *html.Node) bool {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch n.DataAtom {
|
||||||
|
case atom.Input, atom.Menuitem:
|
||||||
|
return hasAttr(n, "checked") && matchAttribute(n, "type", func(val string) bool {
|
||||||
|
t := toLowerASCII(val)
|
||||||
|
return t == "checkbox" || t == "radio"
|
||||||
|
})
|
||||||
|
case atom.Option:
|
||||||
|
return hasAttr(n, "selected")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
586
vendor/github.com/andybalholm/cascadia/selector.go
generated
vendored
Normal file
586
vendor/github.com/andybalholm/cascadia/selector.go
generated
vendored
Normal file
@@ -0,0 +1,586 @@
|
|||||||
|
package cascadia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Matcher is the interface for basic selector functionality.
|
||||||
|
// Match returns whether a selector matches n.
|
||||||
|
type Matcher interface {
|
||||||
|
Match(n *html.Node) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sel is the interface for all the functionality provided by selectors.
|
||||||
|
type Sel interface {
|
||||||
|
Matcher
|
||||||
|
Specificity() Specificity
|
||||||
|
|
||||||
|
// Returns a CSS input compiling to this selector.
|
||||||
|
String() string
|
||||||
|
|
||||||
|
// Returns a pseudo-element, or an empty string.
|
||||||
|
PseudoElement() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses a selector. Use `ParseWithPseudoElement`
|
||||||
|
// if you need support for pseudo-elements.
|
||||||
|
func Parse(sel string) (Sel, error) {
|
||||||
|
p := &parser{s: sel}
|
||||||
|
compiled, err := p.parseSelector()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.i < len(sel) {
|
||||||
|
return nil, fmt.Errorf("parsing %q: %d bytes left over", sel, len(sel)-p.i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return compiled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseWithPseudoElement parses a single selector,
|
||||||
|
// with support for pseudo-element.
|
||||||
|
func ParseWithPseudoElement(sel string) (Sel, error) {
|
||||||
|
p := &parser{s: sel, acceptPseudoElements: true}
|
||||||
|
compiled, err := p.parseSelector()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.i < len(sel) {
|
||||||
|
return nil, fmt.Errorf("parsing %q: %d bytes left over", sel, len(sel)-p.i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return compiled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseGroup parses a selector, or a group of selectors separated by commas.
|
||||||
|
// Use `ParseGroupWithPseudoElements`
|
||||||
|
// if you need support for pseudo-elements.
|
||||||
|
func ParseGroup(sel string) (SelectorGroup, error) {
|
||||||
|
p := &parser{s: sel}
|
||||||
|
compiled, err := p.parseSelectorGroup()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.i < len(sel) {
|
||||||
|
return nil, fmt.Errorf("parsing %q: %d bytes left over", sel, len(sel)-p.i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return compiled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseGroupWithPseudoElements parses a selector, or a group of selectors separated by commas.
|
||||||
|
// It supports pseudo-elements.
|
||||||
|
func ParseGroupWithPseudoElements(sel string) (SelectorGroup, error) {
|
||||||
|
p := &parser{s: sel, acceptPseudoElements: true}
|
||||||
|
compiled, err := p.parseSelectorGroup()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.i < len(sel) {
|
||||||
|
return nil, fmt.Errorf("parsing %q: %d bytes left over", sel, len(sel)-p.i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return compiled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Selector is a function which tells whether a node matches or not.
|
||||||
|
//
|
||||||
|
// This type is maintained for compatibility; I recommend using the newer and
|
||||||
|
// more idiomatic interfaces Sel and Matcher.
|
||||||
|
type Selector func(*html.Node) bool
|
||||||
|
|
||||||
|
// Compile parses a selector and returns, if successful, a Selector object
|
||||||
|
// that can be used to match against html.Node objects.
|
||||||
|
func Compile(sel string) (Selector, error) {
|
||||||
|
compiled, err := ParseGroup(sel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Selector(compiled.Match), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustCompile is like Compile, but panics instead of returning an error.
|
||||||
|
func MustCompile(sel string) Selector {
|
||||||
|
compiled, err := Compile(sel)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return compiled
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchAll returns a slice of the nodes that match the selector,
|
||||||
|
// from n and its children.
|
||||||
|
func (s Selector) MatchAll(n *html.Node) []*html.Node {
|
||||||
|
return s.matchAllInto(n, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Selector) matchAllInto(n *html.Node, storage []*html.Node) []*html.Node {
|
||||||
|
if s(n) {
|
||||||
|
storage = append(storage, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
for child := n.FirstChild; child != nil; child = child.NextSibling {
|
||||||
|
storage = s.matchAllInto(child, storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryInto(n *html.Node, m Matcher, storage []*html.Node) []*html.Node {
|
||||||
|
for child := n.FirstChild; child != nil; child = child.NextSibling {
|
||||||
|
if m.Match(child) {
|
||||||
|
storage = append(storage, child)
|
||||||
|
}
|
||||||
|
storage = queryInto(child, m, storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return storage
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryAll returns a slice of all the nodes that match m, from the descendants
|
||||||
|
// of n.
|
||||||
|
func QueryAll(n *html.Node, m Matcher) []*html.Node {
|
||||||
|
return queryInto(n, m, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match returns true if the node matches the selector.
|
||||||
|
func (s Selector) Match(n *html.Node) bool {
|
||||||
|
return s(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchFirst returns the first node that matches s, from n and its children.
|
||||||
|
func (s Selector) MatchFirst(n *html.Node) *html.Node {
|
||||||
|
if s.Match(n) {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
m := s.MatchFirst(c)
|
||||||
|
if m != nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query returns the first node that matches m, from the descendants of n.
|
||||||
|
// If none matches, it returns nil.
|
||||||
|
func Query(n *html.Node, m Matcher) *html.Node {
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
if m.Match(c) {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
if matched := Query(c, m); matched != nil {
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter returns the nodes in nodes that match the selector.
|
||||||
|
func (s Selector) Filter(nodes []*html.Node) (result []*html.Node) {
|
||||||
|
for _, n := range nodes {
|
||||||
|
if s(n) {
|
||||||
|
result = append(result, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter returns the nodes that match m.
|
||||||
|
func Filter(nodes []*html.Node, m Matcher) (result []*html.Node) {
|
||||||
|
for _, n := range nodes {
|
||||||
|
if m.Match(n) {
|
||||||
|
result = append(result, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type tagSelector struct {
|
||||||
|
tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches elements with a given tag name.
|
||||||
|
func (t tagSelector) Match(n *html.Node) bool {
|
||||||
|
return n.Type == html.ElementNode && n.Data == t.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c tagSelector) Specificity() Specificity {
|
||||||
|
return Specificity{0, 0, 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c tagSelector) PseudoElement() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type classSelector struct {
|
||||||
|
class string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches elements by class attribute.
|
||||||
|
func (t classSelector) Match(n *html.Node) bool {
|
||||||
|
return matchAttribute(n, "class", func(s string) bool {
|
||||||
|
return matchInclude(t.class, s, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c classSelector) Specificity() Specificity {
|
||||||
|
return Specificity{0, 1, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c classSelector) PseudoElement() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type idSelector struct {
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches elements by id attribute.
|
||||||
|
func (t idSelector) Match(n *html.Node) bool {
|
||||||
|
return matchAttribute(n, "id", func(s string) bool {
|
||||||
|
return s == t.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c idSelector) Specificity() Specificity {
|
||||||
|
return Specificity{1, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c idSelector) PseudoElement() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type attrSelector struct {
|
||||||
|
key, val, operation string
|
||||||
|
regexp *regexp.Regexp
|
||||||
|
insensitive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches elements by attribute value.
|
||||||
|
func (t attrSelector) Match(n *html.Node) bool {
|
||||||
|
switch t.operation {
|
||||||
|
case "":
|
||||||
|
return matchAttribute(n, t.key, func(string) bool { return true })
|
||||||
|
case "=":
|
||||||
|
return matchAttribute(n, t.key, func(s string) bool { return matchInsensitiveValue(s, t.val, t.insensitive) })
|
||||||
|
case "!=":
|
||||||
|
return attributeNotEqualMatch(t.key, t.val, n, t.insensitive)
|
||||||
|
case "~=":
|
||||||
|
// matches elements where the attribute named key is a whitespace-separated list that includes val.
|
||||||
|
return matchAttribute(n, t.key, func(s string) bool { return matchInclude(t.val, s, t.insensitive) })
|
||||||
|
case "|=":
|
||||||
|
return attributeDashMatch(t.key, t.val, n, t.insensitive)
|
||||||
|
case "^=":
|
||||||
|
return attributePrefixMatch(t.key, t.val, n, t.insensitive)
|
||||||
|
case "$=":
|
||||||
|
return attributeSuffixMatch(t.key, t.val, n, t.insensitive)
|
||||||
|
case "*=":
|
||||||
|
return attributeSubstringMatch(t.key, t.val, n, t.insensitive)
|
||||||
|
case "#=":
|
||||||
|
return attributeRegexMatch(t.key, t.regexp, n)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unsuported operation : %s", t.operation))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches elements where we ignore (or not) the case of the attribute value
|
||||||
|
// the user attribute is the value set by the user to match elements
|
||||||
|
// the real attribute is the attribute value found in the code parsed
|
||||||
|
func matchInsensitiveValue(userAttr string, realAttr string, ignoreCase bool) bool {
|
||||||
|
if ignoreCase {
|
||||||
|
return strings.EqualFold(userAttr, realAttr)
|
||||||
|
}
|
||||||
|
return userAttr == realAttr
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches elements where the attribute named key satisifes the function f.
|
||||||
|
func matchAttribute(n *html.Node, key string, f func(string) bool) bool {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, a := range n.Attr {
|
||||||
|
if a.Key == key && f(a.Val) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// attributeNotEqualMatch matches elements where
|
||||||
|
// the attribute named key does not have the value val.
|
||||||
|
func attributeNotEqualMatch(key, val string, n *html.Node, ignoreCase bool) bool {
|
||||||
|
if n.Type != html.ElementNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, a := range n.Attr {
|
||||||
|
if a.Key == key && matchInsensitiveValue(a.Val, val, ignoreCase) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if s is a whitespace-separated list that includes val.
|
||||||
|
func matchInclude(val string, s string, ignoreCase bool) bool {
|
||||||
|
for s != "" {
|
||||||
|
i := strings.IndexAny(s, " \t\r\n\f")
|
||||||
|
if i == -1 {
|
||||||
|
return matchInsensitiveValue(s, val, ignoreCase)
|
||||||
|
}
|
||||||
|
if matchInsensitiveValue(s[:i], val, ignoreCase) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
s = s[i+1:]
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches elements where the attribute named key equals val or starts with val plus a hyphen.
|
||||||
|
func attributeDashMatch(key, val string, n *html.Node, ignoreCase bool) bool {
|
||||||
|
return matchAttribute(n, key,
|
||||||
|
func(s string) bool {
|
||||||
|
if matchInsensitiveValue(s, val, ignoreCase) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(s) <= len(val) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if matchInsensitiveValue(s[:len(val)], val, ignoreCase) && s[len(val)] == '-' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// attributePrefixMatch returns a Selector that matches elements where
|
||||||
|
// the attribute named key starts with val.
|
||||||
|
func attributePrefixMatch(key, val string, n *html.Node, ignoreCase bool) bool {
|
||||||
|
return matchAttribute(n, key,
|
||||||
|
func(s string) bool {
|
||||||
|
if strings.TrimSpace(s) == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ignoreCase {
|
||||||
|
return strings.HasPrefix(strings.ToLower(s), strings.ToLower(val))
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(s, val)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// attributeSuffixMatch matches elements where
|
||||||
|
// the attribute named key ends with val.
|
||||||
|
func attributeSuffixMatch(key, val string, n *html.Node, ignoreCase bool) bool {
|
||||||
|
return matchAttribute(n, key,
|
||||||
|
func(s string) bool {
|
||||||
|
if strings.TrimSpace(s) == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ignoreCase {
|
||||||
|
return strings.HasSuffix(strings.ToLower(s), strings.ToLower(val))
|
||||||
|
}
|
||||||
|
return strings.HasSuffix(s, val)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// attributeSubstringMatch matches nodes where
|
||||||
|
// the attribute named key contains val.
|
||||||
|
func attributeSubstringMatch(key, val string, n *html.Node, ignoreCase bool) bool {
|
||||||
|
return matchAttribute(n, key,
|
||||||
|
func(s string) bool {
|
||||||
|
if strings.TrimSpace(s) == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ignoreCase {
|
||||||
|
return strings.Contains(strings.ToLower(s), strings.ToLower(val))
|
||||||
|
}
|
||||||
|
return strings.Contains(s, val)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// attributeRegexMatch matches nodes where
|
||||||
|
// the attribute named key matches the regular expression rx
|
||||||
|
func attributeRegexMatch(key string, rx *regexp.Regexp, n *html.Node) bool {
|
||||||
|
return matchAttribute(n, key,
|
||||||
|
func(s string) bool {
|
||||||
|
return rx.MatchString(s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c attrSelector) Specificity() Specificity {
|
||||||
|
return Specificity{0, 1, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c attrSelector) PseudoElement() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// see pseudo_classes.go for pseudo classes selectors
|
||||||
|
|
||||||
|
// on a static context, some selectors can't match anything
|
||||||
|
type neverMatchSelector struct {
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s neverMatchSelector) Match(n *html.Node) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s neverMatchSelector) Specificity() Specificity {
|
||||||
|
return Specificity{0, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c neverMatchSelector) PseudoElement() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type compoundSelector struct {
|
||||||
|
selectors []Sel
|
||||||
|
pseudoElement string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches elements if each sub-selectors matches.
|
||||||
|
func (t compoundSelector) Match(n *html.Node) bool {
|
||||||
|
if len(t.selectors) == 0 {
|
||||||
|
return n.Type == html.ElementNode
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sel := range t.selectors {
|
||||||
|
if !sel.Match(n) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s compoundSelector) Specificity() Specificity {
|
||||||
|
var out Specificity
|
||||||
|
for _, sel := range s.selectors {
|
||||||
|
out = out.Add(sel.Specificity())
|
||||||
|
}
|
||||||
|
if s.pseudoElement != "" {
|
||||||
|
// https://drafts.csswg.org/selectors-3/#specificity
|
||||||
|
out = out.Add(Specificity{0, 0, 1})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c compoundSelector) PseudoElement() string {
|
||||||
|
return c.pseudoElement
|
||||||
|
}
|
||||||
|
|
||||||
|
type combinedSelector struct {
|
||||||
|
first Sel
|
||||||
|
combinator byte
|
||||||
|
second Sel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t combinedSelector) Match(n *html.Node) bool {
|
||||||
|
if t.first == nil {
|
||||||
|
return false // maybe we should panic
|
||||||
|
}
|
||||||
|
switch t.combinator {
|
||||||
|
case 0:
|
||||||
|
return t.first.Match(n)
|
||||||
|
case ' ':
|
||||||
|
return descendantMatch(t.first, t.second, n)
|
||||||
|
case '>':
|
||||||
|
return childMatch(t.first, t.second, n)
|
||||||
|
case '+':
|
||||||
|
return siblingMatch(t.first, t.second, true, n)
|
||||||
|
case '~':
|
||||||
|
return siblingMatch(t.first, t.second, false, n)
|
||||||
|
default:
|
||||||
|
panic("unknown combinator")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches an element if it matches d and has an ancestor that matches a.
|
||||||
|
func descendantMatch(a, d Matcher, n *html.Node) bool {
|
||||||
|
if !d.Match(n) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for p := n.Parent; p != nil; p = p.Parent {
|
||||||
|
if a.Match(p) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches an element if it matches d and its parent matches a.
|
||||||
|
func childMatch(a, d Matcher, n *html.Node) bool {
|
||||||
|
return d.Match(n) && n.Parent != nil && a.Match(n.Parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches an element if it matches s2 and is preceded by an element that matches s1.
|
||||||
|
// If adjacent is true, the sibling must be immediately before the element.
|
||||||
|
func siblingMatch(s1, s2 Matcher, adjacent bool, n *html.Node) bool {
|
||||||
|
if !s2.Match(n) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if adjacent {
|
||||||
|
for n = n.PrevSibling; n != nil; n = n.PrevSibling {
|
||||||
|
if n.Type == html.TextNode || n.Type == html.CommentNode {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return s1.Match(n)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk backwards looking for element that matches s1
|
||||||
|
for c := n.PrevSibling; c != nil; c = c.PrevSibling {
|
||||||
|
if s1.Match(c) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s combinedSelector) Specificity() Specificity {
|
||||||
|
spec := s.first.Specificity()
|
||||||
|
if s.second != nil {
|
||||||
|
spec = spec.Add(s.second.Specificity())
|
||||||
|
}
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// on combinedSelector, a pseudo-element only makes sens on the last
|
||||||
|
// selector, although others increase specificity.
|
||||||
|
func (c combinedSelector) PseudoElement() string {
|
||||||
|
if c.second == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.second.PseudoElement()
|
||||||
|
}
|
||||||
|
|
||||||
|
// A SelectorGroup is a list of selectors, which matches if any of the
|
||||||
|
// individual selectors matches.
|
||||||
|
type SelectorGroup []Sel
|
||||||
|
|
||||||
|
// Match returns true if the node matches one of the single selectors.
|
||||||
|
func (s SelectorGroup) Match(n *html.Node) bool {
|
||||||
|
for _, sel := range s {
|
||||||
|
if sel.Match(n) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
176
vendor/github.com/andybalholm/cascadia/serialize.go
generated
vendored
Normal file
176
vendor/github.com/andybalholm/cascadia/serialize.go
generated
vendored
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package cascadia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// implements the reverse operation Sel -> string
|
||||||
|
|
||||||
|
var specialCharReplacer *strings.Replacer
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var pairs []string
|
||||||
|
for _, s := range ",!\"#$%&'()*+ -./:;<=>?@[\\]^`{|}~" {
|
||||||
|
pairs = append(pairs, string(s), "\\"+string(s))
|
||||||
|
}
|
||||||
|
specialCharReplacer = strings.NewReplacer(pairs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// espace special CSS char
|
||||||
|
func escape(s string) string { return specialCharReplacer.Replace(s) }
|
||||||
|
|
||||||
|
func (c tagSelector) String() string {
|
||||||
|
return c.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c idSelector) String() string {
|
||||||
|
return "#" + escape(c.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c classSelector) String() string {
|
||||||
|
return "." + escape(c.class)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c attrSelector) String() string {
|
||||||
|
val := c.val
|
||||||
|
if c.operation == "#=" {
|
||||||
|
val = c.regexp.String()
|
||||||
|
} else if c.operation != "" {
|
||||||
|
val = fmt.Sprintf(`"%s"`, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreCase := ""
|
||||||
|
|
||||||
|
if c.insensitive {
|
||||||
|
ignoreCase = " i"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`[%s%s%s%s]`, c.key, c.operation, val, ignoreCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c relativePseudoClassSelector) String() string {
|
||||||
|
return fmt.Sprintf(":%s(%s)", c.name, c.match.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c containsPseudoClassSelector) String() string {
|
||||||
|
s := "contains"
|
||||||
|
if c.own {
|
||||||
|
s += "Own"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`:%s("%s")`, s, c.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c regexpPseudoClassSelector) String() string {
|
||||||
|
s := "matches"
|
||||||
|
if c.own {
|
||||||
|
s += "Own"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(":%s(%s)", s, c.regexp.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c nthPseudoClassSelector) String() string {
|
||||||
|
if c.a == 0 && c.b == 1 { // special cases
|
||||||
|
s := ":first-"
|
||||||
|
if c.last {
|
||||||
|
s = ":last-"
|
||||||
|
}
|
||||||
|
if c.ofType {
|
||||||
|
s += "of-type"
|
||||||
|
} else {
|
||||||
|
s += "child"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
var name string
|
||||||
|
switch [2]bool{c.last, c.ofType} {
|
||||||
|
case [2]bool{true, true}:
|
||||||
|
name = "nth-last-of-type"
|
||||||
|
case [2]bool{true, false}:
|
||||||
|
name = "nth-last-child"
|
||||||
|
case [2]bool{false, true}:
|
||||||
|
name = "nth-of-type"
|
||||||
|
case [2]bool{false, false}:
|
||||||
|
name = "nth-child"
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("+%d", c.b)
|
||||||
|
if c.b < 0 { // avoid +-8 invalid syntax
|
||||||
|
s = strconv.Itoa(c.b)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(":%s(%dn%s)", name, c.a, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c onlyChildPseudoClassSelector) String() string {
|
||||||
|
if c.ofType {
|
||||||
|
return ":only-of-type"
|
||||||
|
}
|
||||||
|
return ":only-child"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c inputPseudoClassSelector) String() string {
|
||||||
|
return ":input"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c emptyElementPseudoClassSelector) String() string {
|
||||||
|
return ":empty"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c rootPseudoClassSelector) String() string {
|
||||||
|
return ":root"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c linkPseudoClassSelector) String() string {
|
||||||
|
return ":link"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c langPseudoClassSelector) String() string {
|
||||||
|
return fmt.Sprintf(":lang(%s)", c.lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c neverMatchSelector) String() string {
|
||||||
|
return c.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c enabledPseudoClassSelector) String() string {
|
||||||
|
return ":enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c disabledPseudoClassSelector) String() string {
|
||||||
|
return ":disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c checkedPseudoClassSelector) String() string {
|
||||||
|
return ":checked"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c compoundSelector) String() string {
|
||||||
|
if len(c.selectors) == 0 && c.pseudoElement == "" {
|
||||||
|
return "*"
|
||||||
|
}
|
||||||
|
chunks := make([]string, len(c.selectors))
|
||||||
|
for i, sel := range c.selectors {
|
||||||
|
chunks[i] = sel.String()
|
||||||
|
}
|
||||||
|
s := strings.Join(chunks, "")
|
||||||
|
if c.pseudoElement != "" {
|
||||||
|
s += "::" + c.pseudoElement
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c combinedSelector) String() string {
|
||||||
|
start := c.first.String()
|
||||||
|
if c.second != nil {
|
||||||
|
start += fmt.Sprintf(" %s %s", string(c.combinator), c.second.String())
|
||||||
|
}
|
||||||
|
return start
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c SelectorGroup) String() string {
|
||||||
|
ck := make([]string, len(c))
|
||||||
|
for i, s := range c {
|
||||||
|
ck[i] = s.String()
|
||||||
|
}
|
||||||
|
return strings.Join(ck, ", ")
|
||||||
|
}
|
||||||
26
vendor/github.com/andybalholm/cascadia/specificity.go
generated
vendored
Normal file
26
vendor/github.com/andybalholm/cascadia/specificity.go
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package cascadia
|
||||||
|
|
||||||
|
// Specificity is the CSS specificity as defined in
|
||||||
|
// https://www.w3.org/TR/selectors/#specificity-rules
|
||||||
|
// with the convention Specificity = [A,B,C].
|
||||||
|
type Specificity [3]int
|
||||||
|
|
||||||
|
// returns `true` if s < other (strictly), false otherwise
|
||||||
|
func (s Specificity) Less(other Specificity) bool {
|
||||||
|
for i := range s {
|
||||||
|
if s[i] < other[i] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s[i] > other[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Specificity) Add(other Specificity) Specificity {
|
||||||
|
for i, sp := range other {
|
||||||
|
s[i] += sp
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
9
vendor/github.com/go-sql-driver/mysql/.gitignore
generated
vendored
Normal file
9
vendor/github.com/go-sql-driver/mysql/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
Icon?
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
.idea
|
||||||
159
vendor/github.com/go-sql-driver/mysql/AUTHORS
generated
vendored
Normal file
159
vendor/github.com/go-sql-driver/mysql/AUTHORS
generated
vendored
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# This is the official list of Go-MySQL-Driver authors for copyright purposes.
|
||||||
|
|
||||||
|
# If you are submitting a patch, please add your name or the name of the
|
||||||
|
# organization which holds the copyright to this list in alphabetical order.
|
||||||
|
|
||||||
|
# Names should be added to this file as
|
||||||
|
# Name <email address>
|
||||||
|
# The email address is not required for organizations.
|
||||||
|
# Please keep the list sorted.
|
||||||
|
|
||||||
|
|
||||||
|
# Individual Persons
|
||||||
|
|
||||||
|
Aaron Hopkins <go-sql-driver at die.net>
|
||||||
|
Achille Roussel <achille.roussel at gmail.com>
|
||||||
|
Aidan <aidan.liu at pingcap.com>
|
||||||
|
Alex Snast <alexsn at fb.com>
|
||||||
|
Alexey Palazhchenko <alexey.palazhchenko at gmail.com>
|
||||||
|
Andrew Reid <andrew.reid at tixtrack.com>
|
||||||
|
Animesh Ray <mail.rayanimesh at gmail.com>
|
||||||
|
Ariel Mashraki <ariel at mashraki.co.il>
|
||||||
|
Arne Hormann <arnehormann at gmail.com>
|
||||||
|
Artur Melanchyk <artur.melanchyk@gmail.com>
|
||||||
|
Asta Xie <xiemengjun at gmail.com>
|
||||||
|
B Lamarche <blam413 at gmail.com>
|
||||||
|
Bes Dollma <bdollma@thousandeyes.com>
|
||||||
|
Bogdan Constantinescu <bog.con.bc at gmail.com>
|
||||||
|
Brad Higgins <brad at defined.net>
|
||||||
|
Brian Hendriks <brian at dolthub.com>
|
||||||
|
Bulat Gaifullin <gaifullinbf at gmail.com>
|
||||||
|
Caine Jette <jette at alum.mit.edu>
|
||||||
|
Carlos Nieto <jose.carlos at menteslibres.net>
|
||||||
|
Chris Kirkland <chriskirkland at github.com>
|
||||||
|
Chris Moos <chris at tech9computers.com>
|
||||||
|
Craig Wilson <craiggwilson at gmail.com>
|
||||||
|
Daemonxiao <735462752 at qq.com>
|
||||||
|
Daniel Montoya <dsmontoyam at gmail.com>
|
||||||
|
Daniel Nichter <nil at codenode.com>
|
||||||
|
Daniël van Eeden <git at myname.nl>
|
||||||
|
Dave Protasowski <dprotaso at gmail.com>
|
||||||
|
Demouth <yuya at demouth.net>
|
||||||
|
Diego Dupin <diego.dupin at gmail.com>
|
||||||
|
Dirkjan Bussink <d.bussink at gmail.com>
|
||||||
|
DisposaBoy <disposaboy at dby.me>
|
||||||
|
Egor Smolyakov <egorsmkv at gmail.com>
|
||||||
|
Erwan Martin <hello at erwan.io>
|
||||||
|
Evan Elias <evan at skeema.net>
|
||||||
|
Evan Shaw <evan at vendhq.com>
|
||||||
|
Frederick Mayle <frederickmayle at gmail.com>
|
||||||
|
Gustavo Kristic <gkristic at gmail.com>
|
||||||
|
Gusted <postmaster at gusted.xyz>
|
||||||
|
Hajime Nakagami <nakagami at gmail.com>
|
||||||
|
Hanno Braun <mail at hannobraun.com>
|
||||||
|
Henri Yandell <flamefew at gmail.com>
|
||||||
|
Hirotaka Yamamoto <ymmt2005 at gmail.com>
|
||||||
|
Huyiguang <hyg at webterren.com>
|
||||||
|
ICHINOSE Shogo <shogo82148 at gmail.com>
|
||||||
|
Ilia Cimpoes <ichimpoesh at gmail.com>
|
||||||
|
INADA Naoki <songofacandy at gmail.com>
|
||||||
|
Jacek Szwec <szwec.jacek at gmail.com>
|
||||||
|
Jakub Adamus <kratky at zobak.cz>
|
||||||
|
James Harr <james.harr at gmail.com>
|
||||||
|
Janek Vedock <janekvedock at comcast.net>
|
||||||
|
Jason Ng <oblitorum at gmail.com>
|
||||||
|
Jean-Yves Pellé <jy at pelle.link>
|
||||||
|
Jeff Hodges <jeff at somethingsimilar.com>
|
||||||
|
Jeffrey Charles <jeffreycharles at gmail.com>
|
||||||
|
Jennifer Purevsuren <jennifer at dolthub.com>
|
||||||
|
Jerome Meyer <jxmeyer at gmail.com>
|
||||||
|
Jiabin Zhang <jiabin.z at qq.com>
|
||||||
|
Jiajia Zhong <zhong2plus at gmail.com>
|
||||||
|
Jian Zhen <zhenjl at gmail.com>
|
||||||
|
Joe Mann <contact at joemann.co.uk>
|
||||||
|
Joshua Prunier <joshua.prunier at gmail.com>
|
||||||
|
Julien Lefevre <julien.lefevr at gmail.com>
|
||||||
|
Julien Schmidt <go-sql-driver at julienschmidt.com>
|
||||||
|
Justin Li <jli at j-li.net>
|
||||||
|
Justin Nuß <nuss.justin at gmail.com>
|
||||||
|
Kamil Dziedzic <kamil at klecza.pl>
|
||||||
|
Kei Kamikawa <x00.x7f.x86 at gmail.com>
|
||||||
|
Kevin Malachowski <kevin at chowski.com>
|
||||||
|
Kieron Woodhouse <kieron.woodhouse at infosum.com>
|
||||||
|
Lance Tian <lance6716 at gmail.com>
|
||||||
|
Lennart Rudolph <lrudolph at hmc.edu>
|
||||||
|
Leonardo YongUk Kim <dalinaum at gmail.com>
|
||||||
|
Linh Tran Tuan <linhduonggnu at gmail.com>
|
||||||
|
Lion Yang <lion at aosc.xyz>
|
||||||
|
Luca Looz <luca.looz92 at gmail.com>
|
||||||
|
Lucas Liu <extrafliu at gmail.com>
|
||||||
|
Luke Scott <luke at webconnex.com>
|
||||||
|
Lunny Xiao <xiaolunwen at gmail.com>
|
||||||
|
Maciej Zimnoch <maciej.zimnoch at codilime.com>
|
||||||
|
Michael Woolnough <michael.woolnough at gmail.com>
|
||||||
|
Minh Quang <minhquang4334 at gmail.com>
|
||||||
|
Morgan Tocker <tocker at gmail.com>
|
||||||
|
Nao Yokotsuka <yokotukanao at gmail.com>
|
||||||
|
Nathanial Murphy <nathanial.murphy at gmail.com>
|
||||||
|
Nicola Peduzzi <thenikso at gmail.com>
|
||||||
|
Oliver Bone <owbone at github.com>
|
||||||
|
Olivier Mengué <dolmen at cpan.org>
|
||||||
|
oscarzhao <oscarzhaosl at gmail.com>
|
||||||
|
Paul Bonser <misterpib at gmail.com>
|
||||||
|
Paulius Lozys <pauliuslozys at gmail.com>
|
||||||
|
Peter Schultz <peter.schultz at classmarkets.com>
|
||||||
|
Phil Porada <philporada at gmail.com>
|
||||||
|
Rebecca Chin <rchin at pivotal.io>
|
||||||
|
Reed Allman <rdallman10 at gmail.com>
|
||||||
|
Richard Wilkes <wilkes at me.com>
|
||||||
|
Robert Russell <robert at rrbrussell.com>
|
||||||
|
Runrioter Wung <runrioter at gmail.com>
|
||||||
|
Samantha Frank <hello at entropy.cat>
|
||||||
|
Santhosh Kumar Tekuri <santhosh.tekuri at gmail.com>
|
||||||
|
Sho Iizuka <sho.i518 at gmail.com>
|
||||||
|
Sho Ikeda <suicaicoca at gmail.com>
|
||||||
|
Shuode Li <elemount at qq.com>
|
||||||
|
Simon J Mudd <sjmudd at pobox.com>
|
||||||
|
Soroush Pour <me at soroushjp.com>
|
||||||
|
Stan Putrya <root.vagner at gmail.com>
|
||||||
|
Stanley Gunawan <gunawan.stanley at gmail.com>
|
||||||
|
Steven Hartland <steven.hartland at multiplay.co.uk>
|
||||||
|
Tan Jinhua <312841925 at qq.com>
|
||||||
|
Tetsuro Aoki <t.aoki1130 at gmail.com>
|
||||||
|
Thomas Wodarek <wodarekwebpage at gmail.com>
|
||||||
|
Tim Ruffles <timruffles at gmail.com>
|
||||||
|
Tom Jenkinson <tom at tjenkinson.me>
|
||||||
|
Vladimir Kovpak <cn007b at gmail.com>
|
||||||
|
Vladyslav Zhelezniak <zhvladi at gmail.com>
|
||||||
|
Xiangyu Hu <xiangyu.hu at outlook.com>
|
||||||
|
Xiaobing Jiang <s7v7nislands at gmail.com>
|
||||||
|
Xiuming Chen <cc at cxm.cc>
|
||||||
|
Xuehong Chan <chanxuehong at gmail.com>
|
||||||
|
Zhang Xiang <angwerzx at 126.com>
|
||||||
|
Zhenye Xie <xiezhenye at gmail.com>
|
||||||
|
Zhixin Wen <john.wenzhixin at gmail.com>
|
||||||
|
Ziheng Lyu <zihenglv at gmail.com>
|
||||||
|
|
||||||
|
# Organizations
|
||||||
|
|
||||||
|
Barracuda Networks, Inc.
|
||||||
|
Block, Inc.
|
||||||
|
Counting Ltd.
|
||||||
|
Defined Networking Inc.
|
||||||
|
DigitalOcean Inc.
|
||||||
|
Dolthub Inc.
|
||||||
|
dyves labs AG
|
||||||
|
Facebook Inc.
|
||||||
|
GitHub Inc.
|
||||||
|
Google Inc.
|
||||||
|
InfoSum Ltd.
|
||||||
|
Keybase Inc.
|
||||||
|
Microsoft Corp.
|
||||||
|
Multiplay Ltd.
|
||||||
|
Percona LLC
|
||||||
|
PingCAP Inc.
|
||||||
|
Pivotal Inc.
|
||||||
|
Shattered Silicon Ltd.
|
||||||
|
Stripe Inc.
|
||||||
|
ThousandEyes
|
||||||
|
Zendesk Inc.
|
||||||
373
vendor/github.com/go-sql-driver/mysql/CHANGELOG.md
generated
vendored
Normal file
373
vendor/github.com/go-sql-driver/mysql/CHANGELOG.md
generated
vendored
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v1.10.0 (2026-04-28)
|
||||||
|
|
||||||
|
* Fix `getSystemVar("max_allowed_packet")` potentially returned wrong value. (#1754)
|
||||||
|
This affects only when `maxAllowedPacket=0` is set.
|
||||||
|
|
||||||
|
* Bump filippo.io/edwards25519 from 1.1.1 to 1.2.0. (#1756)
|
||||||
|
While older versions have reported CVEs, they do not affect go-mysql.
|
||||||
|
|
||||||
|
* Update Go versions to 1.24-1.26. (#1763)
|
||||||
|
|
||||||
|
* Enhance interpolateParams to correctly handle placeholders. (#1732)
|
||||||
|
The question mark (?) within strings and comments will no longer be treated as a placeholder.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.9.3 (2025-06-13)
|
||||||
|
|
||||||
|
* `tx.Commit()` and `tx.Rollback()` returned `ErrInvalidConn` always.
|
||||||
|
Now they return cached real error if present. (#1690)
|
||||||
|
|
||||||
|
* Optimize reading small result sets to fix a performance regression
|
||||||
|
introduced by compression protocol support. (`#1707`)
|
||||||
|
* Fix `db.Ping()` on compressed connection. (#1723)
|
||||||
|
|
||||||
|
|
||||||
|
## v1.9.2 (2025-04-07)
|
||||||
|
|
||||||
|
v1.9.2 is a re-release of v1.9.1 due to a release process issue; no changes were made to the content.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.9.1 (2025-03-21)
|
||||||
|
|
||||||
|
### Major Changes
|
||||||
|
|
||||||
|
* Add Charset() option. (#1679)
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
* go.mod: fix go version format (#1682)
|
||||||
|
* Fix FormatDSN missing ConnectionAttributes (#1619)
|
||||||
|
|
||||||
|
## v1.9.0 (2025-02-18)
|
||||||
|
|
||||||
|
### Major Changes
|
||||||
|
|
||||||
|
- Implement zlib compression. (#1487)
|
||||||
|
- Supported Go version is updated to Go 1.21+. (#1639)
|
||||||
|
- Add support for VECTOR type introduced in MySQL 9.0. (#1609)
|
||||||
|
- Config object can have custom dial function. (#1527)
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fix auth errors when username/password are too long. (#1625)
|
||||||
|
- Check if MySQL supports CLIENT_CONNECT_ATTRS before sending client attributes. (#1640)
|
||||||
|
- Fix auth switch request handling. (#1666)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
- Add "filename:line" prefix to log in go-mysql. Custom loggers now show it. (#1589)
|
||||||
|
- Improve error handling. It reduces the "busy buffer" errors. (#1595, #1601, #1641)
|
||||||
|
- Use `strconv.Atoi` to parse max_allowed_packet. (#1661)
|
||||||
|
- `rejectReadOnly` option now handles ER_READ_ONLY_MODE (1290) error too. (#1660)
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.8.1 (2024-03-26)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- fix race condition when context is canceled in [#1562](https://github.com/go-sql-driver/mysql/pull/1562) and [#1570](https://github.com/go-sql-driver/mysql/pull/1570)
|
||||||
|
|
||||||
|
## Version 1.8.0 (2024-03-09)
|
||||||
|
|
||||||
|
Major Changes:
|
||||||
|
|
||||||
|
- Use `SET NAMES charset COLLATE collation`. by @methane in [#1437](https://github.com/go-sql-driver/mysql/pull/1437)
|
||||||
|
- Older go-mysql-driver used `collation_id` in the handshake packet. But it caused collation mismatch in some situation.
|
||||||
|
- If you don't specify charset nor collation, go-mysql-driver sends `SET NAMES utf8mb4` for new connection. This uses server's default collation for utf8mb4.
|
||||||
|
- If you specify charset, go-mysql-driver sends `SET NAMES <charset>`. This uses the server's default collation for `<charset>`.
|
||||||
|
- If you specify collation and/or charset, go-mysql-driver sends `SET NAMES charset COLLATE collation`.
|
||||||
|
- PathEscape dbname in DSN. by @methane in [#1432](https://github.com/go-sql-driver/mysql/pull/1432)
|
||||||
|
- This is backward incompatible in rare case. Check your DSN.
|
||||||
|
- Drop Go 1.13-17 support by @methane in [#1420](https://github.com/go-sql-driver/mysql/pull/1420)
|
||||||
|
- Use Go 1.18+
|
||||||
|
- Parse numbers on text protocol too by @methane in [#1452](https://github.com/go-sql-driver/mysql/pull/1452)
|
||||||
|
- When text protocol is used, go-mysql-driver passed bare `[]byte` to database/sql for avoid unnecessary allocation and conversion.
|
||||||
|
- If user specified `*any` to `Scan()`, database/sql passed the `[]byte` into the target variable.
|
||||||
|
- This confused users because most user doesn't know when text/binary protocol used.
|
||||||
|
- go-mysql-driver 1.8 converts integer/float values into int64/double even in text protocol. This doesn't increase allocation compared to `[]byte` and conversion cost is negatable.
|
||||||
|
- New options start using the Functional Option Pattern to avoid increasing technical debt in the Config object. Future version may introduce Functional Option for existing options, but not for now.
|
||||||
|
- Make TimeTruncate functional option by @methane in [1552](https://github.com/go-sql-driver/mysql/pull/1552)
|
||||||
|
- Add BeforeConnect callback to configuration object by @ItalyPaleAle in [#1469](https://github.com/go-sql-driver/mysql/pull/1469)
|
||||||
|
|
||||||
|
|
||||||
|
Other changes:
|
||||||
|
|
||||||
|
- Adding DeregisterDialContext to prevent memory leaks with dialers we don't need anymore by @jypelle in https://github.com/go-sql-driver/mysql/pull/1422
|
||||||
|
- Make logger configurable per connection by @frozenbonito in https://github.com/go-sql-driver/mysql/pull/1408
|
||||||
|
- Fix ColumnType.DatabaseTypeName for mediumint unsigned by @evanelias in https://github.com/go-sql-driver/mysql/pull/1428
|
||||||
|
- Add connection attributes by @Daemonxiao in https://github.com/go-sql-driver/mysql/pull/1389
|
||||||
|
- Stop `ColumnTypeScanType()` from returning `sql.RawBytes` by @methane in https://github.com/go-sql-driver/mysql/pull/1424
|
||||||
|
- Exec() now provides access to status of multiple statements. by @mherr-google in https://github.com/go-sql-driver/mysql/pull/1309
|
||||||
|
- Allow to change (or disable) the default driver name for registration by @dolmen in https://github.com/go-sql-driver/mysql/pull/1499
|
||||||
|
- Add default connection attribute '_server_host' by @oblitorum in https://github.com/go-sql-driver/mysql/pull/1506
|
||||||
|
- QueryUnescape DSN ConnectionAttribute value by @zhangyangyu in https://github.com/go-sql-driver/mysql/pull/1470
|
||||||
|
- Add client_ed25519 authentication by @Gusted in https://github.com/go-sql-driver/mysql/pull/1518
|
||||||
|
|
||||||
|
## Version 1.7.1 (2023-04-25)
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- bump actions/checkout@v3 and actions/setup-go@v3 (#1375)
|
||||||
|
- Add go1.20 and mariadb10.11 to the testing matrix (#1403)
|
||||||
|
- Increase default maxAllowedPacket size. (#1411)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Use SET syntax as specified in the MySQL documentation (#1402)
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.7 (2022-11-29)
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Drop support of Go 1.12 (#1211)
|
||||||
|
- Refactoring `(*textRows).readRow` in a more clear way (#1230)
|
||||||
|
- util: Reduce boundary check in escape functions. (#1316)
|
||||||
|
- enhancement for mysqlConn handleAuthResult (#1250)
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
|
||||||
|
- support Is comparison on MySQLError (#1210)
|
||||||
|
- return unsigned in database type name when necessary (#1238)
|
||||||
|
- Add API to express like a --ssl-mode=PREFERRED MySQL client (#1370)
|
||||||
|
- Add SQLState to MySQLError (#1321)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Fix parsing 0 year. (#1257)
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.6 (2021-04-01)
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Migrate the CI service from travis-ci to GitHub Actions (#1176, #1183, #1190)
|
||||||
|
- `NullTime` is deprecated (#960, #1144)
|
||||||
|
- Reduce allocations when building SET command (#1111)
|
||||||
|
- Performance improvement for time formatting (#1118)
|
||||||
|
- Performance improvement for time parsing (#1098, #1113)
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
|
||||||
|
- Implement `driver.Validator` interface (#1106, #1174)
|
||||||
|
- Support returning `uint64` from `Valuer` in `ConvertValue` (#1143)
|
||||||
|
- Add `json.RawMessage` for converter and prepared statement (#1059)
|
||||||
|
- Interpolate `json.RawMessage` as `string` (#1058)
|
||||||
|
- Implements `CheckNamedValue` (#1090)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Stop rounding times (#1121, #1172)
|
||||||
|
- Put zero filler into the SSL handshake packet (#1066)
|
||||||
|
- Fix checking cancelled connections back into the connection pool (#1095)
|
||||||
|
- Fix remove last 0 byte for mysql_old_password when password is empty (#1133)
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.5 (2020-01-07)
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Dropped support Go 1.9 and lower (#823, #829, #886, #1016, #1017)
|
||||||
|
- Improve buffer handling (#890)
|
||||||
|
- Document potentially insecure TLS configs (#901)
|
||||||
|
- Use a double-buffering scheme to prevent data races (#943)
|
||||||
|
- Pass uint64 values without converting them to string (#838, #955)
|
||||||
|
- Update collations and make utf8mb4 default (#877, #1054)
|
||||||
|
- Make NullTime compatible with sql.NullTime in Go 1.13+ (#995)
|
||||||
|
- Removed CloudSQL support (#993, #1007)
|
||||||
|
- Add Go Module support (#1003)
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
|
||||||
|
- Implement support of optional TLS (#900)
|
||||||
|
- Check connection liveness (#934, #964, #997, #1048, #1051, #1052)
|
||||||
|
- Implement Connector Interface (#941, #958, #1020, #1035)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Mark connections as bad on error during ping (#875)
|
||||||
|
- Mark connections as bad on error during dial (#867)
|
||||||
|
- Fix connection leak caused by rapid context cancellation (#1024)
|
||||||
|
- Mark connections as bad on error during Conn.Prepare (#1030)
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.4.1 (2018-11-14)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Fix TIME format for binary columns (#818)
|
||||||
|
- Fix handling of empty auth plugin names (#835)
|
||||||
|
- Fix caching_sha2_password with empty password (#826)
|
||||||
|
- Fix canceled context broke mysqlConn (#862)
|
||||||
|
- Fix OldAuthSwitchRequest support (#870)
|
||||||
|
- Fix Auth Response packet for cleartext password (#887)
|
||||||
|
|
||||||
|
## Version 1.4 (2018-06-03)
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Documentation fixes (#530, #535, #567)
|
||||||
|
- Refactoring (#575, #579, #580, #581, #603, #615, #704)
|
||||||
|
- Cache column names (#444)
|
||||||
|
- Sort the DSN parameters in DSNs generated from a config (#637)
|
||||||
|
- Allow native password authentication by default (#644)
|
||||||
|
- Use the default port if it is missing in the DSN (#668)
|
||||||
|
- Removed the `strict` mode (#676)
|
||||||
|
- Do not query `max_allowed_packet` by default (#680)
|
||||||
|
- Dropped support Go 1.6 and lower (#696)
|
||||||
|
- Updated `ConvertValue()` to match the database/sql/driver implementation (#760)
|
||||||
|
- Document the usage of `0000-00-00T00:00:00` as the time.Time zero value (#783)
|
||||||
|
- Improved the compatibility of the authentication system (#807)
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
|
||||||
|
- Multi-Results support (#537)
|
||||||
|
- `rejectReadOnly` DSN option (#604)
|
||||||
|
- `context.Context` support (#608, #612, #627, #761)
|
||||||
|
- Transaction isolation level support (#619, #744)
|
||||||
|
- Read-Only transactions support (#618, #634)
|
||||||
|
- `NewConfig` function which initializes a config with default values (#679)
|
||||||
|
- Implemented the `ColumnType` interfaces (#667, #724)
|
||||||
|
- Support for custom string types in `ConvertValue` (#623)
|
||||||
|
- Implemented `NamedValueChecker`, improving support for uint64 with high bit set (#690, #709, #710)
|
||||||
|
- `caching_sha2_password` authentication plugin support (#794, #800, #801, #802)
|
||||||
|
- Implemented `driver.SessionResetter` (#779)
|
||||||
|
- `sha256_password` authentication plugin support (#808)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Use the DSN hostname as TLS default ServerName if `tls=true` (#564, #718)
|
||||||
|
- Fixed LOAD LOCAL DATA INFILE for empty files (#590)
|
||||||
|
- Removed columns definition cache since it sometimes cached invalid data (#592)
|
||||||
|
- Don't mutate registered TLS configs (#600)
|
||||||
|
- Make RegisterTLSConfig concurrency-safe (#613)
|
||||||
|
- Handle missing auth data in the handshake packet correctly (#646)
|
||||||
|
- Do not retry queries when data was written to avoid data corruption (#302, #736)
|
||||||
|
- Cache the connection pointer for error handling before invalidating it (#678)
|
||||||
|
- Fixed imports for appengine/cloudsql (#700)
|
||||||
|
- Fix sending STMT_LONG_DATA for 0 byte data (#734)
|
||||||
|
- Set correct capacity for []bytes read from length-encoded strings (#766)
|
||||||
|
- Make RegisterDial concurrency-safe (#773)
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.3 (2016-12-01)
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Go 1.1 is no longer supported
|
||||||
|
- Use decimals fields in MySQL to format time types (#249)
|
||||||
|
- Buffer optimizations (#269)
|
||||||
|
- TLS ServerName defaults to the host (#283)
|
||||||
|
- Refactoring (#400, #410, #437)
|
||||||
|
- Adjusted documentation for second generation CloudSQL (#485)
|
||||||
|
- Documented DSN system var quoting rules (#502)
|
||||||
|
- Made statement.Close() calls idempotent to avoid errors in Go 1.6+ (#512)
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
|
||||||
|
- Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249)
|
||||||
|
- Support for returning table alias on Columns() (#289, #359, #382)
|
||||||
|
- Placeholder interpolation, can be activated with the DSN parameter `interpolateParams=true` (#309, #318, #490)
|
||||||
|
- Support for uint64 parameters with high bit set (#332, #345)
|
||||||
|
- Cleartext authentication plugin support (#327)
|
||||||
|
- Exported ParseDSN function and the Config struct (#403, #419, #429)
|
||||||
|
- Read / Write timeouts (#401)
|
||||||
|
- Support for JSON field type (#414)
|
||||||
|
- Support for multi-statements and multi-results (#411, #431)
|
||||||
|
- DSN parameter to set the driver-side max_allowed_packet value manually (#489)
|
||||||
|
- Native password authentication plugin support (#494, #524)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Fixed handling of queries without columns and rows (#255)
|
||||||
|
- Fixed a panic when SetKeepAlive() failed (#298)
|
||||||
|
- Handle ERR packets while reading rows (#321)
|
||||||
|
- Fixed reading NULL length-encoded integers in MySQL 5.6+ (#349)
|
||||||
|
- Fixed absolute paths support in LOAD LOCAL DATA INFILE (#356)
|
||||||
|
- Actually zero out bytes in handshake response (#378)
|
||||||
|
- Fixed race condition in registering LOAD DATA INFILE handler (#383)
|
||||||
|
- Fixed tests with MySQL 5.7.9+ (#380)
|
||||||
|
- QueryUnescape TLS config names (#397)
|
||||||
|
- Fixed "broken pipe" error by writing to closed socket (#390)
|
||||||
|
- Fixed LOAD LOCAL DATA INFILE buffering (#424)
|
||||||
|
- Fixed parsing of floats into float64 when placeholders are used (#434)
|
||||||
|
- Fixed DSN tests with Go 1.7+ (#459)
|
||||||
|
- Handle ERR packets while waiting for EOF (#473)
|
||||||
|
- Invalidate connection on error while discarding additional results (#513)
|
||||||
|
- Allow terminating packets of length 0 (#516)
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.2 (2014-06-03)
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- We switched back to a "rolling release". `go get` installs the current master branch again
|
||||||
|
- Version v1 of the driver will not be maintained anymore. Go 1.0 is no longer supported by this driver
|
||||||
|
- Exported errors to allow easy checking from application code
|
||||||
|
- Enabled TCP Keepalives on TCP connections
|
||||||
|
- Optimized INFILE handling (better buffer size calculation, lazy init, ...)
|
||||||
|
- The DSN parser also checks for a missing separating slash
|
||||||
|
- Faster binary date / datetime to string formatting
|
||||||
|
- Also exported the MySQLWarning type
|
||||||
|
- mysqlConn.Close returns the first error encountered instead of ignoring all errors
|
||||||
|
- writePacket() automatically writes the packet size to the header
|
||||||
|
- readPacket() uses an iterative approach instead of the recursive approach to merge split packets
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
|
||||||
|
- `RegisterDial` allows the usage of a custom dial function to establish the network connection
|
||||||
|
- Setting the connection collation is possible with the `collation` DSN parameter. This parameter should be preferred over the `charset` parameter
|
||||||
|
- Logging of critical errors is configurable with `SetLogger`
|
||||||
|
- Google CloudSQL support
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Allow more than 32 parameters in prepared statements
|
||||||
|
- Various old_password fixes
|
||||||
|
- Fixed TestConcurrent test to pass Go's race detection
|
||||||
|
- Fixed appendLengthEncodedInteger for large numbers
|
||||||
|
- Renamed readLengthEnodedString to readLengthEncodedString and skipLengthEnodedString to skipLengthEncodedString (fixed typo)
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.1 (2013-11-02)
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Go-MySQL-Driver now requires Go 1.1
|
||||||
|
- Connections now use the collation `utf8_general_ci` by default. Adding `&charset=UTF8` to the DSN should not be necessary anymore
|
||||||
|
- Made closing rows and connections error tolerant. This allows for example deferring rows.Close() without checking for errors
|
||||||
|
- `[]byte(nil)` is now treated as a NULL value. Before, it was treated like an empty string / `[]byte("")`
|
||||||
|
- DSN parameter values must now be url.QueryEscape'ed. This allows text values to contain special characters, such as '&'.
|
||||||
|
- Use the IO buffer also for writing. This results in zero allocations (by the driver) for most queries
|
||||||
|
- Optimized the buffer for reading
|
||||||
|
- stmt.Query now caches column metadata
|
||||||
|
- New Logo
|
||||||
|
- Changed the copyright header to include all contributors
|
||||||
|
- Improved the LOAD INFILE documentation
|
||||||
|
- The driver struct is now exported to make the driver directly accessible
|
||||||
|
- Refactored the driver tests
|
||||||
|
- Added more benchmarks and moved all to a separate file
|
||||||
|
- Other small refactoring
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
|
||||||
|
- Added *old_passwords* support: Required in some cases, but must be enabled by adding `allowOldPasswords=true` to the DSN since it is insecure
|
||||||
|
- Added a `clientFoundRows` parameter: Return the number of matching rows instead of the number of rows changed on UPDATEs
|
||||||
|
- Added TLS/SSL support: Use a TLS/SSL encrypted connection to the server. Custom TLS configs can be registered and used
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
- Fixed MySQL 4.1 support: MySQL 4.1 sends packets with lengths which differ from the specification
|
||||||
|
- Convert to DB timezone when inserting `time.Time`
|
||||||
|
- Split packets (more than 16MB) are now merged correctly
|
||||||
|
- Fixed false positive `io.EOF` errors when the data was fully read
|
||||||
|
- Avoid panics on reuse of closed connections
|
||||||
|
- Fixed empty string producing false nil values
|
||||||
|
- Fixed sign byte for positive TIME fields
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.0 (2013-05-14)
|
||||||
|
|
||||||
|
Initial Release
|
||||||
373
vendor/github.com/go-sql-driver/mysql/LICENSE
generated
vendored
Normal file
373
vendor/github.com/go-sql-driver/mysql/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
598
vendor/github.com/go-sql-driver/mysql/README.md
generated
vendored
Normal file
598
vendor/github.com/go-sql-driver/mysql/README.md
generated
vendored
Normal file
@@ -0,0 +1,598 @@
|
|||||||
|
# Go-MySQL-Driver
|
||||||
|
|
||||||
|
[](https://deepwiki.com/go-sql-driver/mysql)
|
||||||
|
|
||||||
|
|
||||||
|
A MySQL-Driver for Go's [database/sql](https://golang.org/pkg/database/sql/) package
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---------------------------------------
|
||||||
|
* [Features](#features)
|
||||||
|
* [Requirements](#requirements)
|
||||||
|
* [Installation](#installation)
|
||||||
|
* [Usage](#usage)
|
||||||
|
* [DSN (Data Source Name)](#dsn-data-source-name)
|
||||||
|
* [Password](#password)
|
||||||
|
* [Protocol](#protocol)
|
||||||
|
* [Address](#address)
|
||||||
|
* [Parameters](#parameters)
|
||||||
|
* [Examples](#examples)
|
||||||
|
* [Connection pool and timeouts](#connection-pool-and-timeouts)
|
||||||
|
* [context.Context Support](#contextcontext-support)
|
||||||
|
* [ColumnType Support](#columntype-support)
|
||||||
|
* [LOAD DATA LOCAL INFILE support](#load-data-local-infile-support)
|
||||||
|
* [time.Time support](#timetime-support)
|
||||||
|
* [Unicode support](#unicode-support)
|
||||||
|
* [Testing / Development](#testing--development)
|
||||||
|
* [License](#license)
|
||||||
|
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
## Features
|
||||||
|
* Lightweight and [fast](https://github.com/go-sql-driver/sql-benchmark "golang MySQL-Driver performance")
|
||||||
|
* Native Go implementation. No C-bindings, just pure Go
|
||||||
|
* Connections over TCP/IPv4, TCP/IPv6, Unix domain sockets or [custom protocols](https://godoc.org/github.com/go-sql-driver/mysql#DialFunc)
|
||||||
|
* Automatic handling of broken connections
|
||||||
|
* Automatic Connection Pooling *(by database/sql package)*
|
||||||
|
* Supports queries larger than 16MB
|
||||||
|
* Full [`sql.RawBytes`](https://golang.org/pkg/database/sql/#RawBytes) support.
|
||||||
|
* Intelligent `LONG DATA` handling in prepared statements
|
||||||
|
* Secure `LOAD DATA LOCAL INFILE` support with file allowlisting and `io.Reader` support
|
||||||
|
* Optional `time.Time` parsing
|
||||||
|
* Optional placeholder interpolation
|
||||||
|
* Supports zlib compression.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* Go 1.24 or higher. We aim to support the 3 latest versions of Go.
|
||||||
|
* MySQL (5.7+) and MariaDB (10.5+) are supported by maintainers.
|
||||||
|
* [TiDB](https://github.com/pingcap/tidb) is supported by PingCAP.
|
||||||
|
* Do not ask questions about TiDB in our issue tracker or forum.
|
||||||
|
* [Document](https://docs.pingcap.com/tidb/v6.1/dev-guide-sample-application-golang)
|
||||||
|
* [Forum](https://ask.pingcap.com/)
|
||||||
|
* go-mysql would work with Percona Server, Google CloudSQL or Sphinx (2.2.3+).
|
||||||
|
* Maintainers won't support them. Do not expect issues are investigated and resolved by maintainers.
|
||||||
|
* Investigate issues yourself and please send a pull request to fix it.
|
||||||
|
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Simple install the package to your [$GOPATH](https://github.com/golang/go/wiki/GOPATH "GOPATH") with the [go tool](https://golang.org/cmd/go/ "go command") from shell:
|
||||||
|
```bash
|
||||||
|
go get -u github.com/go-sql-driver/mysql
|
||||||
|
```
|
||||||
|
Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
_Go MySQL Driver_ is an implementation of Go's `database/sql/driver` interface. You only need to import the driver and can use the full [`database/sql`](https://golang.org/pkg/database/sql/) API then.
|
||||||
|
|
||||||
|
Use `mysql` as `driverName` and a valid [DSN](#dsn-data-source-name) as `dataSourceName`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
db, err := sql.Open("mysql", "user:password@/dbname")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// See "Important settings" section.
|
||||||
|
db.SetConnMaxLifetime(time.Minute * 3)
|
||||||
|
db.SetMaxOpenConns(10)
|
||||||
|
db.SetMaxIdleConns(10)
|
||||||
|
```
|
||||||
|
|
||||||
|
[Examples are available in our Wiki](https://github.com/go-sql-driver/mysql/wiki/Examples "Go-MySQL-Driver Examples").
|
||||||
|
|
||||||
|
### Important settings
|
||||||
|
|
||||||
|
`db.SetConnMaxLifetime()` is required to ensure connections are closed by the driver safely before connection is closed by MySQL server, OS, or other middlewares. Since some middlewares close idle connections by 5 minutes, we recommend timeout shorter than 5 minutes. This setting helps load balancing and changing system variables too.
|
||||||
|
|
||||||
|
`db.SetMaxOpenConns()` is highly recommended to limit the number of connection used by the application. There is no recommended limit number because it depends on application and MySQL server.
|
||||||
|
|
||||||
|
`db.SetMaxIdleConns()` is recommended to be set same to `db.SetMaxOpenConns()`. When it is smaller than `SetMaxOpenConns()`, connections can be opened and closed much more frequently than you expect. Idle connections can be closed by the `db.SetConnMaxLifetime()`. If you want to close idle connections more rapidly, you can use `db.SetConnMaxIdleTime()` since Go 1.15.
|
||||||
|
|
||||||
|
|
||||||
|
### DSN (Data Source Name)
|
||||||
|
|
||||||
|
The Data Source Name has a common format, like e.g. [PEAR DB](http://pear.php.net/manual/en/package.database.db.intro-dsn.php) uses it, but without type-prefix (optional parts marked by squared brackets):
|
||||||
|
```
|
||||||
|
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
|
||||||
|
```
|
||||||
|
|
||||||
|
A DSN in its fullest form:
|
||||||
|
```
|
||||||
|
username:password@protocol(address)/dbname?param=value
|
||||||
|
```
|
||||||
|
|
||||||
|
Except for the databasename, all values are optional. So the minimal DSN is:
|
||||||
|
```
|
||||||
|
/dbname
|
||||||
|
```
|
||||||
|
|
||||||
|
If you do not want to preselect a database, leave `dbname` empty:
|
||||||
|
```
|
||||||
|
/
|
||||||
|
```
|
||||||
|
This has the same effect as an empty DSN string:
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
`dbname` is escaped by [PathEscape()](https://pkg.go.dev/net/url#PathEscape) since v1.8.0. If your database name is `dbname/withslash`, it becomes:
|
||||||
|
|
||||||
|
```
|
||||||
|
/dbname%2Fwithslash
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mysql#Config.FormatDSN) can be used to create a DSN string by filling a struct.
|
||||||
|
|
||||||
|
#### Password
|
||||||
|
Passwords can consist of any character. Escaping is **not** necessary.
|
||||||
|
|
||||||
|
#### Protocol
|
||||||
|
See [net.Dial](https://golang.org/pkg/net/#Dial) for more information which networks are available.
|
||||||
|
In general you should use a Unix domain socket if available and TCP otherwise for best performance.
|
||||||
|
|
||||||
|
#### Address
|
||||||
|
For TCP and UDP networks, addresses have the form `host[:port]`.
|
||||||
|
If `port` is omitted, the default port will be used.
|
||||||
|
If `host` is a literal IPv6 address, it must be enclosed in square brackets.
|
||||||
|
The functions [net.JoinHostPort](https://golang.org/pkg/net/#JoinHostPort) and [net.SplitHostPort](https://golang.org/pkg/net/#SplitHostPort) manipulate addresses in this form.
|
||||||
|
|
||||||
|
For Unix domain sockets the address is the absolute path to the MySQL-Server-socket, e.g. `/var/run/mysqld/mysqld.sock` or `/tmp/mysql.sock`.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
*Parameters are case-sensitive!*
|
||||||
|
|
||||||
|
Notice that any of `true`, `TRUE`, `True` or `1` is accepted to stand for a true boolean value. Not surprisingly, false can be specified as any of: `false`, `FALSE`, `False` or `0`.
|
||||||
|
|
||||||
|
##### `allowAllFiles`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`allowAllFiles=true` disables the file allowlist for `LOAD DATA LOCAL INFILE` and allows *all* files.
|
||||||
|
[*Might be insecure!*](https://dev.mysql.com/doc/refman/8.0/en/load-data.html#load-data-local)
|
||||||
|
|
||||||
|
##### `allowCleartextPasswords`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`allowCleartextPasswords=true` allows using the [cleartext client side plugin](https://dev.mysql.com/doc/en/cleartext-pluggable-authentication.html) if required by an account, such as one defined with the [PAM authentication plugin](http://dev.mysql.com/doc/en/pam-authentication-plugin.html). Sending passwords in clear text may be a security problem in some configurations. To avoid problems if there is any possibility that the password would be intercepted, clients should connect to MySQL Server using a method that protects the password. Possibilities include [TLS / SSL](#tls), IPsec, or a private network.
|
||||||
|
|
||||||
|
|
||||||
|
##### `allowFallbackToPlaintext`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`allowFallbackToPlaintext=true` acts like a `--ssl-mode=PREFERRED` MySQL client as described in [Command Options for Connecting to the Server](https://dev.mysql.com/doc/refman/5.7/en/connection-options.html#option_general_ssl-mode)
|
||||||
|
|
||||||
|
##### `allowNativePasswords`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: true
|
||||||
|
```
|
||||||
|
`allowNativePasswords=false` disallows the usage of MySQL native password method.
|
||||||
|
|
||||||
|
##### `allowOldPasswords`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
`allowOldPasswords=true` allows the usage of the insecure old password method. This should be avoided, but is necessary in some cases. See also [the old_passwords wiki page](https://github.com/go-sql-driver/mysql/wiki/old_passwords).
|
||||||
|
|
||||||
|
##### `charset`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: string
|
||||||
|
Valid Values: <name>
|
||||||
|
Default: none
|
||||||
|
```
|
||||||
|
|
||||||
|
Sets the charset used for client-server interaction (`"SET NAMES <value>"`). If multiple charsets are set (separated by a comma), the following charset is used if setting the charset fails. This enables for example support for `utf8mb4` ([introduced in MySQL 5.5.3](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)) with fallback to `utf8` for older servers (`charset=utf8mb4,utf8`).
|
||||||
|
|
||||||
|
See also [Unicode Support](#unicode-support).
|
||||||
|
|
||||||
|
##### `checkConnLiveness`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: true
|
||||||
|
```
|
||||||
|
|
||||||
|
On supported platforms connections retrieved from the connection pool are checked for liveness before using them. If the check fails, the respective connection is marked as bad and the query retried with another connection.
|
||||||
|
`checkConnLiveness=false` disables this liveness check of connections.
|
||||||
|
|
||||||
|
##### `collation`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: string
|
||||||
|
Valid Values: <name>
|
||||||
|
Default: utf8mb4_general_ci
|
||||||
|
```
|
||||||
|
|
||||||
|
Sets the collation used for client-server interaction on connection. In contrast to `charset`, `collation` does not issue additional queries. If the specified collation is unavailable on the target server, the connection will fail.
|
||||||
|
|
||||||
|
A list of valid charsets for a server is retrievable with `SHOW COLLATION`.
|
||||||
|
|
||||||
|
The default collation (`utf8mb4_general_ci`) is supported from MySQL 5.5. You should use an older collation (e.g. `utf8_general_ci`) for older MySQL.
|
||||||
|
|
||||||
|
Collations for charset "ucs2", "utf16", "utf16le", and "utf32" can not be used ([ref](https://dev.mysql.com/doc/refman/5.7/en/charset-connection.html#charset-connection-impermissible-client-charset)).
|
||||||
|
|
||||||
|
See also [Unicode Support](#unicode-support).
|
||||||
|
|
||||||
|
##### `clientFoundRows`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`clientFoundRows=true` causes an UPDATE to return the number of matching rows instead of the number of rows changed.
|
||||||
|
|
||||||
|
##### `columnsWithAlias`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
When `columnsWithAlias` is true, calls to `sql.Rows.Columns()` will return the table alias and the column name separated by a dot. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
SELECT u.id FROM users as u
|
||||||
|
```
|
||||||
|
|
||||||
|
will return `u.id` instead of just `id` if `columnsWithAlias=true`.
|
||||||
|
|
||||||
|
##### `compress`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Toggles zlib compression. false by default.
|
||||||
|
|
||||||
|
##### `interpolateParams`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
If `interpolateParams` is true, placeholders (`?`) in calls to `db.Query()` and `db.Exec()` are interpolated into a single query string with given parameters. This reduces the number of roundtrips, since the driver has to prepare a statement, execute it with given parameters and close the statement again with `interpolateParams=false`.
|
||||||
|
|
||||||
|
*This can not be used together with the multibyte encodings BIG5, CP932, GB2312, GBK or SJIS. These are rejected as they may [introduce a SQL injection vulnerability](http://stackoverflow.com/a/12118602/3430118)!*
|
||||||
|
|
||||||
|
##### `loc`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: string
|
||||||
|
Valid Values: <escaped name>
|
||||||
|
Default: UTC
|
||||||
|
```
|
||||||
|
|
||||||
|
Sets the location for time.Time values (when using `parseTime=true`). *"Local"* sets the system's location. See [time.LoadLocation](https://golang.org/pkg/time/#LoadLocation) for details.
|
||||||
|
|
||||||
|
Note that this sets the location for time.Time values but does not change MySQL's [time_zone setting](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html). For that see the [time_zone system variable](#system-variables), which can also be set as a DSN parameter.
|
||||||
|
|
||||||
|
Please keep in mind, that param values must be [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`.
|
||||||
|
|
||||||
|
##### `timeTruncate`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: duration
|
||||||
|
Default: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
[Truncate time values](https://pkg.go.dev/time#Duration.Truncate) to the specified duration. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
|
||||||
|
|
||||||
|
##### `maxAllowedPacket`
|
||||||
|
```
|
||||||
|
Type: decimal number
|
||||||
|
Default: 64*1024*1024
|
||||||
|
```
|
||||||
|
|
||||||
|
Max packet size allowed in bytes. The default value is 64 MiB and should be adjusted to match the server settings. `maxAllowedPacket=0` can be used to automatically fetch the `max_allowed_packet` variable from server *on every connection*.
|
||||||
|
|
||||||
|
##### `multiStatements`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Allow multiple statements in one query. This can be used to bach multiple queries. Use [Rows.NextResultSet()](https://pkg.go.dev/database/sql#Rows.NextResultSet) to get result of the second and subsequent queries.
|
||||||
|
|
||||||
|
When `multiStatements` is used, `?` parameters must only be used in the first statement. [interpolateParams](#interpolateparams) can be used to avoid this limitation unless prepared statement is used explicitly.
|
||||||
|
|
||||||
|
It's possible to access the last inserted ID and number of affected rows for multiple statements by using `sql.Conn.Raw()` and the `mysql.Result`. For example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
conn, _ := db.Conn(ctx)
|
||||||
|
conn.Raw(func(conn any) error {
|
||||||
|
ex := conn.(driver.Execer)
|
||||||
|
res, err := ex.Exec(`
|
||||||
|
UPDATE point SET x = 1 WHERE y = 2;
|
||||||
|
UPDATE point SET x = 2 WHERE y = 3;
|
||||||
|
`, nil)
|
||||||
|
// Both slices have 2 elements.
|
||||||
|
log.Print(res.(mysql.Result).AllRowsAffected())
|
||||||
|
log.Print(res.(mysql.Result).AllLastInsertIds())
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
##### `parseTime`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`parseTime=true` changes the output type of `DATE` and `DATETIME` values to `time.Time` instead of `[]byte` / `string`
|
||||||
|
The date or datetime like `0000-00-00 00:00:00` is converted into zero value of `time.Time`.
|
||||||
|
|
||||||
|
|
||||||
|
##### `readTimeout`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: duration
|
||||||
|
Default: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
I/O read timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
|
||||||
|
|
||||||
|
##### `rejectReadOnly`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool
|
||||||
|
Valid Values: true, false
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
`rejectReadOnly=true` causes the driver to reject read-only connections. This
|
||||||
|
is for a possible race condition during an automatic failover, where the mysql
|
||||||
|
client gets connected to a read-only replica after the failover.
|
||||||
|
|
||||||
|
Note that this should be a fairly rare case, as an automatic failover normally
|
||||||
|
happens when the primary is down, and the race condition shouldn't happen
|
||||||
|
unless it comes back up online as soon as the failover is kicked off. On the
|
||||||
|
other hand, when this happens, a MySQL application can get stuck on a
|
||||||
|
read-only connection until restarted. It is however fairly easy to reproduce,
|
||||||
|
for example, using a manual failover on AWS Aurora's MySQL-compatible cluster.
|
||||||
|
|
||||||
|
If you are not relying on read-only transactions to reject writes that aren't
|
||||||
|
supposed to happen, setting this on some MySQL providers (such as AWS Aurora)
|
||||||
|
is safer for failovers.
|
||||||
|
|
||||||
|
Note that ERROR 1290 can be returned for a `read-only` server and this option will
|
||||||
|
cause a retry for that error. However the same error number is used for some
|
||||||
|
other cases. You should ensure your application will never cause an ERROR 1290
|
||||||
|
except for `read-only` mode when enabling this option.
|
||||||
|
|
||||||
|
|
||||||
|
##### `serverPubKey`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: string
|
||||||
|
Valid Values: <name>
|
||||||
|
Default: none
|
||||||
|
```
|
||||||
|
|
||||||
|
Server public keys can be registered with [`mysql.RegisterServerPubKey`](https://godoc.org/github.com/go-sql-driver/mysql#RegisterServerPubKey), which can then be used by the assigned name in the DSN.
|
||||||
|
Public keys are used to transmit encrypted data, e.g. for authentication.
|
||||||
|
If the server's public key is known, it should be set manually to avoid expensive and potentially insecure transmissions of the public key from the server to the client each time it is required.
|
||||||
|
|
||||||
|
|
||||||
|
##### `timeout`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: duration
|
||||||
|
Default: OS default
|
||||||
|
```
|
||||||
|
|
||||||
|
Timeout for establishing connections, aka dial timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
|
||||||
|
|
||||||
|
|
||||||
|
##### `tls`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: bool / string
|
||||||
|
Valid Values: true, false, skip-verify, preferred, <name>
|
||||||
|
Default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`tls=true` enables TLS / SSL encrypted connection to the server. Use `skip-verify` if you want to use a self-signed or invalid certificate (server side) or use `preferred` to use TLS only when advertised by the server. This is similar to `skip-verify`, but additionally allows a fallback to a connection which is not encrypted. Neither `skip-verify` nor `preferred` add any reliable security. You can use a custom TLS config after registering it with [`mysql.RegisterTLSConfig`](https://godoc.org/github.com/go-sql-driver/mysql#RegisterTLSConfig).
|
||||||
|
|
||||||
|
|
||||||
|
##### `writeTimeout`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: duration
|
||||||
|
Default: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
I/O write timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
|
||||||
|
|
||||||
|
##### `connectionAttributes`
|
||||||
|
|
||||||
|
```
|
||||||
|
Type: comma-delimited string of user-defined "key:value" pairs
|
||||||
|
Valid Values: (<name1>:<value1>,<name2>:<value2>,...)
|
||||||
|
Default: none
|
||||||
|
```
|
||||||
|
|
||||||
|
[Connection attributes](https://dev.mysql.com/doc/refman/8.0/en/performance-schema-connection-attribute-tables.html) are key-value pairs that application programs can pass to the server at connect time.
|
||||||
|
|
||||||
|
##### System Variables
|
||||||
|
|
||||||
|
Any other parameters are interpreted as system variables:
|
||||||
|
* `<boolean_var>=<value>`: `SET <boolean_var>=<value>`
|
||||||
|
* `<enum_var>=<value>`: `SET <enum_var>=<value>`
|
||||||
|
* `<string_var>=%27<value>%27`: `SET <string_var>='<value>'`
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
* The values for string variables must be quoted with `'`.
|
||||||
|
* The values must also be [url.QueryEscape](http://golang.org/pkg/net/url/#QueryEscape)'ed!
|
||||||
|
(which implies values of string variables must be wrapped with `%27`).
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
* `autocommit=1`: `SET autocommit=1`
|
||||||
|
* [`time_zone=%27Europe%2FParis%27`](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html): `SET time_zone='Europe/Paris'`
|
||||||
|
* [`transaction_isolation=%27REPEATABLE-READ%27`](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_transaction_isolation): `SET transaction_isolation='REPEATABLE-READ'`
|
||||||
|
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
```
|
||||||
|
user@unix(/path/to/socket)/dbname
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
root:pw@unix(/tmp/mysql.sock)/myDatabase?loc=Local
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
user:password@tcp(localhost:5555)/dbname?tls=skip-verify&autocommit=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Treat warnings as errors by setting the system variable [`sql_mode`](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html):
|
||||||
|
```
|
||||||
|
user:password@/dbname?sql_mode=TRADITIONAL
|
||||||
|
```
|
||||||
|
|
||||||
|
TCP via IPv6:
|
||||||
|
```
|
||||||
|
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname?timeout=90s&collation=utf8mb4_unicode_ci
|
||||||
|
```
|
||||||
|
|
||||||
|
TCP on a remote host, e.g. Amazon RDS:
|
||||||
|
```
|
||||||
|
id:password@tcp(your-amazonaws-uri.com:3306)/dbname
|
||||||
|
```
|
||||||
|
|
||||||
|
Google Cloud SQL on App Engine:
|
||||||
|
```
|
||||||
|
user:password@unix(/cloudsql/project-id:region-name:instance-name)/dbname
|
||||||
|
```
|
||||||
|
|
||||||
|
TCP using default port (3306) on localhost:
|
||||||
|
```
|
||||||
|
user:password@tcp/dbname?charset=utf8mb4,utf8&sys_var=esc%40ped
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the default protocol (tcp) and host (localhost:3306):
|
||||||
|
```
|
||||||
|
user:password@/dbname
|
||||||
|
```
|
||||||
|
|
||||||
|
No Database preselected:
|
||||||
|
```
|
||||||
|
user:password@/
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Connection pool and timeouts
|
||||||
|
The connection pool is managed by Go's database/sql package. For details on how to configure the size of the pool and how long connections stay in the pool see `*DB.SetMaxOpenConns`, `*DB.SetMaxIdleConns`, and `*DB.SetConnMaxLifetime` in the [database/sql documentation](https://golang.org/pkg/database/sql/). The read, write, and dial timeouts for each individual connection are configured with the DSN parameters [`readTimeout`](#readtimeout), [`writeTimeout`](#writetimeout), and [`timeout`](#timeout), respectively.
|
||||||
|
|
||||||
|
## `ColumnType` Support
|
||||||
|
This driver supports the [`ColumnType` interface](https://golang.org/pkg/database/sql/#ColumnType) introduced in Go 1.8, with the exception of [`ColumnType.Length()`](https://golang.org/pkg/database/sql/#ColumnType.Length), which is currently not supported. All Unsigned database type names will be returned `UNSIGNED ` with `INT`, `TINYINT`, `SMALLINT`, `MEDIUMINT`, `BIGINT`.
|
||||||
|
|
||||||
|
## `context.Context` Support
|
||||||
|
Go 1.8 added `database/sql` support for `context.Context`. This driver supports query timeouts and cancellation via contexts.
|
||||||
|
See [context support in the database/sql package](https://golang.org/doc/go1.8#database_sql) for more details.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> The `QueryContext`, `ExecContext`, etc. variants provided by `database/sql` will cause the connection to be closed if the provided context is cancelled or timed out before the result is received by the driver.
|
||||||
|
|
||||||
|
|
||||||
|
### `LOAD DATA LOCAL INFILE` support
|
||||||
|
For this feature you need direct access to the package. Therefore you must change the import path (no `_`):
|
||||||
|
```go
|
||||||
|
import "github.com/go-sql-driver/mysql"
|
||||||
|
```
|
||||||
|
|
||||||
|
Files must be explicitly allowed by registering them with `mysql.RegisterLocalFile(filepath)` (recommended) or the allowlist check must be deactivated by using the DSN parameter `allowAllFiles=true` ([*Might be insecure!*](https://dev.mysql.com/doc/refman/8.0/en/load-data.html#load-data-local)).
|
||||||
|
|
||||||
|
To use a `io.Reader` a handler function must be registered with `mysql.RegisterReaderHandler(name, handler)` which returns a `io.Reader` or `io.ReadCloser`. The Reader is available with the filepath `Reader::<name>` then. Choose different names for different handlers and `DeregisterReaderHandler` when you don't need it anymore.
|
||||||
|
|
||||||
|
See the [godoc of Go-MySQL-Driver](https://godoc.org/github.com/go-sql-driver/mysql "golang mysql driver documentation") for details.
|
||||||
|
|
||||||
|
|
||||||
|
### `time.Time` support
|
||||||
|
The default internal output type of MySQL `DATE` and `DATETIME` values is `[]byte` which allows you to scan the value into a `[]byte`, `string` or `sql.RawBytes` variable in your program.
|
||||||
|
|
||||||
|
However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` variables, which is the logical equivalent in Go to `DATE` and `DATETIME` in MySQL. You can do that by changing the internal output type from `[]byte` to `time.Time` with the DSN parameter `parseTime=true`. You can set the default [`time.Time` location](https://golang.org/pkg/time/#Location) with the `loc` DSN parameter.
|
||||||
|
|
||||||
|
**Caution:** As of Go 1.1, this makes `time.Time` the only variable type you can scan `DATE` and `DATETIME` values into. This breaks for example [`sql.RawBytes` support](https://github.com/go-sql-driver/mysql/wiki/Examples#rawbytes).
|
||||||
|
|
||||||
|
|
||||||
|
### Unicode support
|
||||||
|
Since version 1.5 Go-MySQL-Driver automatically uses the collation ` utf8mb4_general_ci` by default.
|
||||||
|
|
||||||
|
Other charsets / collations can be set using the [`charset`](#charset) or [`collation`](#collation) DSN parameter.
|
||||||
|
|
||||||
|
- When only the `charset` is specified, the `SET NAMES <charset>` query is sent and the server's default collation is used.
|
||||||
|
- When both the `charset` and `collation` are specified, the `SET NAMES <charset> COLLATE <collation>` query is sent.
|
||||||
|
- When only the `collation` is specified, the collation is specified in the protocol handshake and the `SET NAMES` query is not sent. This can save one roundtrip, but note that the server may ignore the specified collation silently and use the server's default charset/collation instead.
|
||||||
|
|
||||||
|
See http://dev.mysql.com/doc/refman/8.0/en/charset-unicode.html for more details on MySQL's Unicode support.
|
||||||
|
|
||||||
|
## Testing / Development
|
||||||
|
To run the driver tests you may need to adjust the configuration. See the [Testing Wiki-Page](https://github.com/go-sql-driver/mysql/wiki/Testing "Testing") for details.
|
||||||
|
|
||||||
|
Go-MySQL-Driver is not feature-complete yet. Your help is very appreciated.
|
||||||
|
If you want to contribute, you can work on an [open issue](https://github.com/go-sql-driver/mysql/issues?state=open) or review a [pull request](https://github.com/go-sql-driver/mysql/pulls).
|
||||||
|
|
||||||
|
See the [Contribution Guidelines](https://github.com/go-sql-driver/mysql/blob/master/.github/CONTRIBUTING.md) for details.
|
||||||
|
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
## License
|
||||||
|
Go-MySQL-Driver is licensed under the [Mozilla Public License Version 2.0](https://raw.github.com/go-sql-driver/mysql/master/LICENSE)
|
||||||
|
|
||||||
|
Mozilla summarizes the license scope as follows:
|
||||||
|
> MPL: The copyleft applies to any files containing MPLed code.
|
||||||
|
|
||||||
|
|
||||||
|
That means:
|
||||||
|
* You can **use** the **unchanged** source code both in private and commercially.
|
||||||
|
* When distributing, you **must publish** the source code of any **changed files** licensed under the MPL 2.0 under a) the MPL 2.0 itself or b) a compatible license (e.g. GPL 3.0 or Apache License 2.0).
|
||||||
|
* You **needn't publish** the source code of your library as long as the files licensed under the MPL 2.0 are **unchanged**.
|
||||||
|
|
||||||
|
Please read the [MPL 2.0 FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/) if you have further questions regarding the license.
|
||||||
|
|
||||||
|
You can read the full terms here: [LICENSE](https://raw.github.com/go-sql-driver/mysql/master/LICENSE).
|
||||||
|
|
||||||
|

|
||||||
484
vendor/github.com/go-sql-driver/mysql/auth.go
generated
vendored
Normal file
484
vendor/github.com/go-sql-driver/mysql/auth.go
generated
vendored
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2018 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"filippo.io/edwards25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
// server pub keys registry
|
||||||
|
var (
|
||||||
|
serverPubKeyLock sync.RWMutex
|
||||||
|
serverPubKeyRegistry map[string]*rsa.PublicKey
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterServerPubKey registers a server RSA public key which can be used to
|
||||||
|
// send data in a secure manner to the server without receiving the public key
|
||||||
|
// in a potentially insecure way from the server first.
|
||||||
|
// Registered keys can afterwards be used adding serverPubKey=<name> to the DSN.
|
||||||
|
//
|
||||||
|
// Note: The provided rsa.PublicKey instance is exclusively owned by the driver
|
||||||
|
// after registering it and may not be modified.
|
||||||
|
//
|
||||||
|
// data, err := os.ReadFile("mykey.pem")
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// block, _ := pem.Decode(data)
|
||||||
|
// if block == nil || block.Type != "PUBLIC KEY" {
|
||||||
|
// log.Fatal("failed to decode PEM block containing public key")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if rsaPubKey, ok := pub.(*rsa.PublicKey); ok {
|
||||||
|
// mysql.RegisterServerPubKey("mykey", rsaPubKey)
|
||||||
|
// } else {
|
||||||
|
// log.Fatal("not a RSA public key")
|
||||||
|
// }
|
||||||
|
func RegisterServerPubKey(name string, pubKey *rsa.PublicKey) {
|
||||||
|
serverPubKeyLock.Lock()
|
||||||
|
if serverPubKeyRegistry == nil {
|
||||||
|
serverPubKeyRegistry = make(map[string]*rsa.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverPubKeyRegistry[name] = pubKey
|
||||||
|
serverPubKeyLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeregisterServerPubKey removes the public key registered with the given name.
|
||||||
|
func DeregisterServerPubKey(name string) {
|
||||||
|
serverPubKeyLock.Lock()
|
||||||
|
if serverPubKeyRegistry != nil {
|
||||||
|
delete(serverPubKeyRegistry, name)
|
||||||
|
}
|
||||||
|
serverPubKeyLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerPubKey(name string) (pubKey *rsa.PublicKey) {
|
||||||
|
serverPubKeyLock.RLock()
|
||||||
|
if v, ok := serverPubKeyRegistry[name]; ok {
|
||||||
|
pubKey = v
|
||||||
|
}
|
||||||
|
serverPubKeyLock.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password using pre 4.1 (old password) method
|
||||||
|
// https://github.com/atcurtis/mariadb/blob/master/mysys/my_rnd.c
|
||||||
|
type myRnd struct {
|
||||||
|
seed1, seed2 uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
const myRndMaxVal = 0x3FFFFFFF
|
||||||
|
|
||||||
|
// Pseudo random number generator
|
||||||
|
func newMyRnd(seed1, seed2 uint32) *myRnd {
|
||||||
|
return &myRnd{
|
||||||
|
seed1: seed1 % myRndMaxVal,
|
||||||
|
seed2: seed2 % myRndMaxVal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tested to be equivalent to MariaDB's floating point variant
|
||||||
|
// http://play.golang.org/p/QHvhd4qved
|
||||||
|
// http://play.golang.org/p/RG0q4ElWDx
|
||||||
|
func (r *myRnd) NextByte() byte {
|
||||||
|
r.seed1 = (r.seed1*3 + r.seed2) % myRndMaxVal
|
||||||
|
r.seed2 = (r.seed1 + r.seed2 + 33) % myRndMaxVal
|
||||||
|
|
||||||
|
return byte(uint64(r.seed1) * 31 / myRndMaxVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate binary hash from byte string using insecure pre 4.1 method
|
||||||
|
func pwHash(password []byte) (result [2]uint32) {
|
||||||
|
var add uint32 = 7
|
||||||
|
var tmp uint32
|
||||||
|
|
||||||
|
result[0] = 1345345333
|
||||||
|
result[1] = 0x12345671
|
||||||
|
|
||||||
|
for _, c := range password {
|
||||||
|
// skip spaces and tabs in password
|
||||||
|
if c == ' ' || c == '\t' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp = uint32(c)
|
||||||
|
result[0] ^= (((result[0] & 63) + add) * tmp) + (result[0] << 8)
|
||||||
|
result[1] += (result[1] << 8) ^ result[0]
|
||||||
|
add += tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove sign bit (1<<31)-1)
|
||||||
|
result[0] &= 0x7FFFFFFF
|
||||||
|
result[1] &= 0x7FFFFFFF
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password using insecure pre 4.1 method
|
||||||
|
func scrambleOldPassword(scramble []byte, password string) []byte {
|
||||||
|
scramble = scramble[:8]
|
||||||
|
|
||||||
|
hashPw := pwHash([]byte(password))
|
||||||
|
hashSc := pwHash(scramble)
|
||||||
|
|
||||||
|
r := newMyRnd(hashPw[0]^hashSc[0], hashPw[1]^hashSc[1])
|
||||||
|
|
||||||
|
var out [8]byte
|
||||||
|
for i := range out {
|
||||||
|
out[i] = r.NextByte() + 64
|
||||||
|
}
|
||||||
|
|
||||||
|
mask := r.NextByte()
|
||||||
|
for i := range out {
|
||||||
|
out[i] ^= mask
|
||||||
|
}
|
||||||
|
|
||||||
|
return out[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password using 4.1+ method (SHA1)
|
||||||
|
func scramblePassword(scramble []byte, password string) []byte {
|
||||||
|
if len(password) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage1Hash = SHA1(password)
|
||||||
|
crypt := sha1.New()
|
||||||
|
crypt.Write([]byte(password))
|
||||||
|
stage1 := crypt.Sum(nil)
|
||||||
|
|
||||||
|
// scrambleHash = SHA1(scramble + SHA1(stage1Hash))
|
||||||
|
// inner Hash
|
||||||
|
crypt.Reset()
|
||||||
|
crypt.Write(stage1)
|
||||||
|
hash := crypt.Sum(nil)
|
||||||
|
|
||||||
|
// outer Hash
|
||||||
|
crypt.Reset()
|
||||||
|
crypt.Write(scramble)
|
||||||
|
crypt.Write(hash)
|
||||||
|
scramble = crypt.Sum(nil)
|
||||||
|
|
||||||
|
// token = scrambleHash XOR stage1Hash
|
||||||
|
for i := range scramble {
|
||||||
|
scramble[i] ^= stage1[i]
|
||||||
|
}
|
||||||
|
return scramble
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password using MySQL 8+ method (SHA256)
|
||||||
|
func scrambleSHA256Password(scramble []byte, password string) []byte {
|
||||||
|
if len(password) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble))
|
||||||
|
|
||||||
|
crypt := sha256.New()
|
||||||
|
crypt.Write([]byte(password))
|
||||||
|
message1 := crypt.Sum(nil)
|
||||||
|
|
||||||
|
crypt.Reset()
|
||||||
|
crypt.Write(message1)
|
||||||
|
message1Hash := crypt.Sum(nil)
|
||||||
|
|
||||||
|
crypt.Reset()
|
||||||
|
crypt.Write(message1Hash)
|
||||||
|
crypt.Write(scramble)
|
||||||
|
message2 := crypt.Sum(nil)
|
||||||
|
|
||||||
|
for i := range message1 {
|
||||||
|
message1[i] ^= message2[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return message1
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte, error) {
|
||||||
|
plain := make([]byte, len(password)+1)
|
||||||
|
copy(plain, password)
|
||||||
|
for i := range plain {
|
||||||
|
j := i % len(seed)
|
||||||
|
plain[i] ^= seed[j]
|
||||||
|
}
|
||||||
|
sha1 := sha1.New()
|
||||||
|
return rsa.EncryptOAEP(sha1, rand.Reader, pub, plain, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// authEd25519 does ed25519 authentication used by MariaDB.
|
||||||
|
func authEd25519(scramble []byte, password string) ([]byte, error) {
|
||||||
|
// Derived from https://github.com/MariaDB/server/blob/d8e6bb00888b1f82c031938f4c8ac5d97f6874c3/plugin/auth_ed25519/ref10/sign.c
|
||||||
|
// Code style is from https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/crypto/ed25519/ed25519.go;l=207
|
||||||
|
h := sha512.Sum512([]byte(password))
|
||||||
|
|
||||||
|
s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
A := (&edwards25519.Point{}).ScalarBaseMult(s)
|
||||||
|
|
||||||
|
mh := sha512.New()
|
||||||
|
mh.Write(h[32:])
|
||||||
|
mh.Write(scramble)
|
||||||
|
messageDigest := mh.Sum(nil)
|
||||||
|
r, err := edwards25519.NewScalar().SetUniformBytes(messageDigest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
R := (&edwards25519.Point{}).ScalarBaseMult(r)
|
||||||
|
|
||||||
|
kh := sha512.New()
|
||||||
|
kh.Write(R.Bytes())
|
||||||
|
kh.Write(A.Bytes())
|
||||||
|
kh.Write(scramble)
|
||||||
|
hramDigest := kh.Sum(nil)
|
||||||
|
k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
S := k.MultiplyAdd(k, s, r)
|
||||||
|
|
||||||
|
return append(R.Bytes(), S.Bytes()...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) sendEncryptedPassword(seed []byte, pub *rsa.PublicKey) error {
|
||||||
|
enc, err := encryptPassword(mc.cfg.Passwd, seed, pub)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return mc.writeAuthSwitchPacket(enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) {
|
||||||
|
switch plugin {
|
||||||
|
case "caching_sha2_password":
|
||||||
|
authResp := scrambleSHA256Password(authData, mc.cfg.Passwd)
|
||||||
|
return authResp, nil
|
||||||
|
|
||||||
|
case "mysql_old_password":
|
||||||
|
if !mc.cfg.AllowOldPasswords {
|
||||||
|
return nil, ErrOldPassword
|
||||||
|
}
|
||||||
|
if len(mc.cfg.Passwd) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
// Note: there are edge cases where this should work but doesn't;
|
||||||
|
// this is currently "wontfix":
|
||||||
|
// https://github.com/go-sql-driver/mysql/issues/184
|
||||||
|
authResp := append(scrambleOldPassword(authData[:8], mc.cfg.Passwd), 0)
|
||||||
|
return authResp, nil
|
||||||
|
|
||||||
|
case "mysql_clear_password":
|
||||||
|
if !mc.cfg.AllowCleartextPasswords {
|
||||||
|
return nil, ErrCleartextPassword
|
||||||
|
}
|
||||||
|
// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
|
||||||
|
// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
|
||||||
|
return append([]byte(mc.cfg.Passwd), 0), nil
|
||||||
|
|
||||||
|
case "mysql_native_password":
|
||||||
|
if !mc.cfg.AllowNativePasswords {
|
||||||
|
return nil, ErrNativePassword
|
||||||
|
}
|
||||||
|
// https://dev.mysql.com/doc/dev/mysql-server/8.4.5/page_protocol_connection_phase_authentication_methods_native_password_authentication.html
|
||||||
|
// Native password authentication only need and will need 20-byte challenge.
|
||||||
|
authResp := scramblePassword(authData[:20], mc.cfg.Passwd)
|
||||||
|
return authResp, nil
|
||||||
|
|
||||||
|
case "sha256_password":
|
||||||
|
if len(mc.cfg.Passwd) == 0 {
|
||||||
|
return []byte{0}, nil
|
||||||
|
}
|
||||||
|
// unlike caching_sha2_password, sha256_password does not accept
|
||||||
|
// cleartext password on unix transport.
|
||||||
|
if mc.cfg.TLS != nil {
|
||||||
|
// write cleartext auth packet
|
||||||
|
return append([]byte(mc.cfg.Passwd), 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey := mc.cfg.pubKey
|
||||||
|
if pubKey == nil {
|
||||||
|
// request public key from server
|
||||||
|
return []byte{1}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypted password
|
||||||
|
enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey)
|
||||||
|
return enc, err
|
||||||
|
|
||||||
|
case "client_ed25519":
|
||||||
|
if len(authData) != 32 {
|
||||||
|
return nil, ErrMalformPkt
|
||||||
|
}
|
||||||
|
return authEd25519(authData, mc.cfg.Passwd)
|
||||||
|
|
||||||
|
default:
|
||||||
|
mc.log("unknown auth plugin:", plugin)
|
||||||
|
return nil, ErrUnknownPlugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
|
||||||
|
// Read Result Packet
|
||||||
|
authData, newPlugin, err := mc.readAuthResult()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle auth plugin switch, if requested
|
||||||
|
if newPlugin != "" {
|
||||||
|
// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is
|
||||||
|
// sent and we have to keep using the cipher sent in the init packet.
|
||||||
|
if authData == nil {
|
||||||
|
authData = oldAuthData
|
||||||
|
} else {
|
||||||
|
// copy data from read buffer to owned slice
|
||||||
|
copy(oldAuthData, authData)
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin = newPlugin
|
||||||
|
|
||||||
|
authResp, err := mc.auth(authData, plugin)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = mc.writeAuthSwitchPacket(authResp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read Result Packet
|
||||||
|
authData, newPlugin, err = mc.readAuthResult()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not allow to change the auth plugin more than once
|
||||||
|
if newPlugin != "" {
|
||||||
|
return ErrMalformPkt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch plugin {
|
||||||
|
|
||||||
|
// https://dev.mysql.com/blog-archive/preparing-your-community-connector-for-mysql-8-part-2-sha256/
|
||||||
|
case "caching_sha2_password":
|
||||||
|
switch len(authData) {
|
||||||
|
case 0:
|
||||||
|
return nil // auth successful
|
||||||
|
case 1:
|
||||||
|
switch authData[0] {
|
||||||
|
case cachingSha2PasswordFastAuthSuccess:
|
||||||
|
if err = mc.resultUnchanged().readResultOK(); err == nil {
|
||||||
|
return nil // auth successful
|
||||||
|
}
|
||||||
|
|
||||||
|
case cachingSha2PasswordPerformFullAuthentication:
|
||||||
|
if mc.cfg.TLS != nil || mc.cfg.Net == "unix" {
|
||||||
|
// write cleartext auth packet
|
||||||
|
err = mc.writeAuthSwitchPacket(append([]byte(mc.cfg.Passwd), 0))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pubKey := mc.cfg.pubKey
|
||||||
|
if pubKey == nil {
|
||||||
|
// request public key from server
|
||||||
|
data, err := mc.buf.takeSmallBuffer(4 + 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data[4] = cachingSha2PasswordRequestPublicKey
|
||||||
|
err = mc.writePacket(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if data, err = mc.readPacket(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if data[0] != iAuthMoreData {
|
||||||
|
return fmt.Errorf("unexpected resp from server for caching_sha2_password, perform full authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse public key
|
||||||
|
block, rest := pem.Decode(data[1:])
|
||||||
|
if block == nil {
|
||||||
|
return fmt.Errorf("no pem data found, data: %s", rest)
|
||||||
|
}
|
||||||
|
pkix, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pubKey = pkix.(*rsa.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// send encrypted password
|
||||||
|
err = mc.sendEncryptedPassword(oldAuthData, pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mc.resultUnchanged().readResultOK()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ErrMalformPkt
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return ErrMalformPkt
|
||||||
|
}
|
||||||
|
|
||||||
|
case "sha256_password":
|
||||||
|
switch len(authData) {
|
||||||
|
case 0:
|
||||||
|
return nil // auth successful
|
||||||
|
default:
|
||||||
|
block, _ := pem.Decode(authData)
|
||||||
|
if block == nil {
|
||||||
|
return fmt.Errorf("no Pem data found, data: %s", authData)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// send encrypted password
|
||||||
|
err = mc.sendEncryptedPassword(oldAuthData, pub.(*rsa.PublicKey))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return mc.resultUnchanged().readResultOK()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil // auth successful
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
149
vendor/github.com/go-sql-driver/mysql/buffer.go
generated
vendored
Normal file
149
vendor/github.com/go-sql-driver/mysql/buffer.go
generated
vendored
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultBufSize = 4096
|
||||||
|
const maxCachedBufSize = 256 * 1024
|
||||||
|
|
||||||
|
// readerFunc is a function that compatible with io.Reader.
|
||||||
|
// We use this function type instead of io.Reader because we want to
|
||||||
|
// just pass mc.readWithTimeout.
|
||||||
|
type readerFunc func([]byte) (int, error)
|
||||||
|
|
||||||
|
// A buffer which is used for both reading and writing.
|
||||||
|
// This is possible since communication on each connection is synchronous.
|
||||||
|
// In other words, we can't write and read simultaneously on the same connection.
|
||||||
|
// The buffer is similar to bufio.Reader / Writer but zero-copy-ish
|
||||||
|
// Also highly optimized for this particular use case.
|
||||||
|
type buffer struct {
|
||||||
|
buf []byte // read buffer.
|
||||||
|
cachedBuf []byte // buffer that will be reused. len(cachedBuf) <= maxCachedBufSize.
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBuffer allocates and returns a new buffer.
|
||||||
|
func newBuffer() buffer {
|
||||||
|
return buffer{
|
||||||
|
cachedBuf: make([]byte, defaultBufSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// busy returns true if the read buffer is not empty.
|
||||||
|
func (b *buffer) busy() bool {
|
||||||
|
return len(b.buf) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// len returns how many bytes in the read buffer.
|
||||||
|
func (b *buffer) len() int {
|
||||||
|
return len(b.buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill reads into the read buffer until at least _need_ bytes are in it.
|
||||||
|
func (b *buffer) fill(need int, r readerFunc) error {
|
||||||
|
// we'll move the contents of the current buffer to dest before filling it.
|
||||||
|
dest := b.cachedBuf
|
||||||
|
|
||||||
|
// grow buffer if necessary to fit the whole packet.
|
||||||
|
if need > len(dest) {
|
||||||
|
// Round up to the next multiple of the default size
|
||||||
|
dest = make([]byte, ((need/defaultBufSize)+1)*defaultBufSize)
|
||||||
|
|
||||||
|
// if the allocated buffer is not too large, move it to backing storage
|
||||||
|
// to prevent extra allocations on applications that perform large reads
|
||||||
|
if len(dest) <= maxCachedBufSize {
|
||||||
|
b.cachedBuf = dest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// move the existing data to the start of the buffer.
|
||||||
|
n := len(b.buf)
|
||||||
|
copy(dest[:n], b.buf)
|
||||||
|
|
||||||
|
for {
|
||||||
|
nn, err := r(dest[n:])
|
||||||
|
n += nn
|
||||||
|
|
||||||
|
if err == nil && n < need {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
b.buf = dest[:n]
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
if n < need {
|
||||||
|
err = io.ErrUnexpectedEOF
|
||||||
|
} else {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns next N bytes from buffer.
|
||||||
|
// The returned slice is only guaranteed to be valid until the next read
|
||||||
|
func (b *buffer) readNext(need int) []byte {
|
||||||
|
data := b.buf[:need:need]
|
||||||
|
b.buf = b.buf[need:]
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// takeBuffer returns a buffer with the requested size.
|
||||||
|
// If possible, a slice from the existing buffer is returned.
|
||||||
|
// Otherwise a bigger buffer is made.
|
||||||
|
// Only one buffer (total) can be used at a time.
|
||||||
|
func (b *buffer) takeBuffer(length int) ([]byte, error) {
|
||||||
|
if b.busy() {
|
||||||
|
return nil, ErrBusyBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// test (cheap) general case first
|
||||||
|
if length <= len(b.cachedBuf) {
|
||||||
|
return b.cachedBuf[:length], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if length < maxCachedBufSize {
|
||||||
|
b.cachedBuf = make([]byte, length)
|
||||||
|
return b.cachedBuf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buffer is larger than we want to store.
|
||||||
|
return make([]byte, length), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// takeSmallBuffer is shortcut which can be used if length is
|
||||||
|
// known to be smaller than defaultBufSize.
|
||||||
|
// Only one buffer (total) can be used at a time.
|
||||||
|
func (b *buffer) takeSmallBuffer(length int) ([]byte, error) {
|
||||||
|
if b.busy() {
|
||||||
|
return nil, ErrBusyBuffer
|
||||||
|
}
|
||||||
|
return b.cachedBuf[:length], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// takeCompleteBuffer returns the complete existing buffer.
|
||||||
|
// This can be used if the necessary buffer size is unknown.
|
||||||
|
// cap and len of the returned buffer will be equal.
|
||||||
|
// Only one buffer (total) can be used at a time.
|
||||||
|
func (b *buffer) takeCompleteBuffer() ([]byte, error) {
|
||||||
|
if b.busy() {
|
||||||
|
return nil, ErrBusyBuffer
|
||||||
|
}
|
||||||
|
return b.cachedBuf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// store stores buf, an updated buffer, if its suitable to do so.
|
||||||
|
func (b *buffer) store(buf []byte) {
|
||||||
|
if cap(buf) <= maxCachedBufSize && cap(buf) > cap(b.cachedBuf) {
|
||||||
|
b.cachedBuf = buf[:cap(buf)]
|
||||||
|
}
|
||||||
|
}
|
||||||
266
vendor/github.com/go-sql-driver/mysql/collations.go
generated
vendored
Normal file
266
vendor/github.com/go-sql-driver/mysql/collations.go
generated
vendored
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2014 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
const defaultCollationID = 45 // utf8mb4_general_ci
|
||||||
|
const binaryCollationID = 63
|
||||||
|
|
||||||
|
// A list of available collations mapped to the internal ID.
|
||||||
|
// To update this map use the following MySQL query:
|
||||||
|
//
|
||||||
|
// SELECT COLLATION_NAME, ID FROM information_schema.COLLATIONS WHERE ID<256 ORDER BY ID
|
||||||
|
//
|
||||||
|
// Handshake packet have only 1 byte for collation_id. So we can't use collations with ID > 255.
|
||||||
|
//
|
||||||
|
// ucs2, utf16, and utf32 can't be used for connection charset.
|
||||||
|
// https://dev.mysql.com/doc/refman/5.7/en/charset-connection.html#charset-connection-impermissible-client-charset
|
||||||
|
// They are commented out to reduce this map.
|
||||||
|
var collations = map[string]byte{
|
||||||
|
"big5_chinese_ci": 1,
|
||||||
|
"latin2_czech_cs": 2,
|
||||||
|
"dec8_swedish_ci": 3,
|
||||||
|
"cp850_general_ci": 4,
|
||||||
|
"latin1_german1_ci": 5,
|
||||||
|
"hp8_english_ci": 6,
|
||||||
|
"koi8r_general_ci": 7,
|
||||||
|
"latin1_swedish_ci": 8,
|
||||||
|
"latin2_general_ci": 9,
|
||||||
|
"swe7_swedish_ci": 10,
|
||||||
|
"ascii_general_ci": 11,
|
||||||
|
"ujis_japanese_ci": 12,
|
||||||
|
"sjis_japanese_ci": 13,
|
||||||
|
"cp1251_bulgarian_ci": 14,
|
||||||
|
"latin1_danish_ci": 15,
|
||||||
|
"hebrew_general_ci": 16,
|
||||||
|
"tis620_thai_ci": 18,
|
||||||
|
"euckr_korean_ci": 19,
|
||||||
|
"latin7_estonian_cs": 20,
|
||||||
|
"latin2_hungarian_ci": 21,
|
||||||
|
"koi8u_general_ci": 22,
|
||||||
|
"cp1251_ukrainian_ci": 23,
|
||||||
|
"gb2312_chinese_ci": 24,
|
||||||
|
"greek_general_ci": 25,
|
||||||
|
"cp1250_general_ci": 26,
|
||||||
|
"latin2_croatian_ci": 27,
|
||||||
|
"gbk_chinese_ci": 28,
|
||||||
|
"cp1257_lithuanian_ci": 29,
|
||||||
|
"latin5_turkish_ci": 30,
|
||||||
|
"latin1_german2_ci": 31,
|
||||||
|
"armscii8_general_ci": 32,
|
||||||
|
"utf8_general_ci": 33,
|
||||||
|
"cp1250_czech_cs": 34,
|
||||||
|
//"ucs2_general_ci": 35,
|
||||||
|
"cp866_general_ci": 36,
|
||||||
|
"keybcs2_general_ci": 37,
|
||||||
|
"macce_general_ci": 38,
|
||||||
|
"macroman_general_ci": 39,
|
||||||
|
"cp852_general_ci": 40,
|
||||||
|
"latin7_general_ci": 41,
|
||||||
|
"latin7_general_cs": 42,
|
||||||
|
"macce_bin": 43,
|
||||||
|
"cp1250_croatian_ci": 44,
|
||||||
|
"utf8mb4_general_ci": 45,
|
||||||
|
"utf8mb4_bin": 46,
|
||||||
|
"latin1_bin": 47,
|
||||||
|
"latin1_general_ci": 48,
|
||||||
|
"latin1_general_cs": 49,
|
||||||
|
"cp1251_bin": 50,
|
||||||
|
"cp1251_general_ci": 51,
|
||||||
|
"cp1251_general_cs": 52,
|
||||||
|
"macroman_bin": 53,
|
||||||
|
//"utf16_general_ci": 54,
|
||||||
|
//"utf16_bin": 55,
|
||||||
|
//"utf16le_general_ci": 56,
|
||||||
|
"cp1256_general_ci": 57,
|
||||||
|
"cp1257_bin": 58,
|
||||||
|
"cp1257_general_ci": 59,
|
||||||
|
//"utf32_general_ci": 60,
|
||||||
|
//"utf32_bin": 61,
|
||||||
|
//"utf16le_bin": 62,
|
||||||
|
"binary": 63,
|
||||||
|
"armscii8_bin": 64,
|
||||||
|
"ascii_bin": 65,
|
||||||
|
"cp1250_bin": 66,
|
||||||
|
"cp1256_bin": 67,
|
||||||
|
"cp866_bin": 68,
|
||||||
|
"dec8_bin": 69,
|
||||||
|
"greek_bin": 70,
|
||||||
|
"hebrew_bin": 71,
|
||||||
|
"hp8_bin": 72,
|
||||||
|
"keybcs2_bin": 73,
|
||||||
|
"koi8r_bin": 74,
|
||||||
|
"koi8u_bin": 75,
|
||||||
|
"utf8_tolower_ci": 76,
|
||||||
|
"latin2_bin": 77,
|
||||||
|
"latin5_bin": 78,
|
||||||
|
"latin7_bin": 79,
|
||||||
|
"cp850_bin": 80,
|
||||||
|
"cp852_bin": 81,
|
||||||
|
"swe7_bin": 82,
|
||||||
|
"utf8_bin": 83,
|
||||||
|
"big5_bin": 84,
|
||||||
|
"euckr_bin": 85,
|
||||||
|
"gb2312_bin": 86,
|
||||||
|
"gbk_bin": 87,
|
||||||
|
"sjis_bin": 88,
|
||||||
|
"tis620_bin": 89,
|
||||||
|
//"ucs2_bin": 90,
|
||||||
|
"ujis_bin": 91,
|
||||||
|
"geostd8_general_ci": 92,
|
||||||
|
"geostd8_bin": 93,
|
||||||
|
"latin1_spanish_ci": 94,
|
||||||
|
"cp932_japanese_ci": 95,
|
||||||
|
"cp932_bin": 96,
|
||||||
|
"eucjpms_japanese_ci": 97,
|
||||||
|
"eucjpms_bin": 98,
|
||||||
|
"cp1250_polish_ci": 99,
|
||||||
|
//"utf16_unicode_ci": 101,
|
||||||
|
//"utf16_icelandic_ci": 102,
|
||||||
|
//"utf16_latvian_ci": 103,
|
||||||
|
//"utf16_romanian_ci": 104,
|
||||||
|
//"utf16_slovenian_ci": 105,
|
||||||
|
//"utf16_polish_ci": 106,
|
||||||
|
//"utf16_estonian_ci": 107,
|
||||||
|
//"utf16_spanish_ci": 108,
|
||||||
|
//"utf16_swedish_ci": 109,
|
||||||
|
//"utf16_turkish_ci": 110,
|
||||||
|
//"utf16_czech_ci": 111,
|
||||||
|
//"utf16_danish_ci": 112,
|
||||||
|
//"utf16_lithuanian_ci": 113,
|
||||||
|
//"utf16_slovak_ci": 114,
|
||||||
|
//"utf16_spanish2_ci": 115,
|
||||||
|
//"utf16_roman_ci": 116,
|
||||||
|
//"utf16_persian_ci": 117,
|
||||||
|
//"utf16_esperanto_ci": 118,
|
||||||
|
//"utf16_hungarian_ci": 119,
|
||||||
|
//"utf16_sinhala_ci": 120,
|
||||||
|
//"utf16_german2_ci": 121,
|
||||||
|
//"utf16_croatian_ci": 122,
|
||||||
|
//"utf16_unicode_520_ci": 123,
|
||||||
|
//"utf16_vietnamese_ci": 124,
|
||||||
|
//"ucs2_unicode_ci": 128,
|
||||||
|
//"ucs2_icelandic_ci": 129,
|
||||||
|
//"ucs2_latvian_ci": 130,
|
||||||
|
//"ucs2_romanian_ci": 131,
|
||||||
|
//"ucs2_slovenian_ci": 132,
|
||||||
|
//"ucs2_polish_ci": 133,
|
||||||
|
//"ucs2_estonian_ci": 134,
|
||||||
|
//"ucs2_spanish_ci": 135,
|
||||||
|
//"ucs2_swedish_ci": 136,
|
||||||
|
//"ucs2_turkish_ci": 137,
|
||||||
|
//"ucs2_czech_ci": 138,
|
||||||
|
//"ucs2_danish_ci": 139,
|
||||||
|
//"ucs2_lithuanian_ci": 140,
|
||||||
|
//"ucs2_slovak_ci": 141,
|
||||||
|
//"ucs2_spanish2_ci": 142,
|
||||||
|
//"ucs2_roman_ci": 143,
|
||||||
|
//"ucs2_persian_ci": 144,
|
||||||
|
//"ucs2_esperanto_ci": 145,
|
||||||
|
//"ucs2_hungarian_ci": 146,
|
||||||
|
//"ucs2_sinhala_ci": 147,
|
||||||
|
//"ucs2_german2_ci": 148,
|
||||||
|
//"ucs2_croatian_ci": 149,
|
||||||
|
//"ucs2_unicode_520_ci": 150,
|
||||||
|
//"ucs2_vietnamese_ci": 151,
|
||||||
|
//"ucs2_general_mysql500_ci": 159,
|
||||||
|
//"utf32_unicode_ci": 160,
|
||||||
|
//"utf32_icelandic_ci": 161,
|
||||||
|
//"utf32_latvian_ci": 162,
|
||||||
|
//"utf32_romanian_ci": 163,
|
||||||
|
//"utf32_slovenian_ci": 164,
|
||||||
|
//"utf32_polish_ci": 165,
|
||||||
|
//"utf32_estonian_ci": 166,
|
||||||
|
//"utf32_spanish_ci": 167,
|
||||||
|
//"utf32_swedish_ci": 168,
|
||||||
|
//"utf32_turkish_ci": 169,
|
||||||
|
//"utf32_czech_ci": 170,
|
||||||
|
//"utf32_danish_ci": 171,
|
||||||
|
//"utf32_lithuanian_ci": 172,
|
||||||
|
//"utf32_slovak_ci": 173,
|
||||||
|
//"utf32_spanish2_ci": 174,
|
||||||
|
//"utf32_roman_ci": 175,
|
||||||
|
//"utf32_persian_ci": 176,
|
||||||
|
//"utf32_esperanto_ci": 177,
|
||||||
|
//"utf32_hungarian_ci": 178,
|
||||||
|
//"utf32_sinhala_ci": 179,
|
||||||
|
//"utf32_german2_ci": 180,
|
||||||
|
//"utf32_croatian_ci": 181,
|
||||||
|
//"utf32_unicode_520_ci": 182,
|
||||||
|
//"utf32_vietnamese_ci": 183,
|
||||||
|
"utf8_unicode_ci": 192,
|
||||||
|
"utf8_icelandic_ci": 193,
|
||||||
|
"utf8_latvian_ci": 194,
|
||||||
|
"utf8_romanian_ci": 195,
|
||||||
|
"utf8_slovenian_ci": 196,
|
||||||
|
"utf8_polish_ci": 197,
|
||||||
|
"utf8_estonian_ci": 198,
|
||||||
|
"utf8_spanish_ci": 199,
|
||||||
|
"utf8_swedish_ci": 200,
|
||||||
|
"utf8_turkish_ci": 201,
|
||||||
|
"utf8_czech_ci": 202,
|
||||||
|
"utf8_danish_ci": 203,
|
||||||
|
"utf8_lithuanian_ci": 204,
|
||||||
|
"utf8_slovak_ci": 205,
|
||||||
|
"utf8_spanish2_ci": 206,
|
||||||
|
"utf8_roman_ci": 207,
|
||||||
|
"utf8_persian_ci": 208,
|
||||||
|
"utf8_esperanto_ci": 209,
|
||||||
|
"utf8_hungarian_ci": 210,
|
||||||
|
"utf8_sinhala_ci": 211,
|
||||||
|
"utf8_german2_ci": 212,
|
||||||
|
"utf8_croatian_ci": 213,
|
||||||
|
"utf8_unicode_520_ci": 214,
|
||||||
|
"utf8_vietnamese_ci": 215,
|
||||||
|
"utf8_general_mysql500_ci": 223,
|
||||||
|
"utf8mb4_unicode_ci": 224,
|
||||||
|
"utf8mb4_icelandic_ci": 225,
|
||||||
|
"utf8mb4_latvian_ci": 226,
|
||||||
|
"utf8mb4_romanian_ci": 227,
|
||||||
|
"utf8mb4_slovenian_ci": 228,
|
||||||
|
"utf8mb4_polish_ci": 229,
|
||||||
|
"utf8mb4_estonian_ci": 230,
|
||||||
|
"utf8mb4_spanish_ci": 231,
|
||||||
|
"utf8mb4_swedish_ci": 232,
|
||||||
|
"utf8mb4_turkish_ci": 233,
|
||||||
|
"utf8mb4_czech_ci": 234,
|
||||||
|
"utf8mb4_danish_ci": 235,
|
||||||
|
"utf8mb4_lithuanian_ci": 236,
|
||||||
|
"utf8mb4_slovak_ci": 237,
|
||||||
|
"utf8mb4_spanish2_ci": 238,
|
||||||
|
"utf8mb4_roman_ci": 239,
|
||||||
|
"utf8mb4_persian_ci": 240,
|
||||||
|
"utf8mb4_esperanto_ci": 241,
|
||||||
|
"utf8mb4_hungarian_ci": 242,
|
||||||
|
"utf8mb4_sinhala_ci": 243,
|
||||||
|
"utf8mb4_german2_ci": 244,
|
||||||
|
"utf8mb4_croatian_ci": 245,
|
||||||
|
"utf8mb4_unicode_520_ci": 246,
|
||||||
|
"utf8mb4_vietnamese_ci": 247,
|
||||||
|
"gb18030_chinese_ci": 248,
|
||||||
|
"gb18030_bin": 249,
|
||||||
|
"gb18030_unicode_520_ci": 250,
|
||||||
|
"utf8mb4_0900_ai_ci": 255,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A denylist of collations which is unsafe to interpolate parameters.
|
||||||
|
// These multibyte encodings may contains 0x5c (`\`) in their trailing bytes.
|
||||||
|
var unsafeCollations = map[string]bool{
|
||||||
|
"big5_chinese_ci": true,
|
||||||
|
"sjis_japanese_ci": true,
|
||||||
|
"gbk_chinese_ci": true,
|
||||||
|
"big5_bin": true,
|
||||||
|
"gb2312_bin": true,
|
||||||
|
"gbk_bin": true,
|
||||||
|
"sjis_bin": true,
|
||||||
|
"cp932_japanese_ci": true,
|
||||||
|
"cp932_bin": true,
|
||||||
|
"gb18030_chinese_ci": true,
|
||||||
|
"gb18030_bin": true,
|
||||||
|
"gb18030_unicode_520_ci": true,
|
||||||
|
}
|
||||||
213
vendor/github.com/go-sql-driver/mysql/compress.go
generated
vendored
Normal file
213
vendor/github.com/go-sql-driver/mysql/compress.go
generated
vendored
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2024 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/zlib"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
zrPool *sync.Pool // Do not use directly. Use zDecompress() instead.
|
||||||
|
zwPool *sync.Pool // Do not use directly. Use zCompress() instead.
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
zrPool = &sync.Pool{
|
||||||
|
New: func() any { return nil },
|
||||||
|
}
|
||||||
|
zwPool = &sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
zw, err := zlib.NewWriterLevel(new(bytes.Buffer), 2)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // compress/zlib return non-nil error only if level is invalid
|
||||||
|
}
|
||||||
|
return zw
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func zDecompress(src []byte, dst *bytes.Buffer) (int, error) {
|
||||||
|
br := bytes.NewReader(src)
|
||||||
|
var zr io.ReadCloser
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if a := zrPool.Get(); a == nil {
|
||||||
|
if zr, err = zlib.NewReader(br); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
zr = a.(io.ReadCloser)
|
||||||
|
if err := zr.(zlib.Resetter).Reset(br, nil); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n, _ := dst.ReadFrom(zr) // ignore err because zr.Close() will return it again.
|
||||||
|
err = zr.Close() // zr.Close() may return chuecksum error.
|
||||||
|
zrPool.Put(zr)
|
||||||
|
return int(n), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func zCompress(src []byte, dst io.Writer) error {
|
||||||
|
zw := zwPool.Get().(*zlib.Writer)
|
||||||
|
zw.Reset(dst)
|
||||||
|
if _, err := zw.Write(src); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err := zw.Close()
|
||||||
|
zwPool.Put(zw)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type compIO struct {
|
||||||
|
mc *mysqlConn
|
||||||
|
buff bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCompIO(mc *mysqlConn) *compIO {
|
||||||
|
return &compIO{
|
||||||
|
mc: mc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compIO) reset() {
|
||||||
|
c.buff.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compIO) readNext(need int) ([]byte, error) {
|
||||||
|
for c.buff.Len() < need {
|
||||||
|
if err := c.readCompressedPacket(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data := c.buff.Next(need)
|
||||||
|
return data[:need:need], nil // prevent caller writes into c.buff
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compIO) readCompressedPacket() error {
|
||||||
|
header, err := c.mc.readNext(7)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = header[6] // bounds check hint to compiler; guaranteed by readNext
|
||||||
|
|
||||||
|
// compressed header structure
|
||||||
|
comprLength := getUint24(header[0:3])
|
||||||
|
compressionSequence := header[3]
|
||||||
|
uncompressedLength := getUint24(header[4:7])
|
||||||
|
if debug {
|
||||||
|
fmt.Printf("uncompress cmplen=%v uncomplen=%v pkt_cmp_seq=%v expected_cmp_seq=%v\n",
|
||||||
|
comprLength, uncompressedLength, compressionSequence, c.mc.sequence)
|
||||||
|
}
|
||||||
|
// Do not return ErrPktSync here.
|
||||||
|
// Server may return error packet (e.g. 1153 Got a packet bigger than 'max_allowed_packet' bytes)
|
||||||
|
// before receiving all packets from client. In this case, seqnr is younger than expected.
|
||||||
|
// NOTE: Both of mariadbclient and mysqlclient do not check seqnr. Only server checks it.
|
||||||
|
if debug && compressionSequence != c.mc.compressSequence {
|
||||||
|
fmt.Printf("WARN: unexpected cmpress seq nr: expected %v, got %v",
|
||||||
|
c.mc.compressSequence, compressionSequence)
|
||||||
|
}
|
||||||
|
c.mc.compressSequence = compressionSequence + 1
|
||||||
|
|
||||||
|
comprData, err := c.mc.readNext(comprLength)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if payload is uncompressed, its length will be specified as zero, and its
|
||||||
|
// true length is contained in comprLength
|
||||||
|
if uncompressedLength == 0 {
|
||||||
|
c.buff.Write(comprData)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// use existing capacity in bytesBuf if possible
|
||||||
|
c.buff.Grow(uncompressedLength)
|
||||||
|
nread, err := zDecompress(comprData, &c.buff)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if nread != uncompressedLength {
|
||||||
|
return fmt.Errorf("invalid compressed packet: uncompressed length in header is %d, actual %d",
|
||||||
|
uncompressedLength, nread)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const minCompressLength = 150
|
||||||
|
const maxPayloadLen = maxPacketSize - 4
|
||||||
|
|
||||||
|
// writePackets sends one or some packets with compression.
|
||||||
|
// Use this instead of mc.netConn.Write() when mc.compress is true.
|
||||||
|
func (c *compIO) writePackets(packets []byte) (int, error) {
|
||||||
|
totalBytes := len(packets)
|
||||||
|
blankHeader := make([]byte, 7)
|
||||||
|
buf := &c.buff
|
||||||
|
|
||||||
|
for len(packets) > 0 {
|
||||||
|
payloadLen := min(maxPayloadLen, len(packets))
|
||||||
|
payload := packets[:payloadLen]
|
||||||
|
uncompressedLen := payloadLen
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
buf.Write(blankHeader) // Buffer.Write() never returns error
|
||||||
|
|
||||||
|
// If payload is less than minCompressLength, don't compress.
|
||||||
|
if uncompressedLen < minCompressLength {
|
||||||
|
buf.Write(payload)
|
||||||
|
uncompressedLen = 0
|
||||||
|
} else {
|
||||||
|
err := zCompress(payload, buf)
|
||||||
|
if debug && err != nil {
|
||||||
|
fmt.Printf("zCompress error: %v", err)
|
||||||
|
}
|
||||||
|
// do not compress if compressed data is larger than uncompressed data
|
||||||
|
// I intentionally miss 7 byte header in the buf; zCompress must compress more than 7 bytes.
|
||||||
|
if err != nil || buf.Len() >= uncompressedLen {
|
||||||
|
buf.Reset()
|
||||||
|
buf.Write(blankHeader)
|
||||||
|
buf.Write(payload)
|
||||||
|
uncompressedLen = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, err := c.writeCompressedPacket(buf.Bytes(), uncompressedLen); err != nil {
|
||||||
|
// To allow returning ErrBadConn when sending really 0 bytes, we sum
|
||||||
|
// up compressed bytes that is returned by underlying Write().
|
||||||
|
return totalBytes - len(packets) + n, err
|
||||||
|
}
|
||||||
|
packets = packets[payloadLen:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeCompressedPacket writes a compressed packet with header.
|
||||||
|
// data should start with 7 size space for header followed by payload.
|
||||||
|
func (c *compIO) writeCompressedPacket(data []byte, uncompressedLen int) (int, error) {
|
||||||
|
mc := c.mc
|
||||||
|
comprLength := len(data) - 7
|
||||||
|
if debug {
|
||||||
|
fmt.Printf(
|
||||||
|
"writeCompressedPacket: comprLength=%v, uncompressedLen=%v, seq=%v\n",
|
||||||
|
comprLength, uncompressedLen, mc.compressSequence)
|
||||||
|
}
|
||||||
|
|
||||||
|
// compression header
|
||||||
|
putUint24(data[0:3], comprLength)
|
||||||
|
data[3] = mc.compressSequence
|
||||||
|
putUint24(data[4:7], uncompressedLen)
|
||||||
|
|
||||||
|
mc.compressSequence++
|
||||||
|
return mc.writeWithTimeout(data)
|
||||||
|
}
|
||||||
54
vendor/github.com/go-sql-driver/mysql/conncheck.go
generated
vendored
Normal file
54
vendor/github.com/go-sql-driver/mysql/conncheck.go
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2019 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
//go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errUnexpectedRead = errors.New("unexpected read from socket")
|
||||||
|
|
||||||
|
func connCheck(conn net.Conn) error {
|
||||||
|
var sysErr error
|
||||||
|
|
||||||
|
sysConn, ok := conn.(syscall.Conn)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rawConn, err := sysConn.SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rawConn.Read(func(fd uintptr) bool {
|
||||||
|
var buf [1]byte
|
||||||
|
n, err := syscall.Read(int(fd), buf[:])
|
||||||
|
switch {
|
||||||
|
case n == 0 && err == nil:
|
||||||
|
sysErr = io.EOF
|
||||||
|
case n > 0:
|
||||||
|
sysErr = errUnexpectedRead
|
||||||
|
case err == syscall.EAGAIN || err == syscall.EWOULDBLOCK:
|
||||||
|
sysErr = nil
|
||||||
|
default:
|
||||||
|
sysErr = err
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sysErr
|
||||||
|
}
|
||||||
17
vendor/github.com/go-sql-driver/mysql/conncheck_dummy.go
generated
vendored
Normal file
17
vendor/github.com/go-sql-driver/mysql/conncheck_dummy.go
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2019 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
//go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !illumos
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
func connCheck(conn net.Conn) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
816
vendor/github.com/go-sql-driver/mysql/connection.go
generated
vendored
Normal file
816
vendor/github.com/go-sql-driver/mysql/connection.go
generated
vendored
Normal file
@@ -0,0 +1,816 @@
|
|||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mysqlConn struct {
|
||||||
|
buf buffer
|
||||||
|
netConn net.Conn
|
||||||
|
rawConn net.Conn // underlying connection when netConn is TLS connection.
|
||||||
|
result mysqlResult // managed by clearResult() and handleOkPacket().
|
||||||
|
compIO *compIO
|
||||||
|
cfg *Config
|
||||||
|
connector *connector
|
||||||
|
maxAllowedPacket int
|
||||||
|
maxWriteSize int
|
||||||
|
capabilities capabilityFlag
|
||||||
|
extCapabilities extendedCapabilityFlag
|
||||||
|
status statusFlag
|
||||||
|
sequence uint8
|
||||||
|
compressSequence uint8
|
||||||
|
parseTime bool
|
||||||
|
compress bool
|
||||||
|
|
||||||
|
// for context support (Go 1.8+)
|
||||||
|
watching bool
|
||||||
|
watcher chan<- context.Context
|
||||||
|
closech chan struct{}
|
||||||
|
finished chan<- struct{}
|
||||||
|
canceled atomicError // set non-nil if conn is canceled
|
||||||
|
closed atomic.Bool // set when conn is closed, before closech is closed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to call per-connection logger.
|
||||||
|
func (mc *mysqlConn) log(v ...any) {
|
||||||
|
_, filename, lineno, ok := runtime.Caller(1)
|
||||||
|
if ok {
|
||||||
|
pos := strings.LastIndexByte(filename, '/')
|
||||||
|
if pos != -1 {
|
||||||
|
filename = filename[pos+1:]
|
||||||
|
}
|
||||||
|
prefix := fmt.Sprintf("%s:%d ", filename, lineno)
|
||||||
|
v = append([]any{prefix}, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.cfg.Logger.Print(v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) readWithTimeout(b []byte) (int, error) {
|
||||||
|
to := mc.cfg.ReadTimeout
|
||||||
|
if to > 0 {
|
||||||
|
if err := mc.netConn.SetReadDeadline(time.Now().Add(to)); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mc.netConn.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) writeWithTimeout(b []byte) (int, error) {
|
||||||
|
to := mc.cfg.WriteTimeout
|
||||||
|
if to > 0 {
|
||||||
|
if err := mc.netConn.SetWriteDeadline(time.Now().Add(to)); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mc.netConn.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) resetSequence() {
|
||||||
|
mc.sequence = 0
|
||||||
|
mc.compressSequence = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncSequence must be called when finished writing some packet and before start reading.
|
||||||
|
func (mc *mysqlConn) syncSequence() {
|
||||||
|
// Syncs compressionSequence to sequence.
|
||||||
|
// This is not documented but done in `net_flush()` in MySQL and MariaDB.
|
||||||
|
// https://github.com/mariadb-corporation/mariadb-connector-c/blob/8228164f850b12353da24df1b93a1e53cc5e85e9/libmariadb/ma_net.c#L170-L171
|
||||||
|
// https://github.com/mysql/mysql-server/blob/824e2b4064053f7daf17d7f3f84b7a3ed92e5fb4/sql-common/net_serv.cc#L293
|
||||||
|
if mc.compress {
|
||||||
|
mc.sequence = mc.compressSequence
|
||||||
|
mc.compIO.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles parameters set in DSN after the connection is established
|
||||||
|
func (mc *mysqlConn) handleParams() (err error) {
|
||||||
|
var cmdSet strings.Builder
|
||||||
|
|
||||||
|
for param, val := range mc.cfg.Params {
|
||||||
|
if cmdSet.Len() == 0 {
|
||||||
|
// Heuristic: 29 chars for each other key=value to reduce reallocations
|
||||||
|
cmdSet.Grow(4 + len(param) + 3 + len(val) + 30*(len(mc.cfg.Params)-1))
|
||||||
|
cmdSet.WriteString("SET ")
|
||||||
|
} else {
|
||||||
|
cmdSet.WriteString(", ")
|
||||||
|
}
|
||||||
|
cmdSet.WriteString(param)
|
||||||
|
cmdSet.WriteString(" = ")
|
||||||
|
cmdSet.WriteString(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmdSet.Len() > 0 {
|
||||||
|
err = mc.exec(cmdSet.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// markBadConn replaces errBadConnNoWrite with driver.ErrBadConn.
|
||||||
|
// This function is used to return driver.ErrBadConn only when safe to retry.
|
||||||
|
func (mc *mysqlConn) markBadConn(err error) error {
|
||||||
|
if err == errBadConnNoWrite {
|
||||||
|
return driver.ErrBadConn
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) Begin() (driver.Tx, error) {
|
||||||
|
return mc.begin(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) {
|
||||||
|
if mc.closed.Load() {
|
||||||
|
return nil, driver.ErrBadConn
|
||||||
|
}
|
||||||
|
var q string
|
||||||
|
if readOnly {
|
||||||
|
q = "START TRANSACTION READ ONLY"
|
||||||
|
} else {
|
||||||
|
q = "START TRANSACTION"
|
||||||
|
}
|
||||||
|
err := mc.exec(q)
|
||||||
|
if err == nil {
|
||||||
|
return &mysqlTx{mc}, err
|
||||||
|
}
|
||||||
|
return nil, mc.markBadConn(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) Close() (err error) {
|
||||||
|
// Makes Close idempotent
|
||||||
|
if !mc.closed.Load() {
|
||||||
|
err = mc.writeCommandPacket(comQuit)
|
||||||
|
}
|
||||||
|
mc.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// close closes the network connection and clear results without sending COM_QUIT.
|
||||||
|
func (mc *mysqlConn) close() {
|
||||||
|
mc.cleanup()
|
||||||
|
mc.clearResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes the network connection and unsets internal variables. Do not call this
|
||||||
|
// function after successful authentication, call Close instead. This function
|
||||||
|
// is called before auth or on auth failure because MySQL will have already
|
||||||
|
// closed the network connection.
|
||||||
|
func (mc *mysqlConn) cleanup() {
|
||||||
|
if mc.closed.Swap(true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Makes cleanup idempotent
|
||||||
|
close(mc.closech)
|
||||||
|
conn := mc.rawConn
|
||||||
|
if conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
mc.log("closing connection:", err)
|
||||||
|
}
|
||||||
|
// This function can be called from multiple goroutines.
|
||||||
|
// So we can not mc.clearResult() here.
|
||||||
|
// Caller should do it if they are in safe goroutine.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) error() error {
|
||||||
|
if mc.closed.Load() {
|
||||||
|
if err := mc.canceled.Value(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ErrInvalidConn
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) {
|
||||||
|
if mc.closed.Load() {
|
||||||
|
return nil, driver.ErrBadConn
|
||||||
|
}
|
||||||
|
// Send command
|
||||||
|
err := mc.writeCommandPacketStr(comStmtPrepare, query)
|
||||||
|
if err != nil {
|
||||||
|
// STMT_PREPARE is safe to retry. So we can return ErrBadConn here.
|
||||||
|
mc.log(err)
|
||||||
|
return nil, driver.ErrBadConn
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt := &mysqlStmt{
|
||||||
|
mc: mc,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read Result
|
||||||
|
columnCount, err := stmt.readPrepareResultPacket()
|
||||||
|
if err == nil {
|
||||||
|
if stmt.paramCount > 0 {
|
||||||
|
if err = mc.skipColumns(stmt.paramCount); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if columnCount > 0 {
|
||||||
|
if mc.extCapabilities&clientCacheMetadata != 0 {
|
||||||
|
if stmt.columns, err = mc.readColumns(int(columnCount), nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err = mc.skipColumns(int(columnCount)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stmt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (string, error) {
|
||||||
|
noBackslashEscapes := (mc.status & statusNoBackslashEscapes) != 0
|
||||||
|
const (
|
||||||
|
stateNormal = iota
|
||||||
|
stateString
|
||||||
|
stateEscape
|
||||||
|
stateEOLComment
|
||||||
|
stateSlashStarComment
|
||||||
|
stateBacktick
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
QUOTE_BYTE = byte('\'')
|
||||||
|
DBL_QUOTE_BYTE = byte('"')
|
||||||
|
BACKSLASH_BYTE = byte('\\')
|
||||||
|
QUESTION_MARK_BYTE = byte('?')
|
||||||
|
SLASH_BYTE = byte('/')
|
||||||
|
STAR_BYTE = byte('*')
|
||||||
|
HASH_BYTE = byte('#')
|
||||||
|
MINUS_BYTE = byte('-')
|
||||||
|
LINE_FEED_BYTE = byte('\n')
|
||||||
|
BACKTICK_BYTE = byte('`')
|
||||||
|
)
|
||||||
|
|
||||||
|
buf, err := mc.buf.takeCompleteBuffer()
|
||||||
|
if err != nil {
|
||||||
|
mc.cleanup()
|
||||||
|
return "", driver.ErrBadConn
|
||||||
|
}
|
||||||
|
buf = buf[:0]
|
||||||
|
state := stateNormal
|
||||||
|
singleQuotes := false
|
||||||
|
lastChar := byte(0)
|
||||||
|
argPos := 0
|
||||||
|
lenQuery := len(query)
|
||||||
|
lastIdx := 0
|
||||||
|
|
||||||
|
for i := range lenQuery {
|
||||||
|
currentChar := query[i]
|
||||||
|
if state == stateEscape && !((currentChar == QUOTE_BYTE && singleQuotes) || (currentChar == DBL_QUOTE_BYTE && !singleQuotes)) {
|
||||||
|
state = stateString
|
||||||
|
lastChar = currentChar
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch currentChar {
|
||||||
|
case STAR_BYTE:
|
||||||
|
if state == stateNormal && lastChar == SLASH_BYTE {
|
||||||
|
state = stateSlashStarComment
|
||||||
|
}
|
||||||
|
case SLASH_BYTE:
|
||||||
|
if state == stateSlashStarComment && lastChar == STAR_BYTE {
|
||||||
|
state = stateNormal
|
||||||
|
// Clear lastChar so the '/' that closed the comment isn't
|
||||||
|
// reused to start a new comment with a following '*'.
|
||||||
|
lastChar = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case HASH_BYTE:
|
||||||
|
if state == stateNormal {
|
||||||
|
state = stateEOLComment
|
||||||
|
}
|
||||||
|
case MINUS_BYTE:
|
||||||
|
if state == stateNormal && lastChar == MINUS_BYTE {
|
||||||
|
// -- only starts a comment if followed by whitespace or control char
|
||||||
|
if i+1 < lenQuery {
|
||||||
|
nextChar := query[i+1]
|
||||||
|
if nextChar == ' ' || nextChar == '\t' || nextChar == '\n' || nextChar == '\r' {
|
||||||
|
state = stateEOLComment
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state = stateEOLComment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case LINE_FEED_BYTE:
|
||||||
|
if state == stateEOLComment {
|
||||||
|
state = stateNormal
|
||||||
|
}
|
||||||
|
case DBL_QUOTE_BYTE:
|
||||||
|
if state == stateNormal {
|
||||||
|
state = stateString
|
||||||
|
singleQuotes = false
|
||||||
|
} else if state == stateString && !singleQuotes {
|
||||||
|
state = stateNormal
|
||||||
|
} else if state == stateEscape {
|
||||||
|
state = stateString
|
||||||
|
}
|
||||||
|
case QUOTE_BYTE:
|
||||||
|
if state == stateNormal {
|
||||||
|
state = stateString
|
||||||
|
singleQuotes = true
|
||||||
|
} else if state == stateString && singleQuotes {
|
||||||
|
state = stateNormal
|
||||||
|
} else if state == stateEscape {
|
||||||
|
state = stateString
|
||||||
|
}
|
||||||
|
case BACKSLASH_BYTE:
|
||||||
|
if state == stateString && !noBackslashEscapes {
|
||||||
|
state = stateEscape
|
||||||
|
}
|
||||||
|
case QUESTION_MARK_BYTE:
|
||||||
|
if state == stateNormal {
|
||||||
|
if argPos >= len(args) {
|
||||||
|
return "", driver.ErrSkip
|
||||||
|
}
|
||||||
|
buf = append(buf, query[lastIdx:i]...)
|
||||||
|
arg := args[argPos]
|
||||||
|
argPos++
|
||||||
|
|
||||||
|
if arg == nil {
|
||||||
|
buf = append(buf, "NULL"...)
|
||||||
|
lastIdx = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := arg.(type) {
|
||||||
|
case int64:
|
||||||
|
buf = strconv.AppendInt(buf, v, 10)
|
||||||
|
case uint64:
|
||||||
|
buf = strconv.AppendUint(buf, v, 10)
|
||||||
|
case float64:
|
||||||
|
buf = strconv.AppendFloat(buf, v, 'g', -1, 64)
|
||||||
|
case bool:
|
||||||
|
if v {
|
||||||
|
buf = append(buf, '1')
|
||||||
|
} else {
|
||||||
|
buf = append(buf, '0')
|
||||||
|
}
|
||||||
|
case time.Time:
|
||||||
|
if v.IsZero() {
|
||||||
|
buf = append(buf, "'0000-00-00'"...)
|
||||||
|
} else {
|
||||||
|
buf = append(buf, '\'')
|
||||||
|
buf, err = appendDateTime(buf, v.In(mc.cfg.Loc), mc.cfg.timeTruncate)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
buf = append(buf, '\'')
|
||||||
|
}
|
||||||
|
case json.RawMessage:
|
||||||
|
if noBackslashEscapes {
|
||||||
|
buf = escapeBytesQuotes(buf, v, false)
|
||||||
|
} else {
|
||||||
|
buf = escapeBytesBackslash(buf, v, false)
|
||||||
|
}
|
||||||
|
case []byte:
|
||||||
|
if v == nil {
|
||||||
|
buf = append(buf, "NULL"...)
|
||||||
|
} else {
|
||||||
|
if noBackslashEscapes {
|
||||||
|
buf = escapeBytesQuotes(buf, v, true)
|
||||||
|
} else {
|
||||||
|
buf = escapeBytesBackslash(buf, v, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
if noBackslashEscapes {
|
||||||
|
buf = escapeStringQuotes(buf, v)
|
||||||
|
} else {
|
||||||
|
buf = escapeStringBackslash(buf, v)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "", driver.ErrSkip
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buf)+4 > mc.maxAllowedPacket {
|
||||||
|
return "", driver.ErrSkip
|
||||||
|
}
|
||||||
|
lastIdx = i + 1
|
||||||
|
}
|
||||||
|
case BACKTICK_BYTE:
|
||||||
|
if state == stateBacktick {
|
||||||
|
state = stateNormal
|
||||||
|
} else if state == stateNormal {
|
||||||
|
state = stateBacktick
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastChar = currentChar
|
||||||
|
}
|
||||||
|
buf = append(buf, query[lastIdx:]...)
|
||||||
|
if argPos != len(args) {
|
||||||
|
return "", driver.ErrSkip
|
||||||
|
}
|
||||||
|
return string(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) {
|
||||||
|
if mc.closed.Load() {
|
||||||
|
return nil, driver.ErrBadConn
|
||||||
|
}
|
||||||
|
if len(args) != 0 {
|
||||||
|
if !mc.cfg.InterpolateParams {
|
||||||
|
return nil, driver.ErrSkip
|
||||||
|
}
|
||||||
|
// try to interpolate the parameters to save extra roundtrips for preparing and closing a statement
|
||||||
|
prepared, err := mc.interpolateParams(query, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
query = prepared
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mc.exec(query)
|
||||||
|
if err == nil {
|
||||||
|
copied := mc.result
|
||||||
|
return &copied, err
|
||||||
|
}
|
||||||
|
return nil, mc.markBadConn(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal function to execute commands
|
||||||
|
func (mc *mysqlConn) exec(query string) error {
|
||||||
|
handleOk := mc.clearResult()
|
||||||
|
// Send command
|
||||||
|
if err := mc.writeCommandPacketStr(comQuery, query); err != nil {
|
||||||
|
return mc.markBadConn(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read Result
|
||||||
|
resLen, _, err := handleOk.readResultSetHeaderPacket()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resLen > 0 {
|
||||||
|
// columns
|
||||||
|
if err := mc.skipColumns(resLen); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// rows
|
||||||
|
if err := mc.skipRows(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleOk.discardResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) {
|
||||||
|
return mc.query(query, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) {
|
||||||
|
handleOk := mc.clearResult()
|
||||||
|
|
||||||
|
if mc.closed.Load() {
|
||||||
|
return nil, driver.ErrBadConn
|
||||||
|
}
|
||||||
|
if len(args) != 0 {
|
||||||
|
if !mc.cfg.InterpolateParams {
|
||||||
|
return nil, driver.ErrSkip
|
||||||
|
}
|
||||||
|
// try client-side prepare to reduce roundtrip
|
||||||
|
prepared, err := mc.interpolateParams(query, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
query = prepared
|
||||||
|
}
|
||||||
|
// Send command
|
||||||
|
err := mc.writeCommandPacketStr(comQuery, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, mc.markBadConn(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read Result
|
||||||
|
var resLen int
|
||||||
|
resLen, _, err = handleOk.readResultSetHeaderPacket()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := new(textRows)
|
||||||
|
rows.mc = mc
|
||||||
|
|
||||||
|
if resLen == 0 {
|
||||||
|
rows.rs.done = true
|
||||||
|
|
||||||
|
switch err := rows.NextResultSet(); err {
|
||||||
|
case nil, io.EOF:
|
||||||
|
return rows, nil
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Columns
|
||||||
|
rows.rs.columns, err = mc.readColumns(resLen, nil)
|
||||||
|
return rows, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the value of the given MySQL System Variable
|
||||||
|
func (mc *mysqlConn) getSystemVar(name string) (string, error) {
|
||||||
|
// Send command
|
||||||
|
handleOk := mc.clearResult()
|
||||||
|
if err := mc.writeCommandPacketStr(comQuery, "SELECT @@"+name); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read Result
|
||||||
|
resLen, _, err := handleOk.readResultSetHeaderPacket()
|
||||||
|
if err == nil {
|
||||||
|
rows := new(textRows)
|
||||||
|
rows.mc = mc
|
||||||
|
rows.rs.columns = []mysqlField{{fieldType: fieldTypeVarChar}}
|
||||||
|
|
||||||
|
if resLen > 0 {
|
||||||
|
// Columns
|
||||||
|
if err := mc.skipColumns(resLen); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := make([]driver.Value, resLen)
|
||||||
|
if err = rows.readRow(dest); err == nil {
|
||||||
|
// Convert to string before skipRows, which may
|
||||||
|
// overwrite the read buffer that dest[0] points into.
|
||||||
|
val := string(dest[0].([]byte))
|
||||||
|
return val, mc.skipRows()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancel is called when the query has canceled.
|
||||||
|
func (mc *mysqlConn) cancel(err error) {
|
||||||
|
mc.canceled.Set(err)
|
||||||
|
mc.cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish is called when the query has succeeded.
|
||||||
|
func (mc *mysqlConn) finish() {
|
||||||
|
if !mc.watching || mc.finished == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case mc.finished <- struct{}{}:
|
||||||
|
mc.watching = false
|
||||||
|
case <-mc.closech:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping implements driver.Pinger interface
|
||||||
|
func (mc *mysqlConn) Ping(ctx context.Context) (err error) {
|
||||||
|
if mc.closed.Load() {
|
||||||
|
return driver.ErrBadConn
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = mc.watchCancel(ctx); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer mc.finish()
|
||||||
|
|
||||||
|
handleOk := mc.clearResult()
|
||||||
|
if err = mc.writeCommandPacket(comPing); err != nil {
|
||||||
|
return mc.markBadConn(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleOk.readResultOK()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeginTx implements driver.ConnBeginTx interface
|
||||||
|
func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
|
||||||
|
if mc.closed.Load() {
|
||||||
|
return nil, driver.ErrBadConn
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mc.watchCancel(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer mc.finish()
|
||||||
|
|
||||||
|
if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault {
|
||||||
|
level, err := mapIsolationLevel(opts.Isolation)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = mc.exec("SET TRANSACTION ISOLATION LEVEL " + level)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mc.begin(opts.ReadOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
||||||
|
dargs, err := namedValueToValue(args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mc.watchCancel(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := mc.query(query, dargs)
|
||||||
|
if err != nil {
|
||||||
|
mc.finish()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rows.finish = mc.finish
|
||||||
|
return rows, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
||||||
|
dargs, err := namedValueToValue(args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mc.watchCancel(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer mc.finish()
|
||||||
|
|
||||||
|
return mc.Exec(query, dargs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
|
||||||
|
if err := mc.watchCancel(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := mc.Prepare(query)
|
||||||
|
mc.finish()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
default:
|
||||||
|
case <-ctx.Done():
|
||||||
|
stmt.Close()
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
return stmt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stmt *mysqlStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
||||||
|
dargs, err := namedValueToValue(args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stmt.mc.watchCancel(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := stmt.query(dargs)
|
||||||
|
if err != nil {
|
||||||
|
stmt.mc.finish()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rows.finish = stmt.mc.finish
|
||||||
|
return rows, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stmt *mysqlStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
|
||||||
|
dargs, err := namedValueToValue(args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stmt.mc.watchCancel(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer stmt.mc.finish()
|
||||||
|
|
||||||
|
return stmt.Exec(dargs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) watchCancel(ctx context.Context) error {
|
||||||
|
if mc.watching {
|
||||||
|
// Reach here if canceled,
|
||||||
|
// so the connection is already invalid
|
||||||
|
mc.cleanup()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// When ctx is already cancelled, don't watch it.
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// When ctx is not cancellable, don't watch it.
|
||||||
|
if ctx.Done() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// When watcher is not alive, can't watch it.
|
||||||
|
if mc.watcher == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.watching = true
|
||||||
|
mc.watcher <- ctx
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) startWatcher() {
|
||||||
|
watcher := make(chan context.Context, 1)
|
||||||
|
mc.watcher = watcher
|
||||||
|
finished := make(chan struct{})
|
||||||
|
mc.finished = finished
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
var ctx context.Context
|
||||||
|
select {
|
||||||
|
case ctx = <-watcher:
|
||||||
|
case <-mc.closech:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
mc.cancel(ctx.Err())
|
||||||
|
case <-finished:
|
||||||
|
case <-mc.closech:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mysqlConn) CheckNamedValue(nv *driver.NamedValue) (err error) {
|
||||||
|
nv.Value, err = converter{}.ConvertValue(nv.Value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetSession implements driver.SessionResetter.
|
||||||
|
// (From Go 1.10)
|
||||||
|
func (mc *mysqlConn) ResetSession(ctx context.Context) error {
|
||||||
|
if mc.closed.Load() || mc.buf.busy() {
|
||||||
|
return driver.ErrBadConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform a stale connection check. We only perform this check for
|
||||||
|
// the first query on a connection that has been checked out of the
|
||||||
|
// connection pool: a fresh connection from the pool is more likely
|
||||||
|
// to be stale, and it has not performed any previous writes that
|
||||||
|
// could cause data corruption, so it's safe to return ErrBadConn
|
||||||
|
// if the check fails.
|
||||||
|
if mc.cfg.CheckConnLiveness {
|
||||||
|
conn := mc.netConn
|
||||||
|
if mc.rawConn != nil {
|
||||||
|
conn = mc.rawConn
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if mc.cfg.ReadTimeout != 0 {
|
||||||
|
err = conn.SetReadDeadline(time.Now().Add(mc.cfg.ReadTimeout))
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = connCheck(conn)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
mc.log("closing bad idle connection: ", err)
|
||||||
|
return driver.ErrBadConn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid implements driver.Validator interface
|
||||||
|
// (From Go 1.15)
|
||||||
|
func (mc *mysqlConn) IsValid() bool {
|
||||||
|
return !mc.closed.Load() && !mc.buf.busy()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ driver.SessionResetter = &mysqlConn{}
|
||||||
|
var _ driver.Validator = &mysqlConn{}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user