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