...

Source file src/code.rocket9labs.com/tslocum/bgammon/pkg/server/server_web.go

Documentation: code.rocket9labs.com/tslocum/bgammon/pkg/server

     1  package server
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"log"
     7  	"net/http"
     8  	"strconv"
     9  	"time"
    10  
    11  	"code.rocket9labs.com/tslocum/bgammon"
    12  	"github.com/gorilla/mux"
    13  )
    14  
    15  func (s *server) listenWebSocket(address string) {
    16  	log.Printf("Listening for WebSocket connections on %s...", address)
    17  
    18  	m := mux.NewRouter()
    19  	m.HandleFunc("/reset/{id:[0-9]+}/{key:[A-Za-z0-9]+}", s.handleResetPassword)
    20  	m.HandleFunc("/match/{id:[0-9]+}", s.handleMatch)
    21  	m.HandleFunc("/matches", s.handleListMatches)
    22  	m.HandleFunc("/leaderboard-casual-backgammon-single", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantBackgammon, false))
    23  	m.HandleFunc("/leaderboard-casual-backgammon-multi", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantBackgammon, true))
    24  	m.HandleFunc("/leaderboard-casual-acey-single", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantAceyDeucey, false))
    25  	m.HandleFunc("/leaderboard-casual-acey-multi", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantAceyDeucey, true))
    26  	m.HandleFunc("/leaderboard-casual-tabula-single", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantTabula, false))
    27  	m.HandleFunc("/leaderboard-casual-tabula-multi", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantTabula, true))
    28  	m.HandleFunc("/leaderboard-rated-backgammon-single", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantBackgammon, false))
    29  	m.HandleFunc("/leaderboard-rated-backgammon-multi", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantBackgammon, true))
    30  	m.HandleFunc("/leaderboard-rated-acey-single", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantAceyDeucey, false))
    31  	m.HandleFunc("/leaderboard-rated-acey-multi", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantAceyDeucey, true))
    32  	m.HandleFunc("/leaderboard-rated-tabula-single", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantTabula, false))
    33  	m.HandleFunc("/leaderboard-rated-tabula-multi", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantTabula, true))
    34  	m.HandleFunc("/stats", s.handlePrintDailyStats)
    35  	m.HandleFunc("/stats-total", s.handlePrintCumulativeStats)
    36  	m.HandleFunc("/stats-tabula", s.handlePrintTabulaStats)
    37  	m.HandleFunc("/stats-wildbg", s.handlePrintWildBGStats)
    38  	m.HandleFunc("/", s.handleWebSocket)
    39  
    40  	err := http.ListenAndServe(address, m)
    41  	log.Fatalf("failed to listen on %s: %s", address, err)
    42  }
    43  
    44  func (s *server) cachedMatches() []byte {
    45  	s.gamesCacheLock.Lock()
    46  	defer s.gamesCacheLock.Unlock()
    47  
    48  	if time.Since(s.gamesCacheTime) < 5*time.Second {
    49  		return s.gamesCache
    50  	}
    51  
    52  	s.gamesLock.Lock()
    53  	defer s.gamesLock.Unlock()
    54  
    55  	var games []*bgammon.GameListing
    56  	for _, g := range s.games {
    57  		listing := g.listing(nil)
    58  		if listing == nil || listing.Password || listing.Players == 2 {
    59  			continue
    60  		}
    61  		games = append(games, listing)
    62  	}
    63  
    64  	s.gamesCacheTime = time.Now()
    65  	if len(games) == 0 {
    66  		s.gamesCache = []byte("[]")
    67  		return s.gamesCache
    68  	}
    69  	var err error
    70  	s.gamesCache, err = json.Marshal(games)
    71  	if err != nil {
    72  		log.Fatalf("failed to marshal %+v: %s", games, err)
    73  	}
    74  	return s.gamesCache
    75  }
    76  
    77  func (s *server) cachedLeaderboard(matchType int, variant int8, multiPoint bool) []byte {
    78  	s.leaderboardCacheLock.Lock()
    79  	defer s.leaderboardCacheLock.Unlock()
    80  
    81  	var i int
    82  	switch matchType {
    83  	case matchTypeCasual:
    84  		if multiPoint {
    85  			i = 1
    86  		}
    87  	case matchTypeRated:
    88  		if !multiPoint {
    89  			i = 2
    90  		} else {
    91  			i = 3
    92  		}
    93  	}
    94  	switch variant {
    95  	case bgammon.VariantAceyDeucey:
    96  		i += 4
    97  	case bgammon.VariantTabula:
    98  		i += 8
    99  	}
   100  
   101  	if time.Since(s.leaderboardCacheTime) < 5*time.Minute {
   102  		return s.leaderboardCache[i]
   103  	}
   104  	s.leaderboardCacheTime = time.Now()
   105  
   106  	for j := 0; j < 3; j++ {
   107  		i := 0
   108  		var v int8
   109  		if j == 1 {
   110  			i = 4
   111  			v = bgammon.VariantAceyDeucey
   112  		} else if j == 2 {
   113  			i = 8
   114  			v = bgammon.VariantTabula
   115  		}
   116  		result, err := getLeaderboard(matchTypeCasual, v, false)
   117  		if err != nil {
   118  			log.Fatalf("failed to get leaderboard: %s", err)
   119  		}
   120  		s.leaderboardCache[i], err = json.Marshal(result)
   121  		if err != nil {
   122  			log.Fatalf("failed to marshal %+v: %s", result, err)
   123  		}
   124  
   125  		result, err = getLeaderboard(matchTypeCasual, v, true)
   126  		if err != nil {
   127  			log.Fatalf("failed to get leaderboard: %s", err)
   128  		}
   129  		s.leaderboardCache[i+1], err = json.Marshal(result)
   130  		if err != nil {
   131  			log.Fatalf("failed to marshal %+v: %s", result, err)
   132  		}
   133  
   134  		result, err = getLeaderboard(matchTypeRated, v, false)
   135  		if err != nil {
   136  			log.Fatalf("failed to get leaderboard: %s", err)
   137  		}
   138  		s.leaderboardCache[i+2], err = json.Marshal(result)
   139  		if err != nil {
   140  			log.Fatalf("failed to marshal %+v: %s", result, err)
   141  		}
   142  
   143  		result, err = getLeaderboard(matchTypeRated, v, true)
   144  		if err != nil {
   145  			log.Fatalf("failed to get leaderboard: %s", err)
   146  		}
   147  		s.leaderboardCache[i+3], err = json.Marshal(result)
   148  		if err != nil {
   149  			log.Fatalf("failed to marshal %+v: %s", result, err)
   150  		}
   151  	}
   152  
   153  	return s.leaderboardCache[i]
   154  }
   155  
   156  func (s *server) cachedStats(statsType int) []byte {
   157  	s.statsCacheLock.Lock()
   158  	defer s.statsCacheLock.Unlock()
   159  
   160  	if time.Since(s.statsCacheTime) < 5*time.Minute {
   161  		return s.statsCache[statsType]
   162  	}
   163  	s.statsCacheTime = time.Now()
   164  
   165  	{
   166  		stats, err := dailyStats(s.tz)
   167  		if err != nil {
   168  			log.Fatalf("failed to fetch server statistics: %s", err)
   169  		}
   170  		s.statsCache[0], err = json.Marshal(stats)
   171  		if err != nil {
   172  			log.Fatalf("failed to marshal %+v: %s", stats, err)
   173  		}
   174  
   175  		stats, err = cumulativeStats(s.tz)
   176  		if err != nil {
   177  			log.Fatalf("failed to fetch server statistics: %s", err)
   178  		}
   179  		s.statsCache[1], err = json.Marshal(stats)
   180  		if err != nil {
   181  			log.Fatalf("failed to fetch serialize server statistics: %s", err)
   182  		}
   183  	}
   184  
   185  	{
   186  		stats, err := botStats("BOT_tabula", s.tz)
   187  		if err != nil {
   188  			log.Fatalf("failed to fetch tabula statistics: %s", err)
   189  		}
   190  		s.statsCache[2], err = json.Marshal(stats)
   191  		if err != nil {
   192  			log.Fatalf("failed to fetch serialize tabula statistics: %s", err)
   193  		}
   194  
   195  		stats, err = botStats("BOT_wildbg", s.tz)
   196  		if err != nil {
   197  			log.Fatalf("failed to fetch wildbg statistics: %s", err)
   198  		}
   199  		s.statsCache[3], err = json.Marshal(stats)
   200  		if err != nil {
   201  			log.Fatalf("failed to fetch serialize wildbg statistics: %s", err)
   202  		}
   203  	}
   204  
   205  	return s.statsCache[statsType]
   206  }
   207  
   208  func (s *server) handleResetPassword(w http.ResponseWriter, r *http.Request) {
   209  	vars := mux.Vars(r)
   210  	id, err := strconv.Atoi(vars["id"])
   211  	if err != nil || id <= 0 {
   212  		return
   213  	}
   214  	key := vars["key"]
   215  
   216  	newPassword, err := confirmResetAccount(s.resetSalt, s.passwordSalt, id, key)
   217  	if err != nil {
   218  		log.Printf("failed to reset password: %s", err)
   219  	}
   220  
   221  	w.Header().Set("Content-Type", "text/html")
   222  	if err != nil || newPassword == "" {
   223  		w.Write([]byte(`<!DOCTYPE html><html><body><h1>Invalid or expired password reset link.</h1></body></html>`))
   224  		return
   225  	}
   226  	w.Write([]byte(`<!DOCTYPE html><html><body><h1>Your bgammon.org password has been reset.</h1>Your new password is <b>` + newPassword + `</b></body></html>`))
   227  }
   228  
   229  func (s *server) handleMatch(w http.ResponseWriter, r *http.Request) {
   230  	vars := mux.Vars(r)
   231  	id, err := strconv.Atoi(vars["id"])
   232  	if err != nil || id <= 0 {
   233  		return
   234  	}
   235  
   236  	timestamp, player1, player2, replay, err := matchInfo(id)
   237  	if err != nil || len(replay) == 0 {
   238  		log.Printf("failed to retrieve match: %s", err)
   239  		return
   240  	}
   241  
   242  	w.Header().Set("Content-Type", "text/plain")
   243  	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%d_%s_%s.match"`, timestamp, player1, player2))
   244  	w.Write(replay)
   245  }
   246  
   247  func (s *server) handleListMatches(w http.ResponseWriter, r *http.Request) {
   248  	w.Header().Set("Content-Type", "application/json")
   249  	w.Write(s.cachedMatches())
   250  }
   251  
   252  func (s *server) handleLeaderboardFunc(matchType int, variant int8, multiPoint bool) func(w http.ResponseWriter, r *http.Request) {
   253  	return func(w http.ResponseWriter, r *http.Request) {
   254  		w.Header().Set("Content-Type", "application/json")
   255  		w.Write(s.cachedLeaderboard(matchType, variant, multiPoint))
   256  	}
   257  }
   258  
   259  func (s *server) handlePrintDailyStats(w http.ResponseWriter, r *http.Request) {
   260  	w.Header().Set("Content-Type", "application/json")
   261  	w.Write(s.cachedStats(0))
   262  }
   263  
   264  func (s *server) handlePrintCumulativeStats(w http.ResponseWriter, r *http.Request) {
   265  	w.Header().Set("Content-Type", "application/json")
   266  	w.Write(s.cachedStats(1))
   267  }
   268  
   269  func (s *server) handlePrintTabulaStats(w http.ResponseWriter, r *http.Request) {
   270  	w.Header().Set("Content-Type", "application/json")
   271  	w.Write(s.cachedStats(2))
   272  }
   273  
   274  func (s *server) handlePrintWildBGStats(w http.ResponseWriter, r *http.Request) {
   275  	w.Header().Set("Content-Type", "application/json")
   276  	w.Write(s.cachedStats(3))
   277  }
   278  

View as plain text