Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Obsolete] Introduce Commenting Feature and Simultaneous Coding Environment. #551

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5a77d20
allows reorganizing of files and folders
parvmor Jun 27, 2017
c1155bd
reorganizing for funblocks
parvmor Jun 27, 2017
26f2990
Merge remote-tracking branch 'upstream/master'
parvmor Jul 1, 2017
282accf
constraints user to create new files before starting to code
parvmor Jul 6, 2017
5559b03
added some handlers for comments
parvmor Jul 8, 2017
42b87ff
Added a basic frontend for feedback
parvmor Jul 14, 2017
16d4cb4
Merge remote-tracking branch 'upstream/master'
parvmor Jul 14, 2017
cc4c713
Rearranges the code to make it more readable. Fixes the warnings of G…
parvmor Jul 15, 2017
32f1244
modifies Folder.hs according to requirements of Comment.hs
parvmor Jul 15, 2017
a364e48
Changes the comment handlers to support versioning of code. frontend …
parvmor Jul 21, 2017
220c182
complete copy dir handler
parvmor Jul 22, 2017
f6dc724
Fixes type mismatches
parvmor Jul 24, 2017
3583656
improves copyHandler and implements moveHandler
parvmor Jul 25, 2017
ad50f2e
completes save handler
parvmor Aug 14, 2017
26c608d
adds a copy feature to frontend
parvmor Aug 15, 2017
01b433c
updates askForFeedback() according to latest backend
parvmor Aug 16, 2017
c26cd61
update frontend to accomodate versions
parvmor Aug 21, 2017
0a07520
remove save as functionality
parvmor Aug 25, 2017
9c27f2c
Make frontend completely compatile to backend
parvmor Aug 27, 2017
f28626b
remove lazy input output
parvmor Aug 28, 2017
1bc2c93
add collaborate file structure
parvmor Sep 2, 2017
923067f
improve linting and correct collaborate file structure
parvmor Sep 4, 2017
0e9d33e
add frontend for simultaneous coding
parvmor Sep 4, 2017
595b6b8
add collaboration handlers
parvmor Sep 5, 2017
2c9cb97
Merge remote-tracking branch 'upstream/master'
parvmor Sep 7, 2017
fd7206e
make a seperate library for funblocks-server due to conflicting handl…
parvmor Sep 7, 2017
b722062
clean the hiding functions hack, improve pattern matching
parvmor Sep 11, 2017
139d75a
replace frequent param extraction type with a data type instead of int
parvmor Sep 11, 2017
bf12126
update operational-transformation install to be dynamic
parvmor Sep 11, 2017
03fd99f
add a separate server for operational transformation
parvmor Sep 12, 2017
c504e04
add cross-origin-resource-sharing in collabServer
parvmor Sep 12, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ run codeworld-api cabal haddock --hoogle

# Build codeworld-server from this project.

run . cabal_install ./codeworld-server \
run . cabal_install ./funblocks-server \
./codeworld-error-sanitizer \
./codeworld-compiler \
./codeworld-server \
./codeworld-game-api \
./codeworld-prediction \
./codeworld-api \
./codeworld-game-server
./codeworld-collab-server

# Build the JavaScript client code for FunBlocks, the block-based UI.
run . cabal_install --ghcjs ./funblocks-client
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,34 @@ Build-type: Simple
Extra-source-files: ChangeLog.md
Cabal-version: >=1.10

Executable codeworld-game-server
Executable codeworld-collab-server
Main-is: Main.hs
Other-modules: CodeWorld.GameServer
Build-depends: base >=4.8 && <4.10,
aeson,
directory,
engine-io,
engine-io-snap,
filepath,
hashable,
http-conduit,
mtl,
ot,
text,
websockets == 0.9.*,
websockets-snap == 0.10.*,
snap-core == 1.0.*,
snap-cors,
snap-server == 1.0.*,
socket-io,
stm,
transformers,
bytestring,
random,
unordered-containers,
time,
codeworld-game-api
codeworld-game-api,
codeworld-server
Hs-source-dirs: src
Default-language: Haskell2010
Ghc-options: -threaded -rtsopts "-with-rtsopts=-N"
Expand Down
File renamed without changes.
59 changes: 59 additions & 0 deletions codeworld-collab-server/src/CodeWorld/CollabModel.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

{-
Copyright 2017 The CodeWorld Authors. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-}

module CodeWorld.CollabModel where

import qualified Control.Concurrent.STM as STM
import Control.OperationalTransformation.Selection (Selection)
import Control.OperationalTransformation.Server (ServerState)
import Control.OperationalTransformation.Text (TextOperation)
import Data.Aeson
import GHC.Generics (Generic)
import Data.Hashable (Hashable)
import qualified Data.HashMap.Strict as HM
import Data.Text (Text)
import Data.Time.Clock (UTCTime)

data CollabServerState = CollabServerState
{ collabProjects :: STM.TVar CollabProjects
, started :: UTCTime
}

type CollabProjects = HM.HashMap CollabId (STM.TVar CollabProject)

data CollabProject = CollabProject
{ totalUsers :: !Int
, collabKey :: CollabId
, collabState :: ServerState Text TextOperation
, users :: [CollabUserState]
}

data CollabUserState = CollabUserState
{ suserId :: !Text
, suserIdent :: !Text
, userSelection :: !Selection
}

instance ToJSON CollabUserState where
toJSON (CollabUserState _ userIdent' sel) =
object $ [ "name" .= userIdent' ] ++ (if sel == mempty then [] else [ "selection" .= sel ])

newtype CollabId = CollabId { unCollabId :: Text } deriving (Eq, Generic)

instance Hashable CollabId
209 changes: 209 additions & 0 deletions codeworld-collab-server/src/CodeWorld/CollabServer.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

{-
Copyright 2017 The CodeWorld Authors. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-}

module CodeWorld.CollabServer
( initCollabServer
, collabServer
) where

import qualified Control.Concurrent.STM as STM
import Control.Monad (when)
import Control.Monad.State.Strict (StateT)
import Control.Monad.Trans
import Control.Monad.Trans.Reader (ReaderT)
import qualified Control.OperationalTransformation.Selection as Sel
import qualified Control.OperationalTransformation.Server as OTS
import Data.Aeson
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import qualified Data.HashMap.Strict as HM
import Data.Maybe (fromJust)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Time.Clock
import DataUtil
import Model
import Network.HTTP.Conduit (simpleHttp)
import qualified Network.SocketIO as SIO
import Snap.Core
import SnapUtil
import System.Directory
import System.FilePath

import CodeWorld.CollabModel

-- Initialize Collab Server

initCollabServer :: IO CollabServerState
initCollabServer = do
started <- getCurrentTime
collabProjects <- STM.newTVarIO HM.empty
return CollabServerState {..}

-- Collaboration requests helpers

getRequestParams :: ClientId -> Snap (User, FilePath)
getRequestParams clientId = do
user <- getUser clientId
mode <- getBuildMode
Just path' <- fmap (splitDirectories . BC.unpack) <$> getParam "path"
Just name <- getParam "name"
let projectId = nameToProjectId $ T.decodeUtf8 name
finalDir = joinPath $ map (dirBase . nameToDirId . T.pack) path'
file = userProjectDir mode (userId user) </> finalDir </> projectFile projectId
case (length path', path' !! 0) of
(0, _) -> return (user, file)
(_, x) | x /= "commentables" -> return (user, file)

initCollaborationHandler :: CollabServerState -> ClientId -> Snap (Text, Text, CollabId)
initCollaborationHandler state clientId = do
(user, filePath) <- getRequestParams clientId
collabHashPath <- liftIO $ BC.unpack <$> B.readFile filePath
let collabHash = take (length collabHashPath - 3) . takeFileName $ collabHashPath
Just (currentUsers :: [UserDump]) <- liftIO $ decodeStrict <$>
B.readFile (collabHashPath <.> "users")
let userIdent' = uuserIdent $ (filter (\x -> uuserId x == userId user) currentUsers) !! 0
Just (project :: Project) <- liftIO $ decodeStrict <$>
B.readFile collabHashPath
liftIO $ addNewCollaborator state (userId user) userIdent' (projectSource project) $
CollabId . T.pack $ collabHash
return ((userId user), userIdent', CollabId . T.pack $ collabHash)

getCollabProject :: CollabServerState -> CollabId -> STM.STM (STM.TVar CollabProject)
getCollabProject state collabHash = do
fromJust . HM.lookup collabHash <$> STM.readTVar (collabProjects state)

addNewCollaborator :: CollabServerState -> Text -> Text -> Text -> CollabId -> IO ()
addNewCollaborator state userId' userIdent' projectSource collabHash = do
let collabUser = CollabUserState userId' userIdent' mempty
STM.atomically $ do
hm <- STM.readTVar $ collabProjects state
case HM.lookup collabHash hm of
Just collabProjectTV -> do
collabProject <- STM.readTVar collabProjectTV
case userId' `elem` (map suserId $ users collabProject) of
True -> do
let collabProject' = collabProject
{ users = map (\x -> if suserId x == userId'
then collabUser
else x) $ users collabProject
}
collabProjectTV' <- STM.newTVar collabProject'
STM.modifyTVar (collabProjects state) $
\x -> HM.adjust (\_ -> collabProjectTV') collabHash x
False -> do
let collabProject' = collabProject
{ totalUsers = totalUsers collabProject + 1
, users = collabUser : users collabProject
}
collabProjectTV' <- STM.newTVar collabProject'
STM.modifyTVar (collabProjects state) $
\x -> HM.adjust (\_ -> collabProjectTV') collabHash x
Nothing -> do
let collabProject = CollabProject
{ totalUsers = 1
, collabKey = collabHash
, collabState = OTS.initialServerState projectSource
, users = [collabUser]
}
collabProjectTV <- STM.newTVar collabProject
STM.modifyTVar (collabProjects state) $
\x -> HM.insert collabHash collabProjectTV x

cleanUp :: CollabServerState -> Text -> STM.TVar CollabProject -> STM.STM ()
cleanUp state userId' collabProjectTV = do
collabProject <- STM.readTVar collabProjectTV
case null (filter ((/= userId') . suserId) $ users collabProject) of
True -> do
STM.modifyTVar collabProjectTV (\collabProject' -> collabProject'
{ totalUsers = 0
, users = []
})
let collabHash = collabKey collabProject
STM.modifyTVar (collabProjects state) $ HM.delete collabHash
False -> do
STM.modifyTVar collabProjectTV (\collabProject' -> collabProject'
{ totalUsers = totalUsers collabProject' - 1
, users = filter ((/= userId') . suserId) $
users collabProject'
})

-- Collaboration requests handler

collabServer :: CollabServerState -> ClientId -> StateT SIO.RoutingTable (ReaderT SIO.Socket Snap) ()
collabServer state clientId = do
(userId', userIdent', collabHash) <- liftSnap $ initCollaborationHandler state clientId
let userHash = hashToId "U" . BC.pack $ (show userId') ++ (show . unCollabId $ collabHash)
SIO.broadcastJSON "set_name" [toJSON userHash, toJSON userIdent']
SIO.broadcast "add_user" userIdent'
SIO.emitJSON "logged_in" []
currentUsers' <- liftIO . STM.atomically $ do
collabProjectTV <- getCollabProject state collabHash
(\x -> map suserIdent $ users x) <$> STM.readTVar collabProjectTV
collabProjectTV' <- liftIO . STM.atomically $ getCollabProject state collabHash
OTS.ServerState rev' doc _ <- liftIO $ collabState <$> STM.readTVarIO collabProjectTV'
SIO.emit "doc" $ object
[ "str" .= doc
, "revision" .= rev'
, "clients" .= currentUsers'
]

SIO.on "operation" $ \rev op (sel :: Sel.Selection) -> do
res <- liftIO . STM.atomically $ do
collabProjectTV <- getCollabProject state collabHash
serverState <- collabState <$> STM.readTVar collabProjectTV
case OTS.applyOperation serverState rev op sel of
Left err -> return $ Left err
Right (op', sel', serverState') -> do
STM.modifyTVar collabProjectTV (\collabProject ->
collabProject { collabState = serverState' })
STM.modifyTVar (collabProjects state) $
\x -> HM.adjust (\_ -> collabProjectTV) collabHash x
return $ Right (op', sel')
case res of
Left _ -> return ()
Right (op', sel') -> do
SIO.emitJSON "ack" []
SIO.broadcastJSON "operation" [toJSON userHash, toJSON op', toJSON sel']

SIO.on "selection" $ \sel -> do
liftIO . STM.atomically $ do
collabProjectTV <- getCollabProject state collabHash
currentUsers <- users <$> STM.readTVar collabProjectTV
let currentUsers'' = map (\x -> if ((/= userId') . suserId) x
then x
else x{ userSelection = sel }) currentUsers
STM.modifyTVar collabProjectTV (\collabProject ->
collabProject { users = currentUsers'' })
STM.modifyTVar (collabProjects state) $
\x -> HM.adjust (\_ -> collabProjectTV) collabHash x
SIO.broadcastJSON "selection" [toJSON userHash, toJSON sel]

SIO.appendDisconnectHandler $ do
liftIO . STM.atomically $ do
collabProjectTV <- getCollabProject state collabHash
cleanUp state userId' collabProjectTV
SIO.broadcast "client_left" userHash
SIO.broadcast "remove_user" userIdent'
60 changes: 60 additions & 0 deletions codeworld-collab-server/src/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{-# LANGUAGE OverloadedStrings #-}

{-
Copyright 2017 The CodeWorld Authors. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-}

import Control.Applicative ((<|>))
import Control.Monad (unless)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Network.SocketIO as SIO
import Network.EngineIO.Snap (snapAPI)
import Snap.Core
import qualified Snap.CORS as CORS
import Snap.Http.Server
import System.Directory

import CodeWorld.GameServer
import CodeWorld.CollabServer
import SnapUtil

main :: IO ()
main = do
hasClientId <- doesFileExist "web/clientId.txt"
unless hasClientId $ do
putStrLn "WARNING: Missing web/clientId.txt"
putStrLn "User logins will not function properly!"

clientId <- case hasClientId of
True -> do
txt <- T.readFile "web/clientId.txt"
return . ClientId . Just . T.strip $ txt
False -> do
return $ ClientId Nothing

gameServerState <- initGameServer
collabServerState <- initCollabServer
socketIOHandler <- SIO.initialize snapAPI (collabServer collabServerState clientId)
config <- commandLineConfig $
setPort 9160 $
setErrorLog (ConfigFileLog "log/collab-error.log") $
setAccessLog (ConfigFileLog "log/collab-access.log") $
mempty
httpServe config $ CORS.applyCORS CORS.defaultOptions $
ifTop (gameStats gameServerState) <|>
route [ ("gameserver", gameServer gameServerState)
, ("socket.io" , socketIOHandler)
]