package daemon import ( "encoding/json" "errors" "fmt" "log" "regexp" "sort" "sync" "time" "gordenko.dev/dima/fixme" "gordenko.dev/dima/flashscore" fsmodel "gordenko.dev/dima/flashscore/model" "gordenko.dev/dima/onexbet" "gordenko.dev/dima/pretty" "gordenko.dev/dima/tipper/model" "gordenko.dev/dima/web" "gordenko.dev/dima/web/api" "gordenko.dev/dima/ws" "gordenko.dev/dima/ws/publisher" ) const ( ChannelHandball = "handball" ChannelFootball = "football" ChannelTennis = "tennis" // Флаги для округления тотала roundUp = 0 roundDown = 1 // /SportFootball int64 = 1 MarketTotalH1 = 1 MarketTotal = 2 MarketHandicap = 3 SideOver = 1 SideUnder = 2 SideHome = 1 SideAway = 2 WSMessageTipsChanged = "tipsChanged" // WSMessageTeamMatches Обновленный список матчей после синхронизации с flashscore WSMessageTeamMatches = "teamMatches" WSMessageWhyNot = "whyNot" WSMessageInplayStatusChanged = "inplayStatusChanged" ) var onexbetSportToFlashscoreSport = map[onexbet.OnexbetSport]int{ onexbet.Football: flashscore.Football, onexbet.Handball: flashscore.Handball, onexbet.Tennis: flashscore.Tennis, } var flashscoreSportToOnexbetSport = map[int]onexbet.OnexbetSport{ flashscore.Football: onexbet.Football, flashscore.Handball: onexbet.Handball, flashscore.Tennis: onexbet.Tennis, } var sportToTelegramChatID = map[int]string{ flashscore.Football: footballTelegramChatID, flashscore.Handball: handballTelegramChatID, flashscore.Tennis: tennisTelegramChatID, } type InplayStatusMessage struct { MatchID string `json:"matchID"` IsInplay bool `json:"isInplay"` IsLinkedToOnexbet bool `json:"isLinkedToOnexbet"` } type TeamMatchesMessage struct { SportID int `json:"sportID"` Matches []TeamMatch `json:"matches"` } // flashscore sportID func getChannel(sportID int) string { switch sportID { case flashscore.Football: return ChannelFootball case flashscore.Handball: return ChannelHandball case flashscore.Tennis: return ChannelTennis default: panic(fmt.Sprintf("Bug: channel is undefined for the sportID %d", sportID)) } } type TipResult struct { Status model.TipStatus Result string } type StrategyOptions struct { ID int64 NotifyInTelegram bool IsChampAccepted func(string) bool } type TeamStrategy interface { ID() int64 ShortName() string // (report) notes, isInteresting IsInteresting(*TeamMatch) (interface{}, bool) // return -> comment why not, GetTip(*TeamMatch, onexbet.TeamLiveMatchData) (string, *BetDetails, TipCode) GetResult(model.Tip, onexbet.TeamMatchResult) *TipResult IsNotifyInTelegram() bool GetTelegramTipPattern() string IsNeedToWatch(onexbet.LiveMatch) bool } type ChampRatingKey struct { ChampID string StrategyID int64 } type ChampRating struct { Won int Lost int } func (s *ChampRating) GetAccuracy() (accuracy float64) { // total = 100% // won = x% // x = (won*100) / total total := s.Won + s.Lost if total > 0 { accuracy = float64(s.Won*100) / float64(total) } return } type FavoriteSide int const ( FavoriteHome FavoriteSide = 1 FavoriteAway FavoriteSide = 2 ) type TeamMatch struct { // COMMON Score onexbet.Score `json:"score"` ScoreByPeriods map[int]onexbet.Score `json:"scoreByPeriods"` CurrentPeriod int `json:"currentPeriod"` // TENNIS stats TwoSetsBreakpoints int `json:"twoSetsBreakpoints"` // Для стратегий где нужно определить доматчевого фаворита // 0 - фаворит не определен FavoriteSide FavoriteSide `json:"favoriteSide"` MaxFavoritHandicapIn1stSet *onexbet.ParamOffer `json:"maxFavoritHandicapIn1stSet"` // less than or equal 1.6 (для алгоритма 4 в теннисе) Lte16FavoriteSide FavoriteSide `json:"lte16FavoriteSide"` //IsTargetPriceIn2ndSetFound bool // Football Favorite string `json:"favorite,omitempty"` // Handball // Начальный тотал (по flashscore до матча) BasicTotal float64 `json:"basicTotal"` H1BasicTotal float64 `json:"h1BasicTotal"` HomeGoalsPerMinute float64 `json:"homeGoalsPerMinute"` AwayGoalsPerMinute float64 `json:"awayGoalsPerMinute"` H2HGoalsPerMinute float64 `json:"h2hGoalsPerMinute"` GamePace float64 `json:"-"` GamePaceFrame int `json:"-"` // Main MatchID string `json:"matchID"` Home fsmodel.Team `json:"home"` Away fsmodel.Team `json:"away"` Zone fsmodel.TeamZone `json:"zone"` Champ fsmodel.TeamChamp `json:"champ"` FullChampName string `json:"fullChampName"` IsLinkedToOnexbet bool `json:"isLinkedToOnexbet"` IsInplay bool `json:"isInplay"` // узнать - отписались от обновлений или нет? SportID int `json:"sportID"` Strategies map[int64]TeamStrategy `json:"-"` StartTime int64 `json:"startTime"` ParseTime time.Time `json:"parseTime"` OnexbetMatchID string `json:"onexbetMatchID"` OnexbetStatsMatchID string `json:"onexbetStatsMatchID"` IsTeamsSwapped bool `json:"isTeamsSwapped"` Notes []StrategyNotes `json:"notes"` //TipTotalOver float64 `json:"tipTotalOver"` //TipTotalUnder float64 `json:"tipTotalUnder"` // Ожимаемый темп игры (голов в минуту). Начальный тотал / 60 минут //BasicGoalsPerMinute float64 `json:"basicGoalsPerMinute"` //BasicOverGoalsPerMinute float64 `json:"basicOverGoalsPerMinute"` //BasicUnderGoalsPerMinute float64 `json:"basicUnderGoalsPerMinute"` // Статистика для расчета HomeTeamMatches []fsmodel.TeamMatch `json:"homeTeamMatches"` AwayTeamMatches []fsmodel.TeamMatch `json:"awayTeamMatches"` H2H []fsmodel.TeamMatch `json:"h2h"` Odds flashscore.Odds `json:"odds"` } type StrategyNotes struct { StrategyID int64 `json:"strategyID"` Notes interface{} `json:"notes"` } type TipCode int const ( Waiting TipCode = 0 Unwatch TipCode = 1 Bet TipCode = 2 ) type BetDetails struct { Market int Side int Param string Price float64 TelegramMessage string } /* var tipTextAlgo100Pattern = `Сигнал # %d. Будет ГОЛ! Алгоритм 100 Первый тайм, Тотал Больше 0.5 Футбол. %s. %s %s - %s Коэф. %.3f` */ var tipResultTextPattern = `Сигнал # %d. %s %s ` const ( EmptyTipStatus fixme.Code = "T1" WrongTipStatus fixme.Code = "T2" EmptyValue fixme.Code = "T3" ) var errorMessages = map[fixme.Code]string{ EmptyTipStatus: "не указан статус", WrongTipStatus: "неверный статус", EmptyValue: "пустое значение недопустимо", } var fix = fixme.New(errorMessages) type Options struct { Logger *log.Logger WsPublicServer *ws.PublicServer FlashscoreAPIAddr string OnexbetWsURL string OnexbetAPIURL string SportIDs []int FlashscoreModel *fsmodel.Model TipperModel *model.Model } type Daemon struct { mutex sync.Mutex fsAPIClient *api.Client onexbetClient *ws.Client logger *log.Logger model *model.Model wsServer *ws.PublicServer publisher *publisher.PublicPublisher onexbetAPIClient *api.Client fsmodel *fsmodel.Model champRatings map[int]map[ChampRatingKey]*ChampRating //map[int64]map[string]*ChampRating //handballChampRatings map[ChampRatingKey]*ChampRating // Отобаранные матчи по видам спорта teamMatches map[int]map[string]*TeamMatch onexbetMatchToTeamMatch map[string]*TeamMatch sportStrategies map[int]map[int64]TeamStrategy historyProcessors map[int]func(*TeamMatch) sportIDs []int unrecognizedOnexbetMatches map[int]map[string]bool identifiedMatches map[string]bool // 1xbet MatchID => true } func New(opt Options) (*Daemon, error) { s := new(Daemon) s.logger = opt.Logger s.sportIDs = opt.SportIDs s.identifiedMatches = make(map[string]bool) s.model = opt.TipperModel s.fsmodel = opt.FlashscoreModel // TELEGRAM, topChamps /* s.footballStrategies[11] = NewP1Over05(StrategyOptions{ ID: 11, NotifyInTelegram: true, IsChampAccepted: acceptedChamps(topChamps), }) s.footballStrategies[22] = NewOver05(StrategyOptions{ ID: 22, NotifyInTelegram: true, IsChampAccepted: acceptedChamps(topChamps), }) s.footballStrategies[33] = NewOver15(StrategyOptions{ ID: 33, NotifyInTelegram: true, IsChampAccepted: acceptedChamps(topChamps), }) // Мои стратегии - ТМ для всех чемпионатов, кроме топовых s.footballStrategies[701] = NewP1Under05(StrategyOptions{ ID: 701, NotifyInTelegram: false, IsChampAccepted: nonAcceptedChamps(topChamps), }) s.footballStrategies[702] = NewUnder05(StrategyOptions{ ID: 702, NotifyInTelegram: false, IsChampAccepted: nonAcceptedChamps(topChamps), }) s.footballStrategies[703] = NewUnder15(StrategyOptions{ ID: 703, NotifyInTelegram: false, IsChampAccepted: nonAcceptedChamps(topChamps), }) */ if opt.FlashscoreAPIAddr == "" { return nil, errors.New("FlashscoreAPIAddr option is required") } var err error s.fsAPIClient, err = api.NewClient(api.ClientOptions{ URL: opt.FlashscoreAPIAddr, BuildRequest: api.BuildEnvelopeRequest, }) if err != nil { return nil, fmt.Errorf("api.NewClient(flashscore): %s", err) } s.onexbetClient, err = ws.NewClient(ws.ClientOptions{ URL: opt.OnexbetWsURL, Logger: opt.Logger, }) if err != nil { return nil, fmt.Errorf("ws.NewClient(onexbet): %s", err) } s.onexbetClient.OnConnected(s.onOnexbetWsClientConnected) s.onexbetClient.Handle(onexbet.WsMsgLiveMatches, s.onOnexbetTeamLiveMatches) s.onexbetClient.Handle(onexbet.WsMsgTeamLiveMatchData, s.onOnexbetTeamLiveMatchData) s.onexbetClient.Handle(onexbet.WsMsgTennisLiveMatchData, s.onOnexbetTeamLiveMatchData) s.wsServer = opt.WsPublicServer s.wsServer.OnConnected(s.onWsClientConnected) s.wsServer.OnDisconnected(s.onWsClientDisconnected) s.publisher, err = publisher.NewPublicPublisher(publisher.PublicPublisherOptions{ WsPublicServer: s.wsServer, }) s.publisher.OnTopicSubscribeRequest(ChannelHandball, s.onWsSubscribeRequestToHandball) s.publisher.OnTopicSubscribeRequest(ChannelFootball, s.onWsSubscribeRequestToFootball) s.publisher.OnTopicSubscribeRequest(ChannelTennis, s.onWsSubscribeRequestToTennis) s.onexbetAPIClient, err = api.NewClient(api.ClientOptions{ URL: opt.OnexbetAPIURL, BuildRequest: api.BuildEnvelopeRequest, }) if err != nil { return nil, fmt.Errorf("api.NewClient(onexbet): %s", err) } s.teamMatches = make(map[int]map[string]*TeamMatch) s.onexbetMatchToTeamMatch = make(map[string]*TeamMatch) s.sportStrategies = make(map[int]map[int64]TeamStrategy) s.unrecognizedOnexbetMatches = make(map[int]map[string]bool) s.champRatings = make(map[int]map[ChampRatingKey]*ChampRating) for _, sportID := range s.sportIDs { s.teamMatches[sportID] = make(map[string]*TeamMatch) s.sportStrategies[sportID] = make(map[int64]TeamStrategy) s.unrecognizedOnexbetMatches[sportID] = make(map[string]bool) s.champRatings[sportID] = make(map[ChampRatingKey]*ChampRating) } // Набор статистики по всем чемпионатам // FOOTBALL /* s.sportStrategies[flashscore.Football][1] = NewP1Over05(StrategyOptions{ ID: 1, NotifyInTelegram: true, }) s.sportStrategies[flashscore.Football][2] = NewOver05(StrategyOptions{ ID: 2, NotifyInTelegram: true, }) s.sportStrategies[flashscore.Football][3] = NewOver15(StrategyOptions{ ID: 3, NotifyInTelegram: true, }) */ s.sportStrategies[flashscore.Football][6] = NewFootballP1Over05(StrategyOptions{ ID: 6, NotifyInTelegram: true, }) s.sportStrategies[flashscore.Football][7] = NewFootballOver05(StrategyOptions{ ID: 7, NotifyInTelegram: true, }) s.sportStrategies[flashscore.Football][8] = NewFootballOver15(StrategyOptions{ ID: 8, NotifyInTelegram: true, }) // HANDBALL s.sportStrategies[flashscore.Handball][4] = NewHandballFTOver(HandballStrategyOptions{ ID: 4, NotifyInTelegram: true, TotalDiff: 10, FixateGamePaceEveryNMinutes: 5, }) s.sportStrategies[flashscore.Handball][5] = NewHandballFTUnder(HandballStrategyOptions{ ID: 5, NotifyInTelegram: true, TotalDiff: 10, FixateGamePaceEveryNMinutes: 5, }) // HANDBALL s.sportStrategies[flashscore.Handball][11] = NewHandballP1Over(HandballStrategyOptions{ ID: 11, NotifyInTelegram: true, TotalDiff: 7, FixateGamePaceEveryNMinutes: 5, }) s.sportStrategies[flashscore.Handball][12] = NewHandballP1Under(HandballStrategyOptions{ ID: 12, NotifyInTelegram: true, TotalDiff: 7, FixateGamePaceEveryNMinutes: 5, }) /* s.sportStrategies[flashscore.Handball][13] = NewHandballP1Over(HandballStrategyOptions{ ID: 13, NotifyInTelegram: true, TotalDiff: 5, FixateGamePaceEveryNMinutes: 5, }) s.sportStrategies[flashscore.Handball][14] = NewHandballP1Under(HandballStrategyOptions{ ID: 14, NotifyInTelegram: true, TotalDiff: 5, FixateGamePaceEveryNMinutes: 5, }) */ // TENNIS s.sportStrategies[flashscore.Tennis][15] = NewTennis3rdSetTotalUnder(StrategyOptions{ ID: 15, NotifyInTelegram: true, }) s.sportStrategies[flashscore.Tennis][16] = NewTennis2ndSetPlusHandicap(StrategyOptions{ ID: 16, NotifyInTelegram: true, }) s.sportStrategies[flashscore.Tennis][17] = NewTennis2ndSetPlusHandicapAny(StrategyOptions{ ID: 17, NotifyInTelegram: true, }) s.sportStrategies[flashscore.Tennis][18] = NewTennis2ndSetFavoriteHandicap(StrategyOptions{ ID: 18, NotifyInTelegram: true, }) s.historyProcessors = make(map[int]func(*TeamMatch)) s.historyProcessors[flashscore.Handball] = HandballHistoryProcessor // Формируем рейтинг для футбольных стратегий for sportID, sportStrategies := range s.sportStrategies { champRatings := s.champRatings[sportID] for strategyID := range sportStrategies { var stats []model.StrategyChampStats stats, err = s.model.ListStrategyChampStats(strategyID) if err != nil { return nil, fmt.Errorf("model.ListStrategyChampStats(%d): %s", strategyID, err) } //buf, _ := json.MarshalIndent(stats, "", " ") //s.logger.Printf("Stats (%d):\n%s\n\n", strategyID, buf) for _, champ := range stats { key := ChampRatingKey{ ChampID: champ.ChampID, StrategyID: strategyID, } champRatings[key] = &ChampRating{ Won: champ.Won, Lost: champ.Lost, } } } } return s, nil } func (s *Daemon) ListRatedChamps(state *web.State, reply *web.Reply) (err error) { var sportID int state.Val("sportID", &sportID) s.mutex.Lock() defer s.mutex.Unlock() ratings, ok := s.champRatings[sportID] if !ok { return } str := "" type ChampAndRating struct { ChampID string StrategyID int64 Won int Lost int Accuracy float64 } var sorted []ChampAndRating for key, rating := range ratings { //if (rating.Won+rating.Lost) >= 6 && rating.GetAccuracy() >= 75 { if rating.GetAccuracy() >= 70 { sorted = append(sorted, ChampAndRating{ ChampID: key.ChampID, StrategyID: key.StrategyID, Won: rating.Won, Lost: rating.Lost, Accuracy: rating.GetAccuracy(), }) } } sort.Slice(sorted, func(i, j int) bool { a := sorted[i] b := sorted[j] if a.StrategyID == b.StrategyID { return a.Accuracy < b.Accuracy } return a.StrategyID < b.StrategyID }) for _, champ := range sorted { str += fmt.Sprintf("%s (стратегия %d):won=%d, lost=%d, accuracy=%.2f", champ.ChampID, champ.StrategyID, champ.Won, champ.Lost, champ.Accuracy) } //reply.SetHeader("Content-Type", "text/pain") reply.WriteString(`` + str + `
`) return } func (s *Daemon) TryLoadAndSetTipResults() { tips, err := s.model.ListWaitingTips() if err != nil { err = fmt.Errorf("ListWaitingTips: %s", err) return } s.logger.Println(pretty.PSprintln("Waiting Tips", tips)) // Час назад //aHourAgo := time.Now().Unix() - 3600 for _, tip := range tips { // Если матч начался менее часа назад - пропускаем. //if tip.TipTime > aHourAgo { // continue //} err = s.tryLoadAndSetTipResult(tip) if err != nil { s.logger.Printf("trySetTipResult: %s; tipID=%d", err, tip.TipID) } } } func (s *Daemon) Run() { go s.flashscoreLoader() go s.tipResultsLoader() go s.oldMatchesRemover() } func (s *Daemon) oldMatchesRemover() { interval := 30 * time.Minute ticker := time.NewTicker(interval) for { select { case <-ticker.C: s.removeOldMatches() } } } func (s *Daemon) tipResultsLoader() { interval := 20 * time.Minute ticker := time.NewTicker(interval) s.TryLoadAndSetTipResults() for { select { case <-ticker.C: s.TryLoadAndSetTipResults() } } } func (s *Daemon) flashscoreLoader() { interval := 1 * time.Hour ticker := time.NewTicker(interval) for _, sportID := range s.sportIDs { err := s.listUpcomingMatchReports(sportID) if err != nil { s.logger.Printf("listUpcomingMatchReports(sportID=%d): %s\n", sportID, err) } } s.onexbetClient.ConnectAsync() for { select { case <-ticker.C: for _, sportID := range s.sportIDs { err := s.listUpcomingMatchReports(sportID) if err != nil { s.logger.Printf("listUpcomingMatchReports(sportID=%d): %s\n", sportID, err) } } } } } func (s *Daemon) removeOldMatches() { s.mutex.Lock() defer s.mutex.Unlock() a2HoursAgo := time.Now().Unix() - 7200 for sportID, sportMatches := range s.teamMatches { var count int for _, match := range sportMatches { _, isInInplay := s.onexbetMatchToTeamMatch[match.OnexbetMatchID] if match.StartTime < a2HoursAgo && !isInInplay { // Если матч стартовал более 2 часов назад и не в Inplay - удаляем delete(sportMatches, match.MatchID) //delete(s.identifiedMatches, match.OnexbetMatchID) count++ } } // Отправляем уведомления if count > 0 { s.publisher.Publish( getChannel(sportID), WSMessageTeamMatches, s.getTeamMatchesMessage(sportID), ) } } } func (s *Daemon) listUpcomingMatchReports(sportID int) (err error) { var buf []byte err = s.fsAPIClient.Call(api.CallReq{ FuncName: "listUpcomingMatchReports", In: sportID, Out: &buf, }) if err != nil { return } var reports []flashscore.UpcomingMatchReport err = json.Unmarshal(buf, &reports) if err != nil { return } now := time.Now().Unix() - 10800 s.mutex.Lock() defer s.mutex.Unlock() sportStrategies := s.sportStrategies[sportID] fmt.Printf("Sport strategies qty: %d\n", len(sportStrategies)) teamMatches := s.teamMatches[sportID] // Сохраняем в этот словарь все matchID из reports, чтобы сделав проход по teamMatches, // обнаружить матчи, которых больше нет в reports //checkedMatchIDs := make(map[string]bool) // Синхронизация: // - матчи из teamMatches, которые по времени начались - пропускаем // - матчи из reports, которые по времени начались - пропускаем // - матчи, которых нет в teamMatches - добавляем // - матчи, которые есть в teamMatches (и еще не начались), но нет в reports - удаляем // - матчи, которые есть и там и там, но не начались - обнуляем список стратегий // и прогоняем с нуля. Если ни одной стратегии не удовлетворяем - удаляем // Первый проход по reports for _, report := range reports { //checkedMatchIDs[report.MatchID] = true if report.StartTime <= now { // матч уже начался - пропускаем s.logger.Printf("match already started: %d < now %d\n", report.StartTime, now) continue } match, ok := teamMatches[report.MatchID] if ok { if match.StartTime <= now { // матч уже начался - пропускаем s.logger.Printf("match already started: %d < now %d\n", match.StartTime, now) continue } if match.IsLinkedToOnexbet { // матч уже в линии 1xBet и распознан - пропускаем s.logger.Printf("match %s already linked\n", match.MatchID) continue } // Обнуляем стратегии чтобы пересканировать заново match.Strategies = make(map[int64]TeamStrategy) match.ParseTime = report.ParseTime match.HomeTeamMatches = report.HomeTeamMatches match.AwayTeamMatches = report.AwayTeamMatches match.H2H = report.H2H match.Odds = report.Odds match.Notes = nil } else { // Чтобы звездочками отметить матчи, которые не были ни разу распознаны home, err := s.fsmodel.GetTeam(report.Home.TeamID) if err != nil { s.logger.Printf("fsmodel.GetTeam: %s; teamID=%s", err, report.Home.TeamID) } else { report.Home.WasLinked = home.WasLinked } away, err := s.fsmodel.GetTeam(report.Away.TeamID) if err != nil { s.logger.Printf("fsmodel.GetTeam: %s; teamID=%s", err, report.Away.TeamID) } else { report.Away.WasLinked = away.WasLinked } // Добавляем новый матч match = &TeamMatch{ MatchID: report.MatchID, Home: report.Home, Away: report.Away, Zone: report.Zone, Champ: report.Champ, FullChampName: report.FullChampName, StartTime: report.StartTime, ParseTime: report.ParseTime, SportID: sportID, Strategies: make(map[int64]TeamStrategy), HomeTeamMatches: report.HomeTeamMatches, AwayTeamMatches: report.AwayTeamMatches, H2H: report.H2H, Odds: report.Odds, } teamMatches[report.MatchID] = match s.logger.Printf("match %s added\n", match.MatchID) } historyProcessor := s.historyProcessors[sportID] if historyProcessor != nil { historyProcessor(match) } for _, strategy := range sportStrategies { // Сперва проверяем не было ли уже прогнозов на этот матч и эту стратегию var hasTip bool hasTip, err = s.model.HasTip(match.MatchID, strategy.ID()) if err != nil { s.logger.Printf("model.HasTip: %s\n", err) } else { if !hasTip { notes, isInteresting := strategy.IsInteresting(match) if isInteresting { match.Strategies[strategy.ID()] = strategy match.Notes = append(match.Notes, StrategyNotes{ StrategyID: strategy.ID(), Notes: notes, }) } } } } if len(match.Strategies) == 0 { // матч никому не интересен - удаляем s.logger.Printf("match %s not interesting -> remove\n", match.MatchID) delete(teamMatches, report.MatchID) } } /* // Второй проход по teamMatches for matchID, match := range teamMatches { if checkedMatchIDs[matchID] { continue } // Матча не было в reports - какндидат на удаление if match.StartTime <= now { // матч уже начался - пропускаем continue } if match.IsLinkedToOnexbet { // матч уже в линии 1xBet и распознан - пропускаем continue } delete(teamMatches, matchID) } */ s.publisher.Publish(getChannel(sportID), WSMessageTeamMatches, s.getTeamMatchesMessage(sportID)) return } func (s *Daemon) getTeamMatchesMessage(sportID int) (msg TeamMatchesMessage) { teamsMatches, ok := s.teamMatches[sportID] if !ok { panic(fmt.Sprintf("Bug: getTeamMatchesMessage of unknown sportID %d\n", sportID)) } msg.SportID = sportID for _, match := range teamsMatches { msg.Matches = append(msg.Matches, *match) } return } func (s *Daemon) tryLoadAndSetTipResult(tip model.Tip) (err error) { if tip.OnexbetStatsMatchID == "" { err = fmt.Errorf("Empty OnexbetStatsMatchID") return } var matchResult onexbet.TeamMatchResult if tip.SportID == flashscore.Tennis { matchResult, err = onexbet.GetTennisMatchResult(tip.OnexbetStatsMatchID) if err != nil { err = fmt.Errorf("onexbet.GetTeamMatchResult: %s", err) return } } else { matchResult, err = onexbet.GetTeamMatchResult(tip.OnexbetStatsMatchID) if err != nil { err = fmt.Errorf("onexbet.GetTeamMatchResult: %s", err) return } } s.logger.Printf("%s result: %s\n", tip.OnexbetStatsMatchID, pretty.Sprint(matchResult)) return s.trySetTipResult(tip, matchResult) } func swapTeamMatchResult(r onexbet.TeamMatchResult) onexbet.TeamMatchResult { r.AwayPoints, r.HomePoints = r.HomePoints, r.AwayPoints for idx, p := range r.ScoreByPeriods { p.AwayPoints, p.HomePoints = p.HomePoints, p.AwayPoints r.ScoreByPeriods[idx] = p } return r } func (s *Daemon) trySetTipResult(tip model.Tip, matchResult onexbet.TeamMatchResult) (err error) { sportStrategies, ok := s.sportStrategies[int(tip.SportID)] if !ok { err = fmt.Errorf("No strategies found for the sportID %d", tip.SportID) return } strategy, ok := sportStrategies[tip.StrategyID] if !ok { s.logger.Printf("Unknown strategyID %d", tip.StrategyID) return } if tip.IsTeamsSwapped { matchResult = swapTeamMatchResult(matchResult) } result := strategy.GetResult(tip, matchResult) if result == nil { err = fmt.Errorf("strategy.GetResult returns nil") return } //s.logger.Printf(pretty.PSprintln) err = s.model.SetTipResult(model.SetTipResultReq{ TipID: tip.TipID, Status: result.Status, Result: result.Result, }) if err != nil { err = fmt.Errorf("model.SetTipResult: %s", err) return } s.notifyAboutTipResult(tip, result, strategy) return } func (s *Daemon) notifyAboutTipResult(tip model.Tip, result *TipResult, strategy TeamStrategy) (err error) { var outcome string switch result.Status { case model.Won: outcome = "Выигрыш" case model.Lost: outcome = "Проигрыш" case model.Void: outcome = "Возврат" } msg := fmt.Sprintf(tipResultTextPattern, tip.TipID, result.Result, outcome) // Обновляем рейтинг чемпионатов key := ChampRatingKey{ ChampID: tip.ChampID, StrategyID: tip.StrategyID, } champRatings, ok := s.champRatings[int(tip.SportID)] if ok { rating, ok := champRatings[key] if !ok { rating = &ChampRating{} champRatings[key] = rating } if result.Status == model.Won { rating.Won++ } else if result.Status == model.Lost { rating.Lost++ } } if strategy.IsNotifyInTelegram() { // дополнительная проверка на isRated. У тенниса нет рейтингов по чемпионатам, // поэтому проверка более хитрая. var ok bool switch tip.SportID { case flashscore.Tennis: ok = true case flashscore.Handball, flashscore.Football: if tip.IsRated { ok = true } } if ok { telegramChatID, hasChat := sportToTelegramChatID[int(tip.SportID)] if hasChat { err = sendMessageToTelegramGroup(telegramChatID, msg) if err != nil { s.logger.Printf("send tip result to Telegram: %s\n", err) } } else { s.logger.Printf("Sport %d hasn't telegram chat\n", tip.SportID) } } } s.publisher.Publish(getChannel(int(tip.SportID)), WSMessageTipsChanged, nil) return } func (s *Daemon) SetTipResultManual(in model.SetTipResultReq) (err error) { tip, err := s.model.GetTip(in.TipID) if err != nil { return } if tip == nil { s.logger.Printf("setTipResultManual: tipID=%d not found in db\n", in.TipID) return } switch in.Status { case 0: err = fix.Field(EmptyTipStatus, "status") return case model.Won, model.Lost, model.Void: // pass default: err = fix.Field(WrongTipStatus, "status") return } err = s.model.SetTipResult(in) if err != nil { return } // Если стратегии не найдены - сообщение в телеграм отправлено не будет sportStrategies, ok := s.sportStrategies[int(tip.SportID)] if !ok { err = fmt.Errorf("No strategies found for the sportID %d", tip.SportID) return } strategy, ok := sportStrategies[tip.StrategyID] if !ok { s.logger.Printf("Unknown strategyID %d", tip.StrategyID) return } s.notifyAboutTipResult( *tip, &TipResult{Status: in.Status, Result: in.Result}, strategy, ) return } func (s *Daemon) onWsSubscribeRequestToHandball(conn *ws.Conn, channel, arg string) { s.mutex.Lock() defer s.mutex.Unlock() s.logger.Printf("ws client subscribes to channel %q\n", channel) conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(flashscore.Handball)) s.publisher.SubscribeTo(conn, channel) } func (s *Daemon) onWsSubscribeRequestToFootball(conn *ws.Conn, channel, arg string) { s.mutex.Lock() defer s.mutex.Unlock() s.logger.Printf("ws client subscribes to channel %q\n", channel) conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(flashscore.Football)) s.publisher.SubscribeTo(conn, channel) } func (s *Daemon) onWsSubscribeRequestToTennis(conn *ws.Conn, channel, arg string) { s.mutex.Lock() defer s.mutex.Unlock() s.logger.Printf("ws client subscribes to channel %q\n", channel) conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(flashscore.Tennis)) s.publisher.SubscribeTo(conn, channel) } // func (s *Daemon) onWsSubscribeRequest(conn *ws.Conn, channel string) { // s.mutex.Lock() // defer s.mutex.Unlock() // s.logger.Printf("ws client subscribes to channel %q\n", channel) // switch channel { // case ChannelHandball: // conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(flashscore.Handball)) // case ChannelFootball: // conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(flashscore.Football)) // case ChannelTennis: // conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(flashscore.Tennis)) // } // s.publisher.SubscribeTo(conn, channel) // } func (s *Daemon) onWsClientConnected(conn *ws.Conn) { //s.mutex.Lock() //defer s.mutex.Unlock() s.logger.Printf("ws client connected\n") //for _, sportID := range s.sportIDs { // conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(sportID)) //} //s.logger.Printf("team matches sent\n") //s.publisher.SubscribeTo(conn, Channel) } func (s *Daemon) onWsClientDisconnected(conn *ws.Conn) { s.mutex.Lock() defer s.mutex.Unlock() s.logger.Printf("ws client disconnected\n") //s.publisher.UnsubscribeConn(conn) } // Регулярка для вырезания буквы W из названия команды. // Поиск на 1xBet не дает результатов если в названии есть W, например Ajax W var reW = regexp.MustCompile(`\bW\b`) type SearchMatchCandidatesIn struct { SportID int `json:"sportID"` Home string `json:"home"` Away string `json:"away"` } func (s *Daemon) SearchMatchCandidates(in SearchMatchCandidatesIn) (matches []onexbet.MatchCandidate, err error) { in.Home = reW.ReplaceAllString(in.Home, "") in.Away = reW.ReplaceAllString(in.Away, "") switch in.SportID { case flashscore.Football: return onexbet.SearchMatchCandidates(in.Home, in.Away, onexbet.Football) case flashscore.Handball: return onexbet.SearchMatchCandidates(in.Home, in.Away, onexbet.Handball) case flashscore.Tennis: return onexbet.SearchMatchCandidates(in.Home, in.Away, onexbet.Tennis) } return } type SearchMatchCandidatesByTeamNameIn struct { SportID int `json:"sportID"` TeamName string `json:"teamName"` } func (s *Daemon) SearchMatchCandidatesByTeamName(in SearchMatchCandidatesByTeamNameIn) (matches []onexbet.MatchCandidate, err error) { in.TeamName = reW.ReplaceAllString(in.TeamName, "") return onexbet.SearchTeamCandidates(in.TeamName, flashscoreSportToOnexbetSport[in.SportID]) } type AddTeamsAliasesIn struct { SportID int Home fsmodel.TeamAlias Away fsmodel.TeamAlias } func (s *Daemon) AddTeamsAliases(in AddTeamsAliasesIn) (err error) { err = s.fsmodel.AddTeamAlias(in.Home) if err != nil { apierr, ok := err.(fixme.Field) if !ok { return } if apierr.Code != fsmodel.Duplicate { return } err = nil } err = s.fsmodel.AddTeamAlias(in.Away) if err != nil { apierr, ok := err.(fixme.Field) if !ok { return } if apierr.Code != fsmodel.Duplicate { return } err = nil } s.mutex.Lock() defer s.mutex.Unlock() // Сбрасываем кэш нераспознанных 1xBet матчей s.unrecognizedOnexbetMatches[in.SportID] = make(map[string]bool) return } func (s *Daemon) ListSportTips(sportID int) (tips []model.Tip, err error) { tips, err = s.model.ListSportTips(sportID) if err != nil { return } for idx, tip := range tips { strategies, foundSport := s.sportStrategies[int(tip.SportID)] if foundSport { strategy, has := strategies[tip.StrategyID] if has { tip.Strategy = strategy.ShortName() tips[idx] = tip } } } return }