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

[Do not merge] JSON and OAuth2 #12

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,19 +1,67 @@
plugins {
id 'scala'
id 'idea'
id "com.github.maiflai.scalatest" version "0.24"
}

repositories {
mavenCentral()
jcenter()
}
configurations {
scalaCompilerPlugin
}

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'
compile group: 'com.criteo.lolhttp', name: 'lolhttp_2.12', version: '10.0.0'
compile group: 'com.criteo.lolhttp', name: 'lolhtml_2.12', version: '10.0.0'
compile group: 'com.criteo.lolhttp', name: 'loljson_2.12', version: '10.0.0'
compile group: 'io.circe', name: 'circe-core_2.12', version: '0.11.1'
compile group: 'io.circe', name: 'circe-generic_2.12', version: '0.11.1'
compile group: 'io.circe', name: 'circe-optics_2.12', version: '0.11.0'
compile group: 'com.nulab-inc', name: 'scala-oauth2-core_2.12', version: '1.3.0'
testImplementation 'org.pegdown:pegdown:1.4.2'
testImplementation 'org.scalatest:scalatest_2.12:3.0.6'
testImplementation 'junit:junit:4.12'

scalaCompilerPlugin "org.scalamacros:paradise_2.12.8:2.1.0"
}

tasks.withType(ScalaCompile) {
scalaCompileOptions.additionalParameters = [
"-Xplugin:" + configurations.scalaCompilerPlugin.asPath
]
}

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

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

task run(type: JavaExec, dependsOn: classes) {
main = 'chatapp.ChatClient'
standardInput = System.in
classpath sourceSets.main.runtimeClasspath
}

compileJava {
options.compilerArgs += [
"--add-modules", "java.se",
"--add-exports", "java.base/jdk.internal.ref=ALL-UNNAMED",
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.base/java.nio=ALL-UNNAMED",
"--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED",
"--add-opens", "java.management/sun.management=ALL-UNNAMED",
"--add-opens", "jdk.management/com.sun.management.internal=ALL-UNNAMED"
]
}

25 changes: 25 additions & 0 deletions src/main/scala/chatapp/CLI.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package chatapp

import cats.implicits._
import com.monovore.decline._

class ChatCLI {

}

// 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!")
}
}
)
69 changes: 21 additions & 48 deletions src/main/scala/chatapp/Chat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,67 +3,40 @@ package chatapp
import rover.rdo.AtomicObjectState
import rover.rdo.client.RdObject

// 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}
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]()

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")
}
// FIXME: ensure messages can be read, but not modified or reassigned...
// FIXME: after state & rd object impl change
class Chat(_onStateModified: Chat#Updater, _initialState: AtomicObjectState[List[ChatMessage]]) extends RdObject[List[ChatMessage]](_initialState) {
// type State = List[ChatMessage]
type Updater = AtomicObjectState[List[ChatMessage]] => Future[Unit]

def removeUser(id : Long)= {
if(users.contains(id)) users = users - id
else println("Non-exsiting id given")
}
def send(message: ChatMessage): Future[Unit]= {
val op: AtomicObjectState[List[ChatMessage]]#Op = s => s :+ message

def appendMessage(chatMessage: ChatMessage): Unit = {
messages = messages :+ chatMessage
async {
modifyState(op)
}
}

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

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

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

def printMessages(): Unit = {
for (i <- messages) {
println(s"body: ${i.body}, author: ${i.author}, time: ${i.timestamp}")
}
object Chat {
def fromRDO(rdo: RdObject[List[ChatMessage]], _onStateModified: Chat#Updater): Chat = {
new Chat(_onStateModified, rdo.state)
}
}

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

}

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()
}
}
140 changes: 140 additions & 0 deletions src/main/scala/chatapp/Client.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package chatapp

import rover.Client.OAuth2Credentials
import rover.rdo.AtomicObjectState
import rover.rdo.client.RdObject
import rover.{Client, HTTPClient, Session}

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

class ChatClient(serverAddress: String) extends
HTTPClient[List[ChatMessage]](serverAddress, (credentials) => credentials.accessToken) {
// Client.OAuth2Client[List[ChatMessage]](serverAddress, (credentials: OAuth2Credentials) => credentials.accessToken, Map[String, AtomicObjectState[List[ChatMessage]]]()) {
var session: Session[OAuth2Credentials, List[ChatMessage]] = null
var user: ChatUser = null
var chat: Chat = null;

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")

s"${REPL.SAVE_CURSOR}$cls\r$text${REPL.RESTORE_CURSOR}"
}

// TODO: This is hacky, figure out a better way to do this
val updater: Chat#Updater = state => async {
val text = state.immutableState.takeRight(ChatClient.SIZE).map(m => m.toString()).mkString("\n")
print(s"${printer(text)}")
}

def login(user: ChatUser): Future[Unit] = {
async {
val credentials = new OAuth2Credentials("fake credentials", "fake credentials")
this.user = user
session = createSession(credentials)
val state = importRDO("chat")
val rdo = new RdObject[List[ChatMessage]](state)
chat = Chat.fromRDO(rdo, updater)
// println(s"Initial state: ${chat.state}")
}
}

def send(message: String): Future[Unit] = {
// println(s"Sending message with intial state: ${chat.state}")
async {
await(chat.send(new ChatMessage(message, user)))
exportRDO("chat", chat.state)
// await(session.exportRDO("chat", chat))
// updater(chat.state)
}
}

def updateLoop(): Future[Unit] = {
async {
while(true) {
// val serverState = await(importRDO("chat")).asInstanceOf[RdObject[List[ChatMessage]]]
// chat = Chat.fromRDO(serverState, updater)
// println(s"Updating state from update loop...")
val state = importRDO("chat")
// println(s"Got updated state: $state")
// chat = Chat.fromRDO(rdo, updater)

appendedState("chat", state)
chat.state = state

// println(s"Rendering chat state: ${chat.state}")
updater(chat.state) // Force re-render
Thread.sleep(ChatClient.UPDATE_DELAY_MS)
}
}
}

def render(): Future[Unit] = {
// val chat = new Chat(updater)
println(s" Welcome to Rover Chat! Connected to: $serverAddress")
print((1 to ChatClient.SIZE).map(i => "\n").mkString(""))

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

}

object ChatClient {
val SIZE = 10
val UPDATE_DELAY_MS = 2000

def main(args: Array[String]): Unit = {
val serverAddress = "localhost"
val client = new ChatClient(serverAddress)
val f = async {
await(client.login(ChatUser.Steffan))
await(client.render())
}

Await.result(f, Duration.Inf)
}
}

object Bot {
def main(args: Array[String]): Unit = {
val serverAddress = "localhost"
val client = new ChatClient(serverAddress)
val f = async {
await(client.login(ChatUser.Giannis))
client.updateLoop() // TODO: Memory leak

// Simulate conversation
Thread.sleep(3000)
await(client.send("Hey man!"))

Thread.sleep(3000)
await(client.send("How's it going?"))

Thread.sleep(10000)
await(client.send("Yea man I'm good"))
}

Await.result(f, Duration.Inf)
}
}
38 changes: 38 additions & 0 deletions src/main/scala/chatapp/Message.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package chatapp
import io.circe._, io.circe.syntax._
import io.circe.{Encoder, Json}


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

// def encoded: Json = Encoder.forProduct2("author", "body")(m => m.body, m.author.username)

override def toString: String = {
s"${author.username}: $body"
}
}

object ChatMessage {
implicit val encodeChatMessage: Encoder[ChatMessage] = new Encoder[ChatMessage] {
final def apply(m: ChatMessage): Json = Json.obj(
("author", m.author.asJson),
// ("author", Json.fromString(m.author.username)),
("body", Json.fromString(m.body)),
("timestamp", Json.fromLong(m.timestamp))
)
}

implicit val decodeAtomicObjectState: Decoder[ChatMessage] = new Decoder[ChatMessage] {
final def apply(c: HCursor): Decoder.Result[ChatMessage] =
for {
author <- c.downField("author").as[ChatUser]
body <- c.downField("body").as[String]
timestamp <- c.downField("timestamp").as[Long]
} yield {
new ChatMessage(body, author)
}
}

}
Loading