Skip to content

Commit

Permalink
Chat client
Browse files Browse the repository at this point in the history
  • Loading branch information
steffansluis committed Mar 20, 2019
1 parent cd1a8cf commit d50c5c4
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 147 deletions.
19 changes: 18 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
plugins {
id 'scala'
id 'idea'
id "com.github.maiflai.scalatest" version "0.24"
}

repositories {
mavenCentral()
jcenter()
}

dependencies {
implementation 'org.scala-lang:scala-library:2.12.8'
implementation 'org.scala-lang.modules:scala-async_2.12:0.9.7'
compile 'com.monovore:decline_2.12:0.6.1'
compile 'org.typelevel:cats-core_2.12:1.5.0'
testImplementation 'org.pegdown:pegdown:1.4.2'
testImplementation 'org.scalatest:scalatest_2.12:3.0.6'
testImplementation 'junit:junit:4.12'
}

task wrapper(type: Wrapper) {
gradleVersion = '4.10'
}
}

idea{
module {
testSourceDirs += sourceSets.main.runtimeClasspath
}
}

task run(type: JavaExec, dependsOn: classes) {
main = 'chatapp.Chat'
standardInput = System.in
classpath sourceSets.main.runtimeClasspath
}
147 changes: 103 additions & 44 deletions src/main/scala/chatapp/Chat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,127 @@ package chatapp

import rover.rdo.AtomicObjectState
import rover.rdo.client.RdObject
import cats.implicits._
import com.monovore.decline._

// FIXME: ensure messages can be read, but not modified or reassigned...
// FIXME: after state & rd object impl change
class Chat(var messages: List[ChatMessage]) extends RdObject[String](AtomicObjectState.initial("")) {
import scala.concurrent.ExecutionContext.Implicits.global
import scala.async.Async.{async, await}
import scala.concurrent.{Await, Future, Promise}
import scala.concurrent.duration._

// TODO: Something with users, crypto stuff on identity
// FIXME: Hashes should be used here as well as user ids
private var users = Map[Long, String]()
class User(val username: String) {

def this(messages: List[ChatMessage], users: Map[Long, String]) {
this(messages)
this.users = users
}
}

def addUser(id: Long, uri: String): Unit = {
if (!this.users.contains(id)) this.users updated (id, uri)
// FIXME: otherwise we can also update, a user's uri
else println("Already existing user")
}
object User {
val Steffan = new User("steffan")
val Giannis = new User("giannis")
}

class ChatMessage(val body: String,
val author: User,
val timestamp: Long = java.time.Instant.now.getEpochSecond()) {

def removeUser(id : Long)= {
if(users.contains(id)) users = users - id
else println("Non-exsiting id given")
override def toString: String = {
s"${author.username}: $body"
}
}

// FIXME: ensure messages can be read, but not modified or reassigned...
// FIXME: after state & rd object impl change
class Chat(_onStateModified: Chat#Updater) extends RdObject[List[ChatMessage]](AtomicObjectState.initial(List[ChatMessage]())) {
type Updater = AtomicObjectState[List[ChatMessage]] => Promise[Unit]

def appendMessage(chatMessage: ChatMessage): Unit = {
messages = messages :+ chatMessage
def send(message: ChatMessage): Promise[Unit]= {
val op: AtomicObjectState[List[ChatMessage]]#Op = s => s :+ message

Promise() completeWith async {
modifyState(op)
}
}

def terminate(): Unit ={
//TODO: terminate
override def onStateModified(oldState: AtomicObjectState[List[ChatMessage]]): Promise[Unit] = {
_onStateModified(state)
}

def currentVersion(): Long = {
messages.size
immutableState.size
}

// override def stableVersion: Long = {
// // TODO: the stable version (last committed)
// messages.size
// }
}
object Chat {
val SIZE = 10

def client(serverAddress: String, user: User): Unit = {
val printer = (string: String) => {
val cls = s"${string.split("\n").map(c => s"${REPL.UP}${REPL.ERASE_LINE_BEFORE}${REPL.ERASE_LINE_AFTER}").mkString("")}"
// Prepend two spaces to match input indentation of "> "
val text = string.split("\n").map(line => s" $line").mkString("\n")

def printMessages(): Unit = {
for (i <- messages) {
println(s"body: ${i.body}, author: ${i.author}, time: ${i.timestamp}")
s"${REPL.SAVE_CURSOR}$cls\r$text${REPL.RESTORE_CURSOR}"
}
}
}

class ChatMessage(val body: String,
val author: String,
val timestamp: Long = java.time.Instant.now.getEpochSecond())
{
// TODO: This is hacky, figure out a better way to do this
val updater: Chat#Updater = state => Promise() completeWith async {
val text = state.immutableState.takeRight(SIZE).map(m => m.toString()).mkString("\n")
print(s"${printer(text)}")
}

}
val chat = new Chat(updater)
println(" Welcome to Rover Chat!")
print((1 to SIZE).map(i => "\n").mkString(""))

// Simulate conversation
async {
Thread.sleep(3000)
await(chat.send(new ChatMessage("Hey man!", User.Giannis)).future)
updater(chat.state)

Thread.sleep(3000)
await(chat.send(new ChatMessage("How's it going?", User.Giannis)).future)
updater(chat.state)

Thread.sleep(10000)
await(chat.send(new ChatMessage("Yea man I'm good", User.Giannis)).future)
updater(chat.state)
}

val reader = () => {
print("> ")
val s = scala.io.StdIn.readLine()
s
}
val executor = (input: String) => {
async {
val p = chat.send(new ChatMessage(input, user))
await(p.future)
// This clears the input line
print(s"${REPL.UP}${REPL.ERASE_LINE_AFTER}")
chat.immutableState.takeRight(SIZE).map(m => s"${m.toString()}").mkString("\n")
}
}
val repl: REPL[String] = new REPL(reader, executor, printer)
Await.result(repl.loop(), Duration.Inf)
}

object chatFoo{
def main(args: Array[String]): Unit ={
val chat = new Chat(messages = List[ChatMessage]())
chat.addUser(1234, "tt")
val m: ChatMessage = new ChatMessage("test message", "Ioa")
chat.appendMessage(m)
chat.printMessages()
def main(args: Array[String]): Unit = {
client("bla", User.Steffan)
}
}

// TODO: Add client and server subcommands
object ChatCLI extends CommandApp(
name = "rover-chat",
header = "Says hello!",
main = {
val userOpt =
Opts.option[String]("target", help = "Person to greet.") .withDefault("world")

val quietOpt = Opts.flag("quiet", help = "Whether to be quiet.").orFalse

(userOpt, quietOpt).mapN { (user, quiet) =>
if (quiet) println("...")
else println(s"Hello $user!")
}
}
)
66 changes: 66 additions & 0 deletions src/main/scala/chatapp/REPL.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package chatapp

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{Await, Future, Promise}
import scala.async.Async.{async, await}


class REPL[A](reader: REPL[A]#Reader, executor: REPL[A]#Executor, printer: REPL[A]#Printer) {
type Reader = () => String
type Executor = String => Future[A]
type Printer = A => String

def loop(): Future[Unit] = {
var looping = true
async {
while(looping) {
val input = reader()

if (input == null || input == "/exit") {
looping = false
} else if(input == "") {
print(s"${REPL.UP}${REPL.ERASE_LINE_AFTER}\r")
} else {
val result = await(executor(input))
val output = printer(result)

print(output)
}
}
}
}

}

object REPL {
val ERASE_SCREEN_AFTER="\u001b[0J"
val ERASE_LINE_BEFORE="\u001b[1K"
val ERASE_LINE_AFTER="\u001b[0K"

val HOME="\u001b[1H"
val UP="\u001b[1A"
val DOWN="\u001b[1B"
val FORWARD="\u001b[1C"
val BACKWARD="\u001b[1D"

val SAVE_CURSOR="\u001b[0s"
val RESTORE_CURSOR="\u001b[0u"

def main(args: Array[String]): Unit = {
val reader = () => {
print(s"${REPL.ERASE_LINE_BEFORE}${REPL.ERASE_SCREEN_AFTER}\r> ")
scala.io.StdIn.readLine()
}
val executor = (input: String) => {
async { input }
}
val printer = (string: String) => {
val cls = s"${string.split("\n").map(c => UP).mkString("")}$ERASE_LINE_BEFORE$ERASE_SCREEN_AFTER\r"
// Prepend two spaces to match input indentation of "> "
val text = string.split("\n").map(line => s" $line").mkString("\n")
s"$cls$text\n"
}
val repl = new REPL(reader, executor, printer)
repl.loop()
}
}
16 changes: 13 additions & 3 deletions src/main/scala/rover/rdo/client/RdObject.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ package rover.rdo.client

import rover.rdo.AtomicObjectState

import scala.concurrent.ExecutionContext.Implicits.global
import scala.async.Async.{async}
import scala.concurrent.{Promise}

//FIXME: use hashes instead of Longs/Strings?
class RdObject[A](var state: AtomicObjectState[A]) {
abstract class RdObject[A](var state: AtomicObjectState[A]) {

// TODO: "is up to date" or "version" methods

protected final def modifyState(op: AtomicObjectState[A]#Op): Unit = {
state = state.applyOp(op)
// onStateModified(state)
}

protected def onStateModified(oldState: AtomicObjectState[A]): Promise[Unit]

protected final def immutableState: A = {
return state.immutableState
}
Expand All @@ -29,6 +36,9 @@ class RdObject[A](var state: AtomicObjectState[A]) {
* @param other Some other RDO
*/
class CommonAncestor[A](private val one: RdObject[A], private val other: RdObject[A]) extends RdObject[A](null) { // todo: fixme with a deferred state
override def onStateModified(oldState: AtomicObjectState[A]): Promise[Unit] = {
Promise() completeWith async { }
}

// determine it once and defer all RdObject methods to it
def commonAncestor: RdObject[A] = {
Expand All @@ -45,8 +55,8 @@ class CommonAncestor[A](private val one: RdObject[A], private val other: RdObjec
val indexOfI = one.state.log.asList.indexOf(i)
val logRecordsUpToI = one.state.log.asList.slice(0, indexOfI+1)
val logUpToI = new Log[A](logRecordsUpToI)
val ancestor = new RdObject[A](AtomicObjectState.fromLog(logUpToI))
return ancestor
// val ancestor = new RdObject[A](AtomicObjectState.fromLog(logUpToI))
// return ancestor
}
}
}
Expand Down
Loading

0 comments on commit d50c5c4

Please sign in to comment.