├── .gitignore ├── README ├── project ├── plugins.sbt └── Build.scala └── src ├── main ├── resources │ └── logback.xml └── scala │ └── org │ └── fooblahblah │ └── bivouac │ ├── Main.scala │ ├── model │ └── Model.scala │ └── Bivouac.scala └── test └── scala └── org └── fooblahblah └── bivouac └── BivouacSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .target 3 | .classpath 4 | .project 5 | .settings 6 | .scala_dependencies 7 | bin 8 | target 9 | project/boot 10 | .ensime* 11 | .cache 12 | *.launch 13 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This is a Scala client API for Campfire. 2 | 3 | Dependencies: 4 | - scala 2.10 5 | - dispatch 0.11.0 6 | 7 | There are decent API examples in the specs. 8 | 9 | Yes, I realize this is a lame README... 10 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.ensime" % "ensime-sbt-cmd" % "0.1.0") 2 | 3 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.0") 4 | 5 | addSbtPlugin("com.github.retronym" % "sbt-onejar" % "0.8") 6 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/scala/org/fooblahblah/bivouac/Main.scala: -------------------------------------------------------------------------------- 1 | package org.fooblahblah.bivouac 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | 6 | object TestApp { 7 | val client = Bivouac() 8 | 9 | def main(args: Array[String]) { 10 | import client._ 11 | 12 | def printRooms: Future[Unit] = 13 | rooms map { rooms => 14 | rooms foreach { r => 15 | println(r.id + " " + r.name) 16 | } 17 | } 18 | 19 | def printMe: Future[Unit] = 20 | me map (println) 21 | 22 | def printRoom(roomId: Int) = 23 | room(roomId) map (println) 24 | 25 | def printRecent(roomId: Int) = 26 | recentMessages(roomId) map (println) 27 | 28 | val roomId = 562997 29 | 30 | for { 31 | _ <- printMe 32 | // _ <- printRooms 33 | // _ <- printRoom(roomId) 34 | // _ <- printRecent(roomId) 35 | _ <- leave(roomId) 36 | _ <- join(roomId) 37 | // _ <- speak(roomId, "Hello, world!") 38 | abort <- Future.successful(live(roomId, println)) 39 | // _ <- Future(abort()) 40 | } yield () 41 | //sys.exit 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | 4 | object BivouacBuild extends Build { 5 | 6 | lazy val buildSettings = Defaults.defaultSettings ++ Seq( 7 | organization := "org.fooblahblah", 8 | version := "1.1.0", 9 | scalaVersion := "2.10.2", 10 | 11 | resolvers ++= Seq( 12 | "typesafe repo" at "http://repo.typesafe.com/typesafe/maven-releases" 13 | ), 14 | 15 | libraryDependencies ++= Seq( 16 | "ch.qos.logback" % "logback-classic" % "1.0.13", 17 | "com.typesafe" % "config" % "1.0.0", 18 | "junit" % "junit" % "4.11", 19 | "net.databinder.dispatch" %% "dispatch-core" % "0.11.0", 20 | "org.apache.directory.studio" % "org.apache.commons.codec" % "1.6", 21 | "org.specs2" %% "specs2" % "1.13" % "test", 22 | "org.slf4j" % "slf4j-api" % "1.7.5" 23 | ), 24 | scalacOptions ++= Seq("-language:postfixOps", "-language:implicitConversions") 25 | ) 26 | 27 | lazy val playJson = RootProject(uri("https://github.com/victorops/play-json.git")) 28 | 29 | lazy val root = Project(id = "bivouac", base = file("."), settings = buildSettings) dependsOn (playJson) 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/org/fooblahblah/bivouac/model/Model.scala: -------------------------------------------------------------------------------- 1 | package org.fooblahblah.bivouac.model 2 | 3 | import java.util.Date 4 | import play.api.libs.json._ 5 | import play.api.libs.json.Reads._ 6 | import play.api.libs.json.util._ 7 | import play.api.libs.functional.syntax._ 8 | 9 | 10 | object Model { 11 | case class CampfireConfig(token: String, domain: String) 12 | 13 | case class Account(id: Int, name: String, subDomain: String, plan: String, ownerId: Int, timezone: String, createdAt: Date, updatedAt: Date) 14 | 15 | case class Message(id: Int, roomId: Int, userId: Option[Int], messageType: String, body: Option[String], createdAt: Date) 16 | 17 | case class Room(id: Int, name: String, topic: String, membershipLimit: Int, locked: Boolean, createdAt: Date, updatedAt: Date, users: Option[List[User]] = None) 18 | 19 | case class Speak(message: String) { 20 | def toJSON = Json.obj("message" -> Json.obj("body" -> JsString(message))) 21 | } 22 | 23 | case class User(id: Int, name: String, email: String, admin: Boolean, avatarUrl: String, userType: String, createdAt: Date) 24 | 25 | implicit val customDateReads = dateReads("yyyy/MM/dd HH:mm:ss Z") 26 | 27 | implicit val accountReads: Reads[Account] = ( 28 | (__ \ "id").read[Int] ~ 29 | (__ \ "name").read[String] ~ 30 | (__ \ "subdomain").read[String] ~ 31 | (__ \ "plan").read[String] ~ 32 | (__ \ "owner_id").read[Int] ~ 33 | (__ \ "time_zone").read[String] ~ 34 | (__ \ "created_at").read[Date](customDateReads) ~ 35 | (__ \ "updated_at").read[Date](customDateReads))(Account) 36 | 37 | 38 | implicit val userReads: Reads[User] = ( 39 | (__ \ "id").read[Int] ~ 40 | (__ \ "name").read[String] ~ 41 | (__ \ "email_address").read[String] ~ 42 | (__ \ "admin").read[Boolean] ~ 43 | (__ \ "avatar_url").read[String] ~ 44 | (__ \ "type").read[String] ~ 45 | (__ \ "created_at").read[Date](customDateReads))(User) 46 | 47 | implicit val listUserReads: Reads[List[User]] = ((__ \ "users").read(list[User])) 48 | 49 | 50 | implicit val roomReads: Reads[Room] = ( 51 | (__ \ "id").read[Int] ~ 52 | (__ \ "name").read[String] ~ 53 | (__ \ "topic").read[String] ~ 54 | (__ \ "membership_limit").read[Int] ~ 55 | (__ \ "locked").read[Boolean] ~ 56 | (__ \ "created_at").read[Date](customDateReads) ~ 57 | (__ \ "updated_at").read[Date](customDateReads) ~ 58 | (__ \ "users").readNullable(list[User]))(Room) 59 | 60 | implicit val listRoomReads: Reads[List[Room]] = ((__ \ "rooms").read(list[Room])) 61 | 62 | 63 | implicit val messageReads: Reads[Message] = ( 64 | (__ \ "id").read[Int] ~ 65 | (__ \ "room_id").read[Int] ~ 66 | (__ \ "user_id").readNullable[Int] ~ 67 | (__ \ "type").read[String] ~ 68 | (__ \ "body").readNullable[String] ~ 69 | (__ \ "created_at").read[Date](customDateReads))(Message) 70 | 71 | implicit val listMessageReads: Reads[List[Message]] = ((__ \ "messages").read(list[Message])) 72 | } 73 | -------------------------------------------------------------------------------- /src/test/scala/org/fooblahblah/bivouac/BivouacSpec.scala: -------------------------------------------------------------------------------- 1 | package org.fooblahblah.bivouac 2 | 3 | import java.util.concurrent.TimeUnit 4 | import model.Model._ 5 | import org.junit.runner._ 6 | import org.specs2.matcher.Matchers._ 7 | import org.specs2.mutable.Specification 8 | import org.specs2.runner.JUnitRunner 9 | import org.specs2.time.TimeConversions._ 10 | import play.api.libs.json._ 11 | import scala.concurrent._ 12 | import scala.concurrent.duration.Duration 13 | import scala.concurrent.ExecutionContext.Implicits.global 14 | import dispatch.Req 15 | 16 | @RunWith(classOf[JUnitRunner]) 17 | class BivouacSpec extends Specification with Bivouac { 18 | 19 | sequential 20 | 21 | val campfireConfig = CampfireConfig("123456", "foo") 22 | val roomId = 22222 23 | val userId = 5555 24 | 25 | val reconnectTimeout = Duration("5s") 26 | 27 | "Bivouac" should { 28 | "Support account" in { 29 | val result = Await.result(account, Duration(1, TimeUnit.SECONDS)) 30 | result must beAnInstanceOf[Option[Account]] 31 | result.map(_.id) === Some(1111) 32 | } 33 | 34 | "Support rooms" in { 35 | val result = Await.result(rooms, Duration(1, TimeUnit.SECONDS)) 36 | result must beAnInstanceOf[List[Room]] 37 | result.map(_.id) === List(22222, 33333) 38 | } 39 | 40 | "Support get room by id" in { 41 | val result = Await.result(room(roomId), Duration(1, TimeUnit.SECONDS)) 42 | result must beAnInstanceOf[Option[Room]] 43 | result.map(_.id) === Some(22222) 44 | } 45 | 46 | "Support presence" in { 47 | val result = Await.result(presence, Duration(1, TimeUnit.SECONDS)) 48 | result must beAnInstanceOf[List[Room]] 49 | result.map(_.id) === List(22222, 33333) 50 | } 51 | 52 | "Support users/me" in { 53 | val result = Await.result(me, Duration(1, TimeUnit.SECONDS)) 54 | result must beAnInstanceOf[Option[User]] 55 | result.map(_.name) === Some("Jeff Simpson") 56 | } 57 | 58 | "Support user by id" in { 59 | val result = Await.result(user(5555), Duration(1, TimeUnit.SECONDS)) 60 | result must beAnInstanceOf[Option[User]] 61 | result.map(_.name) === Some("John Doe") 62 | } 63 | 64 | "Support joining a room" in { 65 | val result = Await.result(join(roomId), Duration(1, TimeUnit.SECONDS)) 66 | result === true 67 | } 68 | 69 | "Support leaving a room" in { 70 | val result = Await.result(leave(roomId), Duration(1, TimeUnit.SECONDS)) 71 | result === true 72 | } 73 | 74 | "Support updating a room topic" in { 75 | val result = Await.result(updateRoomTopic(roomId, "blah"), Duration(1, TimeUnit.SECONDS)) 76 | result === true 77 | } 78 | 79 | "Support posting a message" in { 80 | val result = Await.result(speak(roomId, "this is a test message"), Duration(1, TimeUnit.SECONDS)) 81 | result === true 82 | } 83 | 84 | "Support recent messages" in { 85 | val result = Await.result(recentMessages(roomId), Duration(1, TimeUnit.SECONDS)) 86 | result.map(_.id) === List(1,2,3,4,5) 87 | } 88 | } 89 | 90 | 91 | 92 | val client = new Client { 93 | lazy val firstRoom = Json.obj("room" -> (Json.parse(roomsArtifact) \ "rooms")(0)) 94 | 95 | def GET(path: String) = path match { 96 | case "/account.json" => Future.successful(Right(accountArtifact.getBytes)) 97 | case "/presence.json" => Future.successful(Right(roomsArtifact.getBytes)) 98 | case "/room/22222/recent.json" => Future.successful(Right(messagesArtifact.getBytes)) 99 | case "/rooms.json" => Future.successful(Right(roomsArtifact.getBytes)) 100 | case "/room/22222.json" => Future.successful(Right(firstRoom.toString.getBytes)) 101 | case "/users/me.json" => Future.successful(Right(meArtifact.getBytes)) 102 | case "/users/5555.json" => Future.successful(Right(userArtifact.getBytes)) 103 | case _ => Future.successful(Left("Not Found")) 104 | } 105 | 106 | def POST(path: String, body: Array[Byte] = Array(), contentType: String = "application/json") = path match { 107 | case "/room/22222/join.json" => Future.successful(Right(Array())) 108 | case "/room/22222/leave.json" => Future.successful(Right(Array())) 109 | case "/room/22222/speak.json" => Future.successful(Right(Array())) 110 | case _ => Future.successful(Left("Not Found")) 111 | } 112 | 113 | def PUT(path: String, body: Array[Byte] = Array(), contentType: String = "application/json") = path match { 114 | case "/room/22222.json" => Future.successful(Right(Array())) 115 | case _ => Future.successful(Left("Not Found")) 116 | 117 | } 118 | } 119 | 120 | val accountArtifact = """ 121 | { 122 | "account": { 123 | "updated_at": "2011/09/07 11:51:16 +0000", 124 | "owner_id": 1234, 125 | "plan": "premium", 126 | "created_at": "2011/04/14 20:55:05 +0000", 127 | "time_zone": "America/New_York", 128 | "name": "Fooblahblah", 129 | "id": 1111, 130 | "storage": 11111, 131 | "subdomain": "fooblahblah" 132 | } 133 | } 134 | """ 135 | 136 | val roomsArtifact = """ 137 | { 138 | "rooms": [ 139 | { 140 | "name": "Foo", 141 | "created_at": "2011/04/14 20:55:05 +0000", 142 | "updated_at": "2011/04/21 23:01:15 +0000", 143 | "topic": "Blah", 144 | "id": 22222, 145 | "membership_limit": 60, 146 | "locked": false, 147 | "users" : [] 148 | }, 149 | { 150 | "name": "Blah", 151 | "created_at": "2011/05/09 17:52:05 +0000", 152 | "updated_at": "2011/05/09 17:52:05 +0000", 153 | "topic": "Foo Blah", 154 | "id": 33333, 155 | "membership_limit": 60, 156 | "locked": false, 157 | "users" : [] 158 | } 159 | ] 160 | } 161 | """ 162 | 163 | val meArtifact = """ 164 | { 165 | "user": { 166 | "type": "Member", 167 | "avatar_url": "http://asset0.37img.com/global/missing/avatar.png?r=3", 168 | "created_at": "2011/04/27 15:20:10 +0000", 169 | "admin": true, 170 | "id": 4444, 171 | "name": "Jeff Simpson", 172 | "email_address": "fooblahblah@fooblahblah.org", 173 | "api_auth_token": "123456676" 174 | } 175 | } 176 | """ 177 | 178 | val userArtifact = """ 179 | { 180 | "user": { 181 | "type": "Member", 182 | "avatar_url": "http://asset0.37img.com/global/missing/avatar.png?r=3", 183 | "created_at": "2011/04/27 15:20:10 +0000", 184 | "admin": true, 185 | "id": 5555, 186 | "name": "John Doe", 187 | "email_address": "john.doe@fooblahblah.org", 188 | "api_auth_token": "123456456" 189 | } 190 | } 191 | """ 192 | 193 | val messagesArtifact = """ 194 | { 195 | "messages": [ 196 | { 197 | "type": "TimestampMessage", 198 | "room_id": 33333, 199 | "created_at": "2011/09/13 15:40:00 +0000", 200 | "id": 1, 201 | "body": null, 202 | "user_id": null 203 | }, 204 | { 205 | "type": "EnterMessage", 206 | "room_id": 33333, 207 | "created_at": "2011/09/13 15:44:33 +0000", 208 | "id": 2, 209 | "body": null, 210 | "user_id": 12345 211 | }, 212 | { 213 | "type": "TextMessage", 214 | "room_id": 33333, 215 | "created_at": "2011/09/14 16:33:21 +0000", 216 | "id": 3, 217 | "body": "anyone still having problems getting into the room?", 218 | "user_id": 45347 219 | }, 220 | { 221 | "type": "TextMessage", 222 | "room_id": 33333, 223 | "created_at": "2011/09/14 17:04:03 +0000", 224 | "id": 4, 225 | "body": "i am guessing some people are just not used to signing in and using this tool", 226 | "user_id": 23423423 227 | }, 228 | { 229 | "type": "KickMessage", 230 | "room_id": 33333, 231 | "created_at": "2011/09/14 17:40:19 +0000", 232 | "id": 5, 233 | "body": null, 234 | "user_id": 935596 235 | } 236 | ] 237 | } 238 | """ 239 | } 240 | 241 | -------------------------------------------------------------------------------- /src/main/scala/org/fooblahblah/bivouac/Bivouac.scala: -------------------------------------------------------------------------------- 1 | package org.fooblahblah.bivouac 2 | 3 | import com.typesafe.config._ 4 | import java.util.Date 5 | import model.Model._ 6 | import org.apache.commons.codec.binary.Base64 7 | import play.api.libs.json._ 8 | import Json._ 9 | import scala.util.control.Exception._ 10 | import org.slf4j.LoggerFactory 11 | import dispatch._ 12 | import com.ning.http.client.Response 13 | import scala.concurrent.ExecutionContext.Implicits.global 14 | import com.ning.http.client.AsyncCompletionHandler 15 | import com.ning.http.client.HttpResponseBodyPart 16 | import com.ning.http.client.AsyncHandler 17 | import scala.util.Try 18 | import scala.util.Failure 19 | import java.util.concurrent.atomic.AtomicBoolean 20 | import com.ning.http.client.HttpResponseStatus 21 | import scala.concurrent.Promise 22 | import scala.concurrent.duration.Duration 23 | 24 | 25 | trait Client { 26 | type Payload = Future[Either[String, Array[Byte]]] 27 | 28 | def GET(path: String): Payload 29 | 30 | def POST(path: String, body: Array[Byte]= Array(), contentType: String = "application/json"): Payload 31 | 32 | def PUT(path: String, body: Array[Byte] = Array(), contentType: String = "application/json"): Payload 33 | } 34 | 35 | 36 | trait Bivouac { 37 | val OK = 200 38 | val CREATED = 201 39 | 40 | protected lazy val logger = LoggerFactory.getLogger(classOf[Bivouac]) 41 | 42 | protected val client: Client 43 | 44 | protected val reconnectTimeout: Duration 45 | 46 | protected def campfireConfig: CampfireConfig 47 | 48 | protected def campfireRequest(path: String) = { 49 | val h = host(s"${campfireConfig.domain}.campfirenow.com").as_!(campfireConfig.token, "X").secure 50 | h.setUrl(h.url + path) 51 | } 52 | 53 | protected def streamingRequest(path: String) = { 54 | val h = host("streaming.campfirenow.com").as_!(campfireConfig.token, "X").secure 55 | h.setUrl(h.url + path) 56 | } 57 | 58 | 59 | def account: Future[Option[Account]] = client.GET("/account.json") map { response => 60 | response match { 61 | case Right(body) => Some((parse(body) \ "account").as[Account]) 62 | case _ => None 63 | } 64 | } 65 | 66 | 67 | def rooms: Future[List[Room]] = client.GET("/rooms.json") map { response => 68 | response match { 69 | case Right(body) => parse(body).as[List[Room]] 70 | case _ => List() 71 | } 72 | } 73 | 74 | 75 | def room(id: Int): Future[Option[Room]] = client.GET(s"/room/${id}.json") map { response => 76 | response match { 77 | case Right(body) => Some((parse(body) \ "room").as[Room]) 78 | case _ => None 79 | } 80 | } 81 | 82 | 83 | def presence: Future[List[Room]] = client.GET("/presence.json") map { response => 84 | response match { 85 | case Right(body) => parse(body).as[List[Room]] 86 | case _ => List() 87 | } 88 | } 89 | 90 | 91 | def me: Future[Option[User]] = client.GET("/users/me.json") map { response => 92 | response match { 93 | case Right(body) => Some((parse(body) \ "user").as[User]) 94 | case _ => None 95 | } 96 | } 97 | 98 | 99 | def user(id: Int): Future[Option[User]] = client.GET(s"/users/${id}.json") map { response => 100 | response match { 101 | case Right(body) => Some((parse(body) \ "user").as[User]) 102 | case _ => None 103 | } 104 | } 105 | 106 | 107 | def join(roomId: Int): Future[Boolean] = client.POST(s"/room/${roomId}/join.json") map { response => 108 | response.isRight 109 | } 110 | 111 | 112 | def leave(roomId: Int): Future[Boolean] = client.POST(s"/room/${roomId}/leave.json") map { response => 113 | response.isRight 114 | } 115 | 116 | 117 | def updateRoomTopic(roomId: Int, topic: String): Future[Boolean] = { 118 | val body = Json.obj("room" -> Json.obj("topic" -> JsString(topic))).toString.getBytes() 119 | client.PUT(s"/room/${roomId}.json", body) map { response => 120 | response.isRight 121 | } 122 | } 123 | 124 | 125 | def speak(roomId: Int, message: String, paste: Boolean = false): Future[Boolean] = { 126 | val msgType = if(paste) "PasteMessage" else "TextMessage" 127 | val body = Json.obj("message" -> Json.obj("type" -> msgType, "body" -> message)).toString.getBytes() 128 | client.POST(s"/room/${roomId}/speak.json", body) map { response => 129 | response.isRight 130 | } 131 | } 132 | 133 | 134 | def recentMessages(roomId: Int): Future[List[Message]] = client.GET(s"/room/${roomId}/recent.json") map { response => 135 | response match { 136 | case Right(body) => parse(body).as[List[Message]] 137 | case _ => Nil 138 | } 139 | } 140 | 141 | 142 | def live(roomId: Int, fn: (Message) => Unit): () => Unit = { 143 | val path = s"/room/${roomId}/live.json" 144 | val req = streamingRequest(path).GET.toRequest 145 | val abortP = Promise[Unit] 146 | 147 | def connect: Future[Unit] = { 148 | logger.info(s"Connecting to room $roomId") 149 | 150 | join(roomId) flatMap { joined => 151 | Http(req, new AsyncCompletionHandler[Unit] { 152 | override def onBodyPartReceived(part: HttpResponseBodyPart) = { 153 | val line = new String(part.getBodyPartBytes()) 154 | 155 | if(!line.trim.isEmpty()) { 156 | Try { 157 | new String(part.getBodyPartBytes()).trim split(13.toChar) map(json => fn(parse(json).as[Message])) 158 | } match { 159 | case Failure(e) => logger.error("Error parsing message", e) 160 | case _ => 161 | } 162 | } 163 | 164 | if(!abortP.isCompleted) AsyncHandler.STATE.CONTINUE 165 | else AsyncHandler.STATE.ABORT 166 | } 167 | 168 | def onCompleted(res: Response) = { 169 | tryReconnect(s"Streaming exited for room $roomId") 170 | } 171 | 172 | override def onThrowable(t: Throwable) { 173 | tryReconnect(s"Stream for room $roomId exited ${t.getMessage}. Retrying in 15s") 174 | } 175 | }) 176 | } 177 | } 178 | 179 | def tryReconnect(msg: String): Future[Unit] = if(!abortP.isCompleted) { 180 | logger.info(msg) 181 | Thread.sleep(reconnectTimeout.toMillis) 182 | connect 183 | } else Future.successful() 184 | 185 | connect flatMap { f => 186 | tryReconnect(s"Streaming connection for room $roomId exited. Sleeping 15s and reconnecting..") 187 | } 188 | 189 | 190 | () => abortP.success() 191 | } 192 | } 193 | 194 | 195 | object Bivouac { 196 | 197 | def apply(): Bivouac = { 198 | val config = ConfigFactory.load 199 | val reconnectTimeout = Duration(failAsValue(classOf[Exception])(config.getString("reconnect-timeout"))("15s")) 200 | 201 | apply(config.getString("domain"), config.getString("token"), reconnectTimeout) 202 | } 203 | 204 | 205 | def apply(domain: String, token: String, timeout: Duration = Duration("15s")): Bivouac = new Bivouac { 206 | val campfireConfig = CampfireConfig(token, domain) 207 | 208 | val reconnectTimeout = timeout 209 | 210 | val client = new Client { 211 | def GET(path: String) = Http(campfireRequest(path).GET) map { response => 212 | response.getStatusCode match { 213 | case OK => Right(response.getResponseBodyAsBytes) 214 | case _ => Left(response.getStatusText) 215 | } 216 | } 217 | 218 | def POST(path: String, body: Array[Byte] = Array(), contentType: String = "application/json") = { 219 | Http(campfireRequest(path).POST.setBody(body).addHeader("Content-Type", contentType)) map { response => 220 | response.getStatusCode match { 221 | case s if s == OK || s == CREATED => Right(response.getResponseBodyAsBytes) 222 | case _ => Left(response.getStatusText) 223 | } 224 | } 225 | } 226 | 227 | def PUT(path: String, body: Array[Byte] = Array(), contentType: String = "application/json") = 228 | Http(campfireRequest(path).PUT.setBody(body).addHeader("Content-Type", contentType)) map { response => 229 | response.getStatusCode match { 230 | case s if s == OK || s == CREATED => Right(response.getResponseBodyAsBytes) 231 | case _ => Left(response.getStatusText) 232 | } 233 | } 234 | } 235 | } 236 | } 237 | --------------------------------------------------------------------------------