diff --git a/src/main/scala/io/scalac/slack/IncomingMessageProcessor.scala b/src/main/scala/io/scalac/slack/IncomingMessageProcessor.scala index 761dde6..33bddb5 100644 --- a/src/main/scala/io/scalac/slack/IncomingMessageProcessor.scala +++ b/src/main/scala/io/scalac/slack/IncomingMessageProcessor.scala @@ -21,7 +21,9 @@ class IncomingMessageProcessor(eventBus: MessageEventBus) extends Actor with Act val incomingMessage: IncomingMessage = mType match { case MessageType("hello", _) => Hello case MessageType("pong", _) => Pong - case MessageType("message", None) => s.parseJson.convertTo[BaseMessage] + case MessageType("message", _) => s.parseJson.convertTo[BaseMessage] + case MessageType("message", Some("message_replied")) => + s.parseJson.convertTo[MessageThread] case _ => UndefinedMessage(s) } diff --git a/src/main/scala/io/scalac/slack/OutgoingMessageProcessor.scala b/src/main/scala/io/scalac/slack/OutgoingMessageProcessor.scala index 6a10f69..28e5cbd 100644 --- a/src/main/scala/io/scalac/slack/OutgoingMessageProcessor.scala +++ b/src/main/scala/io/scalac/slack/OutgoingMessageProcessor.scala @@ -18,6 +18,9 @@ class OutgoingMessageProcessor(wsActor: ActorRef, eventBus: MessageEventBus) ext case msg: OutboundMessage => wsActor ! WebSocket.Send(msg.toJson) + case msg: ThreadedOutboundMessage => + wsActor ! WebSocket.Send(msg.toJson) + case ignored => //nothing else } diff --git a/src/main/scala/io/scalac/slack/api/ApiActor.scala b/src/main/scala/io/scalac/slack/api/ApiActor.scala index bb45238..0cd22d0 100644 --- a/src/main/scala/io/scalac/slack/api/ApiActor.scala +++ b/src/main/scala/io/scalac/slack/api/ApiActor.scala @@ -68,7 +68,10 @@ class ApiActor extends Actor with ActorLogging { log.debug("chat.postMessage requested") val attachments = msg.elements.filter(_.isValid).map(_.toJson).mkString("[", ",", "]") - val params = Map("token" -> Config.apiKey.key, "channel" -> msg.channel, "as_user" -> "true", "attachments" -> attachments) + val params = msg.ts match { + case Some(_) => Map("token" -> Config.apiKey.key, "channel" -> msg.channel, "as_user" -> "true", "attachments" -> attachments, "thread_ts" -> msg.ts.get) + case None => Map("token" -> Config.apiKey.key, "channel" -> msg.channel, "as_user" -> "true", "attachments" -> attachments) + } SlackApiClient.post[ChatPostMessageResponse]("chat.postMessage", params) onComplete { case Success(res) => diff --git a/src/main/scala/io/scalac/slack/bots/system/CommandsRecognizerBot.scala b/src/main/scala/io/scalac/slack/bots/system/CommandsRecognizerBot.scala index 87cd837..09723e3 100644 --- a/src/main/scala/io/scalac/slack/bots/system/CommandsRecognizerBot.scala +++ b/src/main/scala/io/scalac/slack/bots/system/CommandsRecognizerBot.scala @@ -10,7 +10,7 @@ class CommandsRecognizerBot(override val bus: MessageEventBus) extends IncomingM def receive: Receive = { - case bm@BaseMessage(text, channel, user, dateTime, edited) => + case bm@BaseMessage(text, channel, user, dateTime, Some(thread_ts),edited) => //COMMAND links list with bot's nam jack can be called: // jack link list // jack: link list diff --git a/src/main/scala/io/scalac/slack/common/MessageJsonProtocol.scala b/src/main/scala/io/scalac/slack/common/MessageJsonProtocol.scala index 2ccf632..d79c410 100644 --- a/src/main/scala/io/scalac/slack/common/MessageJsonProtocol.scala +++ b/src/main/scala/io/scalac/slack/common/MessageJsonProtocol.scala @@ -11,23 +11,84 @@ object MessageJsonProtocol extends DefaultJsonProtocol { def read(value: JsValue) = { - value.asJsObject.getFields("text", "channel", "user", "ts", "edited") match { + value.asJsObject.getFields("text", "channel", "user", "ts", "edited", "thread_ts") match { case Seq(JsString(text), JsString(channel), JsString(user), JsString(ts)) => BaseMessage(text, channel, user, ts, edited = false) case Seq(JsString(text), JsString(channel), JsString(user), JsString(ts), JsObject(edited)) => - BaseMessage(text, channel, user, ts, edited = true) + case Seq(JsString(text), JsString(channel), JsString(user), JsString(ts), JsString(thread_ts)) => + BaseMessage(text, channel, user, ts, Some(thread_ts)) + + case Seq(JsString(text), JsString(channel), JsString(user), JsString(ts), JsString(thread_ts), JsObject(edited)) => + BaseMessage(text, channel, user, ts, Some(thread_ts), edited = true) + case _ => throw new DeserializationException("BaseMessage expected") } } } + implicit object MessageThreadJsonReader extends RootJsonReader[MessageThread] { + def read(value: JsValue) = { + value.asJsObject.getFields("message","hidden","channel", "event_ts", "ts") match { + case Vector(message,JsTrue, JsString(channel), JsString(event_ts), JsString(ts)) => + MessageThread(message.convertTo[Message], true, channel,event_ts,ts) - implicit val messageTypeFormat = jsonFormat(MessageType, "type", "subtype") + case _ => + throw new DeserializationException("MessageThread expected") + } + } + } + implicit object MessageJsonReader extends RootJsonReader[Message] { + def read(value: JsValue) = { + value.asJsObject.getFields("user", "text", "thread_ts", "reply_count", "replies", "unread_count", "ts") match { + case Vector(JsString(user), JsString(text), JsString(thread_ts), JsNumber(reply_count), replies, JsNumber(unread_count),JsString(ts)) => + Message(user, text, thread_ts, reply_count, replies.convertTo[Replies], unread_count, ts) + + case _ => + throw new DeserializationException("Message expected") + } + } + } + + implicit object RepliesJsonReader extends RootJsonFormat[Replies] { + + def read(value: JsValue) = { + println(value.getClass) + value match { + case JsArray(replies) => + val r = replies.map{ x => + x.convertTo[Reply] + } + Replies(r) + + case _ => + throw new DeserializationException("Reply expected") + } + } + + override def write(obj: Replies): JsValue = serializationError("not supported") + } + + implicit object ReplyJsonReader extends RootJsonFormat[Reply] { + + def read(value: JsValue) = { + value.asJsObject.getFields("user", "ts") match { + case Seq(JsString(user), JsString(ts)) => + Reply(user, ts) + + case _ => + throw new DeserializationException("Reply expected") + } + } + + override def write(obj: Reply): JsValue = serializationError("not supported") + } + + implicit val messageTypeFormat = jsonFormat(MessageType, "type", "subtype") } diff --git a/src/main/scala/io/scalac/slack/common/Messages.scala b/src/main/scala/io/scalac/slack/common/Messages.scala index a7a0b42..608c401 100644 --- a/src/main/scala/io/scalac/slack/common/Messages.scala +++ b/src/main/scala/io/scalac/slack/common/Messages.scala @@ -24,7 +24,19 @@ case object Hello extends IncomingMessage * @param user ID of message author * @param ts unique timestamp */ -case class BaseMessage(text: String, channel: String, user: String, ts: String, edited: Boolean = false) extends IncomingMessage +case class BaseMessage(text: String, channel: String, user: String, ts: String, thread_ts: Option[String] = None, edited: Boolean = false) extends IncomingMessage + +//Message thread +case class MessageThread(message: Message, hidden: Boolean, channel: String, event_ts: String, ts: String) extends IncomingMessage + +//Message +case class Message(user: String, text: String, thread_ts: String, reply_count: BigDecimal, replies: Replies, unread_count: BigDecimal, ts: String) extends IncomingMessage + +//Reply +case class Reply(user: String, ts: String) extends IncomingMessage + +//Replies +case class Replies(replies: Seq[Reply]) extends IncomingMessage //user issued command to bot case class Command(command: String, params: List[String], underlying: BaseMessage) extends IncomingMessage @@ -53,12 +65,23 @@ case class OutboundMessage(channel: String, text: String) extends OutgoingMessag override def toJson = s"""{ |"id": ${MessageCounter.next}, - |"type": "message", - |"channel": "$channel", - |"text": "$text" - |}""".stripMargin + |"type": "message", + |"channel": "$channel", + |"text": "$text" + |}""".stripMargin } +//todo: Fold this into Outbound Message +case class ThreadedOutboundMessage(channel: String, text: String, ts: String) extends OutgoingMessage { + override def toJson = + s"""{ + |"id": ${MessageCounter.next}, + |"type": "message", + |"channel": "$channel", + |"text": "$text", + |"thread_ts": "$ts" + |}""".stripMargin +} sealed trait RichMessageElement case class Text(value: String) extends RichMessageElement @@ -79,7 +102,7 @@ object Color { case class ImageUrl(url: String) extends RichMessageElement -case class RichOutboundMessage(channel: String, elements: List[Attachment]) extends MessageEvent +case class RichOutboundMessage(channel: String, elements: List[Attachment], ts: Option[String] = None) extends MessageEvent case class Attachment(text: Option[String] = None, pretext: Option[String] = None, fields: Option[List[Field]] = None, title: Option[String] = None, title_link: Option[String] = None, color: Option[String] = None, image_url: Option[String] = None) { def isValid = text.isDefined || pretext.isDefined || title.isDefined || (fields.isDefined && fields.get.nonEmpty) diff --git a/src/test/scala/io/scalac/slack/UnmarshallerTest.scala b/src/test/scala/io/scalac/slack/UnmarshallerTest.scala index 2ace4b0..5d36b59 100644 --- a/src/test/scala/io/scalac/slack/UnmarshallerTest.scala +++ b/src/test/scala/io/scalac/slack/UnmarshallerTest.scala @@ -718,5 +718,4 @@ class UnmarshallerTest extends FunSuite with Matchers { im.id should equal("D03DQKG1C") im.userId should equal("U03DN1GTQ") } - } diff --git a/src/test/scala/io/scalac/slack/common/MessageJsonProtocolTest.scala b/src/test/scala/io/scalac/slack/common/MessageJsonProtocolTest.scala index 8e1109a..59aaa4e 100644 --- a/src/test/scala/io/scalac/slack/common/MessageJsonProtocolTest.scala +++ b/src/test/scala/io/scalac/slack/common/MessageJsonProtocolTest.scala @@ -3,6 +3,8 @@ package io.scalac.slack.common import org.scalatest.{FunSuite, Matchers} import spray.json._ +import scala.collection.Seq + /** * Created on 10.02.15 17:37 @@ -19,4 +21,98 @@ class MessageJsonProtocolTest extends FunSuite with Matchers { hello.subType should be(None) } + test("Reply") { + + val replyJson = /*language=json*/ """{ + | "user": "U04PFEX3D", + | "ts": "1501038656.314433" + |}""".stripMargin + val reply = replyJson.parseJson.convertTo[Reply] + + reply.user should equal ("U04PFEX3D") + reply.ts should equal ("1501038656.314433") + } + + test("Replies") { + + val repliesJson = /*language=json*/ """[{ + | "user": "U04PFEX3D", + | "ts": "1501038656.314433" + | }, { + | "user": "U04PFEX3D", + | "ts": "1501038707.323684" + | }, { + | "user": "U04PFEX3D", + | "ts": "1501038963.368899" + | }, { + | "user": "U04PFEX3D", + | "ts": "1501038994.374530" + | }, { + | "user": "U04PFEX3D", + | "ts": "1501040179.578238" + | }, { + | "user": "U04PFEX3D", + | "ts": "1501130090.866379" + | }, { + | "user": "U04PFEX3D", + | "ts": "1501130196.883555" + |}]""".stripMargin + val replies = repliesJson.parseJson.convertTo[Replies] + + replies.replies.size should equal (7) + replies.replies.head.user should equal ("U04PFEX3D") + replies.replies.head.ts should equal ("1501038656.314433") + replies.replies(3).user should equal ("U04PFEX3D") + replies.replies(3).ts should equal ("1501038994.374530") + + assert(replies.isInstanceOf[Replies]) + } + + test("Message object") { + /*language=JSON*/ + val messageString = """{ + | "type": "message", + | "user": "U04PFEX3D", + | "text": "can\u2019t", + | "thread_ts": "1500905121.099692", + | "reply_count": 7, + | "replies": [{ + | "user": "U04PFEX3D", + | "ts": "1501038656.314433" + | }, { + | "user": "U04PFEX3D", + | "ts": "1501038707.323684" + | }, { + | "user": "U04PFEX3D", + | "ts": "1501038963.368899" + | }, { + | "user": "U04PFEX3D", + | "ts": "1501038994.374530" + | }, { + | "user": "U04PFEX3D", + | "ts": "1501040179.578238" + | }, { + | "user": "U04PFEX3D", + | "ts": "1501130090.866379" + | }, { + | "user": "U04PFEX3D", + | "ts": "1501130196.883555" + | }], + | "unread_count": 7, + | "ts": "1500905121.099692" + |}""".stripMargin + + val message = messageString.parseJson.convertTo[Message] + message.user should equal("U04PFEX3D") + message.text should equal("can\u2019t") + message.thread_ts should equal("1500905121.099692") + message.reply_count should equal(7) + message.unread_count should equal(7) + message.ts should equal("1500905121.099692") + message.replies.replies.size should equal(7) + + assert(message.replies.isInstanceOf[Replies]) + assert(message.replies.replies.head.isInstanceOf[Reply]) + } + }