-
Notifications
You must be signed in to change notification settings - Fork 0
/
Main.hs
210 lines (182 loc) · 6.08 KB
/
Main.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Main
( main,
)
where
import qualified Data.Aeson as Aeson
import Data.Aeson ((.=))
import Network.HTTP.Types
import qualified Network.Wai as Wai
import qualified Network.WebSockets as WebSockets
import qualified Options.Applicative as Options
import RIO
import qualified RIO.ByteString.Lazy as BL
import qualified RIO.Map as Map
import Web (WebHandler, run, sourceAddress, websocketsOr, withPingThread)
data Opts
= Opts {optPort :: Int}
getOpts :: IO Opts
getOpts = Options.execParser parser
where
parser =
Options.info
( Options.helper
<*> ( Opts
<$> Options.option
Options.auto
( Options.long "port" <> Options.short 'p' <> Options.value 8787
<> Options.metavar "PORT"
<> Options.help "listen port"
)
)
)
(Options.fullDesc <> Options.progDesc "Websockets puzzle server." <> Options.header "puzzld")
type Key = Text
type Rooms = Map Key (TVar Room)
emptyRooms :: Rooms
emptyRooms = Map.empty
data App
= App
{ appLogFunc :: !LogFunc,
appLogContext :: [Utf8Builder],
appRooms :: !(MVar Rooms)
}
instance HasLogFunc App where
logFuncL = lens appLogFunc (\x y -> x {appLogFunc = y})
info :: Utf8Builder -> RIO App ()
info msg = do
logContext <- view $ to appLogContext
logInfo $ mconcat (map (\c -> c <> ": ") logContext) <> msg
debug :: Utf8Builder -> RIO App ()
debug msg = do
logContext <- view $ to appLogContext
logDebug $ mconcat (map (\c -> c <> ": ") logContext) <> msg
withContext :: [Utf8Builder] -> RIO App a -> RIO App a
withContext ctx = local (\app -> app {appLogContext = ctx})
runApp :: RIO App () -> IO ()
runApp inner = runSimpleApp $ do
logFunc <- view logFuncL
rooms <- newMVar emptyRooms
let app = App {appLogFunc = logFunc, appLogContext = [], appRooms = rooms}
runRIO app inner
getRoom :: Key -> RIO App (TVar Room)
getRoom key = do
rooms <- view $ to appRooms
modifyMVar rooms $ \rs ->
case Map.lookup key rs of
Just r -> return (rs, r)
Nothing -> do
info $ "creating new room: " <> display key
r <- atomically $ newTVar emptyRoom
return $ (Map.insert key r rs, r)
type EventId = Int
data Event
= Event
{ eventOperation :: Text
}
encodeEvent :: (EventId, Event) -> BL.ByteString
encodeEvent (eventId, event) =
Aeson.encode $
Aeson.object
["id" .= Aeson.toJSON eventId, "operation" .= Aeson.toJSON (eventOperation event)]
data Room
= Room
{ roomEvents :: [(EventId, Event)], -- newest first
roomNextEventId :: !EventId
}
emptyRoom :: Room
emptyRoom = Room
{ roomEvents = [],
roomNextEventId = 0
}
-- | Events with ID greater or equal to the given, in ascending ID order.
eventsFrom :: Room -> EventId -> [(EventId, Event)]
eventsFrom room start = reverse $ takeWhile ((>= start) . fst) (roomEvents room)
addEvent :: Event -> Room -> (Room, EventId)
addEvent event room =
let next = roomNextEventId room
in ( room
{ roomEvents = (next, event) : roomEvents room,
roomNextEventId = next + 1
},
next
)
-- | Send the current event history down a connection, starting
-- at the given event ID. Returns the ID of the first unsent (future) event.
sendHistoryFrom :: WebSockets.Connection -> Room -> EventId -> RIO App EventId
sendHistoryFrom conn room start =
liftIO $ do
mapM_
(WebSockets.sendTextData conn . encodeEvent)
(eventsFrom room start)
return $ roomNextEventId room
toplevel :: WebHandler (RIO App)
toplevel req respond = do
case Wai.pathInfo req of
[] ->
respond $
Wai.responseLBS
status200
[("Content-Type", "text/plain")]
"Hello, Web!"
["game", key] -> do
let ctx = ["room " <> display key, sourceAddress req]
room <- getRoom key
withContext ctx $ game room req respond
_ -> respond $ Wai.responseLBS status404 [] "not found"
receive :: WebSockets.Connection -> TChan Text -> RIO App ()
receive conn chan = do
msg <- liftIO $ WebSockets.receiveDataMessage conn
case msg of
m@(WebSockets.Text _ _) -> do
atomically $ writeTChan chan $ WebSockets.fromDataMessage m
receive conn chan
WebSockets.Binary _ -> do
debug "ignoring binary message"
receive conn chan
addEventT :: Event -> TVar Room -> RIO App ()
addEventT event room = do
eventId <- atomically $ do
r <- readTVar room
let (r', eventId) = addEvent event r
writeTVar room $! r'
return eventId
debug $ "received operation " <> display eventId <> ": " <> display (eventOperation event)
work :: WebSockets.Connection -> TChan Text -> TVar Room -> EventId -> RIO App ()
work conn chan room start = loop start
where
loop last = do
action <- atomically $ do
r <- readTVar room
if last /= roomNextEventId r
then return $ sendHistoryFrom conn r last
else do
msg <- readTChan chan
return $ do
let event = Event {eventOperation = msg}
addEventT event room >> return last
last' <- action
loop last'
game :: TVar Room -> WebHandler (RIO App)
game room = websocketsOr WebSockets.defaultConnectionOptions handleConn fallback
where
handleConn pendingConn = do
conn <- liftIO $ WebSockets.acceptRequest pendingConn
chan <- atomically newTChan
debug $ "accepted connection"
withPingThread conn 30 $
race_ (work conn chan room 0) (receive conn chan)
`catch` ( \e -> case e of
WebSockets.CloseRequest _ _ -> return ()
WebSockets.ConnectionClosed -> return ()
_ -> throwIO e
)
debug $ "dropped connection"
fallback _ respond = respond $ Wai.responseLBS status400 [] "not a websocket request"
main :: IO ()
main = runApp $ do
opts <- liftIO getOpts
let port = optPort opts
info $ "listening on port " <> display port
run port toplevel