You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@esme.apache.org by vd...@apache.org on 2009/01/08 23:09:56 UTC
svn commit: r732847 - in
/incubator/esme/branches/hooks-actions/server/src/main/scala:
bootstrap/liftweb/ us/esme/actor/ us/esme/external/ us/esme/lib/
us/esme/model/ us/esme/view/
Author: vdichev
Date: Thu Jan 8 14:09:55 2009
New Revision: 732847
URL: http://svn.apache.org/viewvc?rev=732847&view=rev
Log:
Huge merge from old repo- tests for login, follow, unfollow, profile change, every N mins; actions for Atom and RSS feeds; possible to send HTTP data in HTTP POST, and others.
Added:
incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/SchedulerActor.scala (with props)
incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/AtomFeed.scala (with props)
incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/Feed.scala (with props)
incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/RssFeed.scala (with props)
Modified:
incubator/esme/branches/hooks-actions/server/src/main/scala/bootstrap/liftweb/Boot.scala
incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/HttpSender.scala
incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/MessagePullActor.scala
incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/UserActor.scala
incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/lib/MsgParser.scala
incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/Action.scala
incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/Mailbox.scala
incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/User.scala
incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/view/ActionView.scala
Modified: incubator/esme/branches/hooks-actions/server/src/main/scala/bootstrap/liftweb/Boot.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/bootstrap/liftweb/Boot.scala?rev=732847&r1=732846&r2=732847&view=diff
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/bootstrap/liftweb/Boot.scala (original)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/bootstrap/liftweb/Boot.scala Thu Jan 8 14:09:55 2009
@@ -115,6 +115,12 @@
LiftRules.early.append(makeUtf8)
Distributor.touch
+ SchedulerActor.touch
+ MessagePullActor.touch
+
+ Action.findAll(By(Action.disabled, false), By(Action.removed, false)).foreach {
+ _.startActors
+ }
DB.addLogFunc(S.logQuery _)
S.addAnalyzer(RequestAnalyzer.analyze _)
Modified: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/HttpSender.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/HttpSender.scala?rev=732847&r1=732846&r2=732847&view=diff
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/HttpSender.scala (original)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/HttpSender.scala Thu Jan 8 14:09:55 2009
@@ -31,6 +31,9 @@
import lib._
import org.apache.commons.httpclient._
+import org.apache.commons.httpclient.auth._
+import methods._
+import java.io.OutputStream
object HttpSender extends Actor with GetPoster {
def act = loop {
@@ -39,33 +42,92 @@
link(ActorWatcher)
- case SendAMessage(action, msg, token) =>
- send(action, msg, token)
+ case SendAMessage(action, msg, user, reason, token) =>
+ send(action, msg, user, reason, token)
case _ =>
}
}
private case object StartMeUp
- case class SendAMessage(action: Performances, msg: Message, token: String)
+ case class SendAMessage(action: Performances, msg: Message, user: User, reason: MailboxReason, token: String)
- private def send(action: Performances, msg: Message, token: String) {
+ private def send(action: Performances, msg: Message, user: User, reason: MailboxReason, token: String) {
import Mailer._
action match {
- case MailTo(who) => Mailer.sendMail(From("i@esme.us"), Subject("msg"),
- To(who),
- XHTMLMailBodyType(msg.digestedXHTML))
- case HttpTo(url, headers) =>
+ case MailTo(who, text) =>
+ val body = text match {
+ case None => XHTMLMailBodyType(msg.digestedXHTML)
+ case Some(t) => PlainMailBodyType(expandText(t, msg, user, reason))
+ }
+ Mailer.sendMail(From("i@esme.us"), Subject("msg"),
+ To(who), body)
+
+ case HttpTo(url, username, password, headers, data) =>
+ val load = data match {
+ case None => ""
+ case Some(d) => expandText(d, msg, user, reason)
+ }
post(url, httpClient,
("X-ESME-Token" -> token) :: headers,
- msg.toXml)
+ username, password,
+ load)
case PerformResend | PerformFilter => // IGNORE
}
}
+ private def expandText(text: String, msg: Message, user: User, reason: MailboxReason) = {
+ val followerId = reason match {
+ case FollowedReason(followerId) => Some(followerId)
+ case UnfollowedReason(followerId) => Some(followerId)
+ case _ => None
+ }
+
+ val followerName = followerId match {
+ case Some(followerId) => User.find(followerId).map[String](_ nickname).openOr("N/A")
+ case None => "N/A"
+ }
+
+ text.replace("%u", user.nickname).
+ replace("%f", followerName).
+ replace("%i", user.imageUrl).
+ replace("%w", user.wholeName).
+ replace("%s", msg.getText)
+ }
+
+ // Overloaded method from GetPoster
+ private def post(url: String, httpClient: HttpClient,
+ headers: List[(String, String)],
+ username: String, password: String,
+ body: String) {
+ val poster = new PostMethod(baseUrl + url)
+ for ((name, value) <- headers) poster.setRequestHeader(name, value)
+ poster.setRequestEntity(new RequestEntity {
+ private val bytes = body.toString.getBytes("UTF-8")
+
+ def getContentLength() = bytes.length
+ def getContentType() = "application/x-www-form-urlencoded"
+ def isRepeatable() = true
+ def writeRequest(out: OutputStream) {
+ out.write(bytes)
+ }
+ })
+
+ httpClient.getState().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password))
+ poster.setDoAuthentication(true)
+
+ try {
+ httpClient.executeMethod(poster)
+ Log.info(poster.getStatusText)
+ Log.info(poster.getResponseBodyAsString)
+ } finally {
+ poster.releaseConnection
+ }
+ }
+
def httpClient = {
val ret = new HttpClient(new SimpleHttpConnectionManager(false))
Modified: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/MessagePullActor.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/MessagePullActor.scala?rev=732847&r1=732846&r2=732847&view=diff
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/MessagePullActor.scala (original)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/MessagePullActor.scala Thu Jan 8 14:09:55 2009
@@ -17,15 +17,24 @@
*/
import scala.actors.Actor
-import scala.actors.TIMEOUT
import scala.actors.Actor._
+import net.liftweb.http.ActorWatcher
import us.esme.actor.Distributor.{UserCreatedMessage=>Msg}
-class MessagePullActor(val messageProcessor: Actor, val refreshSeconds: Int, private var lastMessage: Option[Msg], val messageSource: UniqueMessageSource) extends Actor {
+class MessagePullActor(val messageProcessor: Actor, private var lastMessage: Option[Msg], val messageSource: UniqueMessageSource) extends Actor {
+
+ import MessagePullActor._
def act {
loop {
- reactWithin (refreshSeconds * 1000) {
+ react {
+ case StartUp => {
+ link(ActorWatcher)
+ }
+ case ByeBye => {
+ unlink(ActorWatcher)
+ self.exit()
+ }
case (msgs: List[Msg]) => {
val lastMessages = messageSource.getLastSortedMessages(msgs, lastMessage)
for (message <- lastMessages) {
@@ -33,7 +42,7 @@
lastMessage = Some(message)
}
}
- case TIMEOUT => actor {
+ case FetchMessages => actor {
// "this" used to reference invoking actor
this ! messageSource()
}
@@ -43,6 +52,48 @@
}
+object MessagePullActor extends Actor {
+
+ private var messagePullActors: Map[Any, Actor] = Map()
+
+ def act = loop {
+ react {
+ case StartPullActor(obj, lastMessage, messageSource) => {
+ if (!messagePullActors.contains(obj)) {
+ val pullActor = new MessagePullActor(Distributor, lastMessage, messageSource)
+ messagePullActors += (obj -> pullActor)
+ pullActor.start
+ pullActor ! StartUp
+ }
+ }
+ case StopPullActor(obj) => {
+ if (messagePullActors.contains(obj)) {
+ messagePullActors(obj) ! ByeBye
+ messagePullActors -= obj
+ }
+ }
+ case Fetch(obj) => {
+ if (messagePullActors.contains(obj)) {
+ messagePullActors(obj) ! FetchMessages
+ }
+ }
+ }
+ }
+
+ start
+
+ // do nothing
+ def touch {
+ }
+
+ private case object StartUp
+ private case object ByeBye
+ private case object FetchMessages
+ case class StartPullActor(any: Any, lastMessage: Option[Msg], messageSource: UniqueMessageSource)
+ case class StopPullActor(any: Any)
+ case class Fetch(any: Any)
+}
+
trait UniqueMessageSource extends (() => List[Msg]) {
def messageSorter(a: Msg, b: Msg) = a.when < b.when
Added: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/SchedulerActor.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/SchedulerActor.scala?rev=732847&view=auto
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/SchedulerActor.scala (added)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/SchedulerActor.scala Thu Jan 8 14:09:55 2009
@@ -0,0 +1,103 @@
+package us.esme.actor
+
+/*
+ * Copyright 2008 WorldWide Conferencing, LLC
+ *
+ * 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 scala.actors.Actor
+import scala.actors.TIMEOUT
+import scala.actors.Actor._
+import us.esme.actor.Distributor.AddMessageToMailbox
+import us.esme.model._
+import net.liftweb.http.ActorWatcher
+import net.liftweb.util.{Full,Empty,TimeHelpers}
+
+class SchedulerActor(val messageProcessor: Actor, val user: Long, val everySeconds: Int, val reason: MailboxReason) extends Actor {
+
+ import SchedulerActor._
+
+ def act {
+ sendMessage()
+ loop {
+ reactWithin (everySeconds * 1000) {
+ case StartUp => {
+ link(ActorWatcher)
+ }
+ case ByeBye => {
+ unlink(ActorWatcher)
+ self.exit()
+ }
+ case TIMEOUT => {
+ sendMessage()
+ }
+ }
+ }
+ }
+
+ def sendMessage() {
+ val now = System.currentTimeMillis
+ val dateString = TimeHelpers.toInternetDate(now)
+ Message.create.author(user).
+ when(now).
+ source("every").
+ setTextAndTags("Regularly scheduled action: " + dateString, Nil, Empty).
+ foreach{ msg =>
+ // Noisy & can't be rejected
+ // if (msg.save) {
+ messageProcessor ! Distributor.AddMessageToMailbox(user, msg, reason)
+ // }
+ }
+ }
+
+}
+
+object SchedulerActor extends Actor{
+
+ private var regularsPerAction: Map[Long, List[Actor]] = Map()
+
+ def act = loop {
+ react {
+ case StartRegular(action, everySeconds) => {
+ val id = action.id.is
+ val regularActor = new SchedulerActor(Distributor, action.user, everySeconds, RegularReason(id))
+ if (!regularsPerAction.contains(id)) {
+ regularsPerAction += (id -> List(regularActor))
+ } else {
+ regularsPerAction(id) += regularActor
+ }
+ regularActor.start
+ regularActor ! StartUp
+ }
+
+ case StopRegular(id) => {
+ if (regularsPerAction.contains(id)) {
+ for(regularActor <- regularsPerAction(id)) regularActor ! ByeBye
+ regularsPerAction -= id
+ }
+ }
+ }
+ }
+
+ start
+
+ // do nothing
+ def touch {
+ }
+
+ case class StartRegular(action: Action, everySeconds: Int)
+ case class StopRegular(action: Long)
+ private case object StartUp
+ private case object ByeBye
+}
Propchange: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/SchedulerActor.scala
------------------------------------------------------------------------------
svn:executable = *
Modified: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/UserActor.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/UserActor.scala?rev=732847&r1=732846&r2=732847&view=diff
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/UserActor.scala (original)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/UserActor.scala Thu Jan 8 14:09:55 2009
@@ -168,7 +168,7 @@
// get all the performance things
val cal = buildCalendar
- val toDo = perform.filter(_.func(msg, userId, cal))
+ val toDo = perform.filter(_.func(msg, userId, cal, reason))
// is one of those reasons rejection of the message
val reject = toDo.exists(_.filter_?)
@@ -181,6 +181,11 @@
case DirectReason(fromId) => mb.directlyFrom(fromId)
case ConversationReason(convId) => mb.conversation(convId)
case ResendReason(resender) => mb.resentBy(resender)
+ case LoginReason(loggedId) => mb.login(loggedId)
+ case FollowedReason(followerId) => mb.followed(followerId)
+ case UnfollowedReason(unfollowerId) => mb.unfollowed(unfollowerId)
+ case ProfileReason(moduserId) => mb.profile(moduserId)
+ case RegularReason(actionId) => mb.regular(actionId)
case NoReason =>
}
mb.saveMe
@@ -193,17 +198,22 @@
td =>
td.whatToDo match {
- case m @ MailTo(_) =>
- HttpSender ! HttpSender.SendAMessage(m, msg, td.uniqueId)
+ case m @ MailTo(_, _) =>
+ User.find(userId).foreach( u =>
+ HttpSender ! HttpSender.SendAMessage(m, msg, u, reason, td.uniqueId))
- case h @ HttpTo(_, _) =>
- HttpSender ! HttpSender.SendAMessage(h, msg, td.uniqueId)
+ case h @ HttpTo(_, _, _, _, _) =>
+ User.find(userId).foreach( u =>
+ HttpSender ! HttpSender.SendAMessage(h, msg, u, reason, td.uniqueId))
case PerformResend =>
+ if (! msg.saved_?) msg.save
for (id <- followers)
Distributor !
Distributor.AddMessageToMailbox(id, msg, ResendReason(userId))
+ case FetchFeed(url) => MessagePullActor ! MessagePullActor.Fetch(td.performId)
+
case PerformFilter => // IGNORE
}
}
Added: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/AtomFeed.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/AtomFeed.scala?rev=732847&view=auto
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/AtomFeed.scala (added)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/AtomFeed.scala Thu Jan 8 14:09:55 2009
@@ -0,0 +1,66 @@
+package us.esme.external
+
+/*
+ * Copyright 2008 WorldWide Conferencing, LLC
+ *
+ * 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 java.text._
+import java.util.Locale.US
+
+import us.esme.model.User
+
+object AtomFeed{
+ val dateFormats = List(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", US),
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", US),
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", US),
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", US))
+}
+
+class AtomFeed(user: User, atomURL: String, source: String, truncateChars: Int, tags: List[String])
+ extends Feed(user, atomURL, source, truncateChars, tags) {
+ import scala.xml._
+
+ override def dateFormats = AtomFeed.dateFormats
+
+ override def getEntries(xml: Elem) = xml \ "entry"
+
+ override def getText(node: Node) = {
+ // a title element is mandatory
+ node \ "title" text
+ }
+
+ override def getLink(node: Node) = {
+ // there must be either a link with @rel="alternate"
+ // or a link without @rel or content
+ val link = node \ "link"
+ if (link isEmpty)
+ node \ "content" text
+ else {
+ val alternate = link find(_ \ "@rel" == "alternate")
+ val anyLink = alternate getOrElse((link find(_ \ "@rel" == Nil)).get)
+ anyLink \ "@href" text
+ }
+ }
+
+ override def getDate(node: Node) = {
+ // parseInternetDate(node \ "published" text).getTime
+ val published = node \ "published"
+ val date = if (published isEmpty)
+ node \ "updated"
+ else published
+ parseInternetDate(date text).getTime
+ }
+}
+
Propchange: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/AtomFeed.scala
------------------------------------------------------------------------------
svn:executable = *
Added: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/Feed.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/Feed.scala?rev=732847&view=auto
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/Feed.scala (added)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/Feed.scala Thu Jan 8 14:09:55 2009
@@ -0,0 +1,98 @@
+package us.esme.external
+
+/*
+ * Copyright 2008 WorldWide Conferencing, LLC
+ *
+ * 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 java.io.IOException
+import java.text._
+import java.util.Date
+
+import org.apache.commons.httpclient.methods.GetMethod
+
+import us.esme.actor.HttpSender
+import us.esme.actor.UniqueMessageSource
+import us.esme.actor.Distributor.UserCreatedMessage
+import us.esme.model.User
+
+import net.liftweb.util._
+
+abstract class Feed(val user: User, val url: String, val source: String, val truncateChars: Int, val tags: List[String]) extends UniqueMessageSource {
+ // import java.net.{URLConnection, URL}
+ import scala.xml._
+
+ protected def dateFormats: List[SimpleDateFormat]
+ var dateFormat: SimpleDateFormat = _
+
+ override def apply() = {
+
+ ( for (node <- (getEntries(XML.loadString(responseString))))
+ yield UserCreatedMessage(
+ if (user != null) {user.id} else 0,
+ getText(node) + " " + getLink(node),
+ tags,
+ getDate(node),
+ Empty,
+ source,
+ Empty
+ )
+ ).toList
+ }
+
+ protected def getEntries(xml: Elem): NodeSeq
+
+ protected def getText(xml: Node): String
+
+ protected def getLink(xml: Node): String
+
+ protected def getDate(xml: Node): Long
+
+ protected def responseString() = {
+ // url.openConnection
+ val httpClient = HttpSender.httpClient
+ val method = new GetMethod(url)
+ try {
+ httpClient.executeMethod(method)
+ method.getResponseBodyAsString
+ } catch {
+ case ioe: IOException => ""
+ } finally {
+ method.releaseConnection
+ }
+ }
+
+ protected def parseInternetDate(dateString: String): Date = {
+ val fixedDateString = fixDateString(dateString)
+ if (dateFormat == null) dateFormats.find { df =>
+ try {
+ dateFormat = df
+ return df.parse(fixedDateString)
+ // true
+ } catch {
+ case pe: ParseException => false
+ }
+ }
+ dateFormat.parse(fixedDateString)
+ }
+
+ protected def fixDateString(dateString: String) = {
+ val l = dateString.length
+ if (dateString.charAt(l - 3) == ':')
+ dateString.substring(0,l-3) + dateString.substring(l - 2, l)
+ else dateString
+ }
+
+}
+
Propchange: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/Feed.scala
------------------------------------------------------------------------------
svn:executable = *
Added: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/RssFeed.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/RssFeed.scala?rev=732847&view=auto
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/RssFeed.scala (added)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/RssFeed.scala Thu Jan 8 14:09:55 2009
@@ -0,0 +1,64 @@
+package us.esme.external
+
+/*
+ * Copyright 2008 WorldWide Conferencing, LLC
+ *
+ * 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 java.text._
+import java.util.Locale.US
+
+import us.esme.model.User
+
+object RssFeed {
+ val dateFormats = List(new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", US))
+}
+
+class RssFeed(user: User, rssURL: String, source: String, truncateChars: Int, tags: List[String])
+ extends Feed(user, rssURL, source, truncateChars, tags) {
+ import scala.xml._
+
+ override def dateFormats = RssFeed.dateFormats
+
+ override def getEntries(xml: Elem) = xml \\ "item"
+
+ override def getText(node: Node) = {
+ // if there's no title, get description
+ val title = node \ "title"
+ if (title isEmpty)
+ node \ "description" text
+ else
+ title text
+ }
+
+ override def getLink(node: Node) = {
+ // a link is optional
+ val link = node \ "link"
+ if (link isEmpty)
+ ""
+ else
+ link text
+ }
+
+ override def getDate(node: Node) = {
+ // if there's no published date, take current time
+ val date = node \ "pubDate"
+ if (date isEmpty)
+ System.currentTimeMillis
+ else
+ parseInternetDate(date text).getTime
+ }
+
+}
+
Propchange: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/RssFeed.scala
------------------------------------------------------------------------------
svn:executable = *
Modified: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/lib/MsgParser.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/lib/MsgParser.scala?rev=732847&r1=732846&r2=732847&view=diff
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/lib/MsgParser.scala (original)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/lib/MsgParser.scala Thu Jan 8 14:09:55 2009
@@ -58,10 +58,16 @@
// def ip_schemepart = (accept("//") ~> login) ~> opt( '/' ~> urlpath)
- lazy val login: Parser[String] =
- opt(user ~ opt( ':' ~> password ) <~ '@' ) ~ hostport ^^ {
- case None ~ hostport => hostport
- case Some(user ~ pwd) ~ hostport => user+(pwd.map(p => ":"+p))+"@"+hostport
+ lazy val login: Parser[String] = userPass ^^ {
+ case ("", _) => ""
+ case (user, "") => user + "@"
+ case (user, password) => user + ":" + password + "@"
+ }
+
+ lazy val userPass: Parser[(String, String)] =
+ opt(user ~ opt( ':' ~> password ) <~ '@' ) ^^ {
+ case None => ("", "")
+ case Some(user ~ pwd) => (user, pwd.getOrElse(""))
}
lazy val hostport: Parser[String] = host ~ opt( ':' ~> port ) ^^ {
@@ -102,13 +108,18 @@
case xs => xs.mkString
}
+ lazy val scheme: Parser[String] = (accept("http://") | accept("https://")) ^^ {_ mkString}
+
+ lazy val httpUrl: Parser[String] = scheme ~ login ~ urlpart ^^ {
+ case front ~ login ~ urlpart => front + login + urlpart
+ }
- lazy val httpUrl: Parser[String] = (accept("http://") | accept("https://")) ~ hostport ~
- opt( '/' ~> hpath ~ opt('?' ~> search )) ^^ {
- case front ~ hp ~ None => front.mkString + hp
- case front ~ hp ~ Some(pth ~ None) => front.mkString + hp + "/" + pth
- case front ~ hp ~ Some(pth ~ Some(search)) =>
- front.mkString + hp + "/" + pth + "?" + search
+ lazy val urlpart: Parser[String] =
+ hostport ~ opt( '/' ~> hpath ~ opt('?' ~> search )) ^^ {
+ case hp ~ None => hp
+ case hp ~ Some(pth ~ None) => hp + "/" + pth
+ case hp ~ Some(pth ~ Some(search)) =>
+ hp + "/" + pth + "?" + search
}
lazy val hpath: Parser[String] = hsegment ~ rep('/' ~> hsegment) ^^ {
@@ -182,16 +193,23 @@
lazy val _perform: Parser[Performances] =
(acceptCI("filter") ~ lineSpace ~ EOF ^^^ PerformFilter) |
(acceptCI("resend") ~ lineSpace ~ EOF ^^^ PerformResend) |
- (mailtoUrl <~ EOF ^^ {case mt => MailTo(mt)}) |
- (httpUrl ~ rep(httpHeader) <~ EOF ^^ {
- case http ~ hdrs => HttpTo(http, hdrs)
- })
+ (mailtoUrl ~ opt(rep(EOL) ~> rep1(anyChar)) <~ EOF ^^ {
+ case mt ~ text => MailTo(mt, text.map(_ mkString))
+ }) |
+ (scheme ~ userPass ~ urlpart ~ rep(httpHeader) ~ httpData <~ EOF ^^ {
+ case protocol ~ userPass ~ urlpart ~ hdrs ~ data =>
+ HttpTo(protocol + urlpart, userPass._1, userPass._2, hdrs, data)
+ }) |
+ (acceptCI("atom:") ~> httpUrl <~ EOF ^^ {url => FetchAtom(UrlStore.make(url))}) |
+ (acceptCI("rss:") ~> httpUrl <~ EOF ^^ {url => FetchRss(UrlStore.make(url))})
lazy val httpHeader: Parser[(String, String)] = EOL ~ accept("header:") ~
lineSpace ~> rep1(uchar) ~ '=' ~ rep1(uchar) ^^ {
case name ~ _ ~ value => (name.mkString, value.mkString)
}
+ lazy val httpData: Parser[Option[String]] = opt(EOL ~> rep1(anyChar)) ^^ { _ map(_ mkString) }
+
def testMessage(in: String): Box[TestAction] = _testMessage(in) match {
case Success(ta, _) => Full(ta)
case _ => Empty
@@ -212,7 +230,9 @@
testAt | testRegex | testString |
testTag |
testParen | testPercent |
- testDates |
+ testDates | testLogin |
+ testFollowed | testUnfollowed |
+ testProfile | testRegular |
anyMsg | testToMe) <~ whiteSpace
lazy val toOpr: Parser[EqOprType] =
@@ -241,6 +261,18 @@
case x ~ xs => x :: xs
}
+ lazy val testLogin: Parser[TestAction] = acceptCI("login") ^^^ LoginAction()
+
+ lazy val testFollowed: Parser[TestAction] = acceptCI("followed") ^^^ FollowedAction()
+
+ lazy val testUnfollowed: Parser[TestAction] = acceptCI("unfollowed") ^^^ UnfollowedAction()
+
+ lazy val testProfile: Parser[TestAction] = acceptCI("profile") ^^^ ProfileAction()
+
+ lazy val testRegular: Parser[TestAction] = acceptCI("every") ~ whiteSpace ~> number <~ whiteSpace ~ acceptCI("mins") ^^ {
+ case mins => RegularAction(mins)
+ }
+
lazy val testDates: Parser[TestAction] =
((whiteSpace ~> dateKeyword) ~ (whiteSpace ~> toOpr) ~ (whiteSpace ~> numberList) ^^
{
Modified: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/Action.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/Action.scala?rev=732847&r1=732846&r2=732847&view=diff
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/Action.scala (original)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/Action.scala Thu Jan 8 14:09:55 2009
@@ -25,6 +25,7 @@
import us.esme._
import lib._
import actor._
+import external._
import java.util.Calendar
import scala.xml.{Text, Node, Elem => XmlElem}
@@ -36,64 +37,134 @@
Distributor ! Distributor.UpdateTrackingFor(in.user,
Distributor.PerformTrackingType)
}
+
+ override def afterSave = startStopActors _ :: super.afterSave
- type TestFunc = (Message, Long, Calendar) => Boolean
+ private def startStopActors(in: Action) {
+ if (!in.removed.is && in.enabled) {
+ in.startActors()
+ } else {
+ SchedulerActor ! SchedulerActor.StopRegular(in.id)
+ MessagePullActor ! MessagePullActor.StopPullActor(in.id)
+ }
+ }
+
+ type TestFunc = (Message, Long, Calendar, MailboxReason) => Boolean
lazy val TrueFunc: TestFunc = {case _ => true}
- lazy val SentToMe: TestFunc = (m, u, c) => m.sentToIds.contains(u)
+ lazy val SentToMe: TestFunc = (m, u, c, r) => m.sentToIds.contains(u)
def toFunc(in: TestAction): TestFunc = in match {
case AnyAction => TrueFunc
case NotAction(action) =>
val f: TestFunc = this.toFunc(action)
- (m, u, c) => !f(m,u,c)
+ (m, u, c, r) => !f(m,u,c,r)
case OrAction(left, right) =>
val f1 = toFunc(left)
val f2 = toFunc(right)
- (m, u, c) => f1(m, u, c) || f2(m, u, c)
+ (m, u, c, r) => f1(m, u, c, r) || f2(m, u, c, r)
case AndAction(left, right) =>
val f1 = toFunc(left)
val f2 = toFunc(right)
- (m, u, c) => f1(m, u, c) && f2(m, u, c)
+ (m, u, c, r) => f1(m, u, c, r) && f2(m, u, c, r)
+ case LoginAction() =>
+ (m, u, c, r) => r.isInstanceOf[LoginReason]
+
+ case FollowedAction() =>
+ (m, u, c, r) => r.isInstanceOf[FollowedReason]
+
+ case UnfollowedAction() =>
+ (m, u, c, r) => r.isInstanceOf[UnfollowedReason]
+
+ case RegularAction(mins) =>
+ (m, u, c, r) => r.isInstanceOf[RegularReason]
+
+ case ProfileAction() =>
+ (m, u, c, r) => r.isInstanceOf[ProfileReason]
+
case AtUserAction(userId) =>
- (m, u, c) => m.author.is == userId
+ (m, u, c, r) => m.author.is == userId
case SentToMeAction =>
SentToMe
case RegexAction(re) =>
val r = re.r
- (m, u, c) => r.findFirstIn(m.getText).isDefined
+ (m, u, c, reason) => r.findFirstIn(m.getText).isDefined
case StringAction(s) =>
val str = s.toLowerCase.trim
- (m, u, c) => m.getText.toLowerCase.indexOf(str) >= 0
+ (m, u, c, r) => m.getText.toLowerCase.indexOf(str) >= 0
case HashAction(id, _) =>
- (m, u, c) => m.tagIds.contains(id)
+ (m, u, c, r) => m.tagIds.contains(id)
case ParenAction(a) =>
toFunc(a)
case PercentAction(percent) =>
- (m, u, c) => Helpers.randomInt(100) <= percent
+ (m, u, c, r) => Helpers.randomInt(100) <= percent
case AtSendAction(users, EqOpr) =>
- (m, u, c) => !m.sentToIds.intersect(users).isEmpty
+ (m, u, c, r) => !m.sentToIds.intersect(users).isEmpty
case AtSendAction(users, NeOpr) =>
- (m, u, c) => m.sentToIds.intersect(users).isEmpty
+ (m, u, c, r) => m.sentToIds.intersect(users).isEmpty
case DateTestAction(dt, ot, what) =>
- (m, u, c) => ot.buildFunc(dt.buildFunc(c), what)
+ (m, u, c, r) => ot.buildFunc(dt.buildFunc(c), what)
+ }
+
+ def regularActions(in: TestAction): List[RegularAction] = in match {
+ case NotAction(a) => regularActions(a)
+
+ case ParenAction(a) => regularActions(a)
+
+ case OrAction(left, right) => regularActions(left) ::: regularActions(right)
+
+ case AndAction(left, right) => regularActions(left) ::: regularActions(right)
+
+ case a @ RegularAction(mins) => List(a)
+
+ case _ => Nil
}
}
class Action extends LongKeyedMapper[Action] {
+
+ def startActors() {
+ for(regular <- regularActions) regular match {
+ case RegularAction(mins) => SchedulerActor ! SchedulerActor.StartRegular(this, mins * 60)
+ }
+ val urlSourcePrefix = "url:"
+ theAction.actionFunc match {
+ case a @ (FetchFeed(url)) => {
+ User.find(user) match {
+ case Full(u) =>
+ val msgList = Message.findAll(By(Message.source, urlSourcePrefix + url.uniqueId),
+ OrderBy(Message.id, Descending),
+ MaxRows(1))
+ val lastMsg = if (msgList.isEmpty) None
+ else {
+ val m = msgList.first
+ Some(Distributor.UserCreatedMessage(user, m.text, m.tags, m.when, Empty, m.source, Full(m.replyTo)))
+ }
+
+ val feed = a match {
+ case FetchAtom(_) => new AtomFeed(u, url.url, urlSourcePrefix + url.uniqueId, 0, Nil)
+ case FetchRss(_) => new RssFeed(u, url.url, urlSourcePrefix + url.uniqueId, 0, Nil)
+ }
+ MessagePullActor ! MessagePullActor.StartPullActor(id, lastMsg, feed)
+ }
+ }
+ case _ =>
+ }
+ }
+
def getSingleton = Action // what's the "meta" server
def primaryKeyField = id
@@ -155,6 +226,10 @@
def testText = theTest.is
+ def regularActions: List[RegularAction] = testExpr(testText) match {
+ case Success(v, _) => Action.regularActions(v)
+ }
+
def actionText = theAction.is
def setAction(in: String): Box[Action] = _perform(in) match {
@@ -183,8 +258,8 @@
class PerformMatcher(val func: Action.TestFunc, val performId: Long,
val uniqueId: String, val whatToDo: Performances) {
- def doesMatch(msg: Message, userId: Long, cal: Calendar): Boolean =
- func(msg, userId, cal)
+ def doesMatch(msg: Message, userId: Long, cal: Calendar, reason: MailboxReason): Boolean =
+ func(msg, userId, cal, reason)
def filter_? = whatToDo == PerformFilter
}
@@ -270,6 +345,26 @@
})
}
+case class LoginAction extends TestAction {
+ def toStr = "login"
+}
+
+case class FollowedAction extends TestAction {
+ def toStr = "followed"
+}
+
+case class UnfollowedAction extends TestAction {
+ def toStr = "unfollowed"
+}
+
+case class ProfileAction extends TestAction {
+ def toStr = "profile"
+}
+
+case class RegularAction(mins: Int) extends TestAction {
+ def toStr = "every " + mins + " mins"
+}
+
case class DateTestAction(dateType: DateType, opt: OprType, what: List[Int]) extends TestAction {
def toStr = dateType.toStr + " " + opt.toStr + " " + (
what match {
@@ -336,7 +431,10 @@
}
sealed trait Performances
-case class MailTo(who: String) extends Performances
-case class HttpTo(url: String, headers: List[(String, String)]) extends Performances
+case class MailTo(who: String, text: Option[String]) extends Performances
+case class HttpTo(url: String, user: String, password: String, headers: List[(String, String)], data: Option[String]) extends Performances
+case class FetchFeed(url: UrlStore) extends Performances
+case class FetchAtom(override val url: UrlStore) extends FetchFeed(url)
+case class FetchRss(override val url: UrlStore) extends FetchFeed(url)
case object PerformResend extends Performances
case object PerformFilter extends Performances
Modified: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/Mailbox.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/Mailbox.scala?rev=732847&r1=732846&r2=732847&view=diff
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/Mailbox.scala (original)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/Mailbox.scala Thu Jan 8 14:09:55 2009
@@ -52,6 +52,11 @@
object directlyFrom extends MappedLongForeignKey(this, User)
object conversation extends MappedLongForeignKey(this, Message)
object resentBy extends MappedLongForeignKey(this, User)
+ object login extends MappedLongForeignKey(this, User)
+ object followed extends MappedLongForeignKey(this, User)
+ object unfollowed extends MappedLongForeignKey(this, User)
+ object profile extends MappedLongForeignKey(this, User)
+ object regular extends MappedLongForeignKey(this, Action)
lazy val reason: MailboxReason =
viaTrack.can.map(TrackReason) or directlyFrom.can.map(DirectReason) or
@@ -76,3 +81,18 @@
case class ConversationReason(conversationId: Long) extends MailboxReason {
def attr = new UnprefixedAttribute("conversation", conversationId.toString, Null)
}
+case class LoginReason(userId: Long) extends MailboxReason {
+ def attr = new UnprefixedAttribute("login", userId.toString, Null)
+}
+case class FollowedReason(userId: Long) extends MailboxReason {
+ def attr = new UnprefixedAttribute("followed", userId.toString, Null)
+}
+case class UnfollowedReason(userId: Long) extends MailboxReason {
+ def attr = new UnprefixedAttribute("unfollowed", userId.toString, Null)
+}
+case class ProfileReason(userId: Long) extends MailboxReason {
+ def attr = new UnprefixedAttribute("profile", userId.toString, Null)
+}
+case class RegularReason(actionId: Long) extends MailboxReason {
+ def attr = new UnprefixedAttribute("regular", actionId.toString, Null)
+}
Modified: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/User.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/User.scala?rev=732847&r1=732846&r2=732847&view=diff
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/User.scala (original)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/model/User.scala Thu Jan 8 14:09:55 2009
@@ -36,6 +36,7 @@
import us.esme._
import actor._
+import view._
import java.net.URL
import java.util.logging._
@@ -43,12 +44,23 @@
val logger: Logger = Logger.getLogger("us.esme.model.User")
logger.setLevel(Level.INFO)
- override def afterSave = notifyActors _ :: super.afterSave
+ override def afterSave = profileChanged _ :: notifyActors _ :: super.afterSave
private def notifyActors(in: User) {
Distributor ! Distributor.UserUpdated(in.id)
}
+ private def profileChanged(in: User) {
+ Message.create.author(in.id).
+ when(Helpers.timeNow.getTime).
+ source("profile").
+ setTextAndTags("User " + in.nickname + " changed profile. Name: " + in.wholeName + ", Image: " + in.imageUrl, Nil, Empty).
+ foreach{ msg =>
+ if (msg.save) {
+ Distributor ! Distributor.AddMessageToMailbox(in.id, msg, ProfileReason(in.id))
+ }
+ }
+ }
def findFromWeb(uid: String): Box[User] =
User.find(By(User.nickname, uid)) or User.find(uid)
@@ -116,6 +128,17 @@
val user = User.findOrCreate(id.getIdentifier)
User.logUserIn(user)
S.notice("Welcome "+user.niceName)
+
+ Message.create.author(user.id).
+ when(Helpers.timeNow.getTime).
+ source("login").
+ setTextAndTags("User " + user.nickname + " logged in.", Nil, Empty).
+ foreach{ msg =>
+ if (msg.save) {
+ Distributor ! Distributor.AddMessageToMailbox(user.id, msg, LoginReason(user.id))
+ }
+ }
+
RedirectResponse("/", S responseCookies :_*)
case (_, Full(exp)) =>
@@ -214,15 +237,35 @@
Relationship.find(By(Relationship.owner, this),
By(Relationship.target, who)) match {
case Full(x) => true
- case Empty => Relationship.create.owner(this).
- target(who).save
+ case Empty => { if (Relationship.create.owner(this).target(who).save)
+ Message.create.author(who.id).
+ when(Helpers.timeNow.getTime).
+ source("followed").
+ setTextAndTags("User " + this.nickname + " followed " + who.nickname + ".", Nil, Empty).
+ foreach { msg =>
+ if (msg.save) {
+ Distributor ! Distributor.AddMessageToMailbox(who.id, msg, FollowedReason(this.id))
+ }
+ }
+ true
+ }
case _ => false
}
}
def unfollow(who: User): Boolean = {
Relationship.findAll(By(Relationship.owner, this),
- By(Relationship.target, who)).foreach(_.delete_!)
+ By(Relationship.target, who)).foreach{ r =>
+ if (r.delete_!) Message.create.author(who.id).
+ when(Helpers.timeNow.getTime).
+ source("unfollowed").
+ setTextAndTags("User " + this.nickname + " unfollowed " + who.nickname + ".", Nil, Empty).
+ foreach{ msg =>
+ if (msg.save) {
+ Distributor ! Distributor.AddMessageToMailbox(who.id, msg, UnfollowedReason(this.id))
+ }
+ }
+ }
true
}
Modified: incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/view/ActionView.scala
URL: http://svn.apache.org/viewvc/incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/view/ActionView.scala?rev=732847&r1=732846&r2=732847&view=diff
==============================================================================
--- incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/view/ActionView.scala (original)
+++ incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/view/ActionView.scala Thu Jan 8 14:09:55 2009
@@ -138,7 +138,12 @@
day = (0,1) -- sent on Sunday or Monday<br/>
#moo -- contains the #moo tag<br/>
50% -- success 50% of the time<br/>
- @foo & 50% -- half the time, something sent by @foo
+ @foo & 50% -- half the time, something sent by @foo<br/>
+ login -- user has logged in<br/>
+ followed -- user is being followed<br/>
+ unfollowed -- user is being unfollowed<br/>
+ profile -- user changed profile<br/>
+ every N mins -- repeat action, N is an integer
</td>
</tr>
@@ -149,7 +154,9 @@
filter -- not put in your timeline<br />
resend -- sends the message to all your followers<br />
mailto:foo@bar.com -- sends the message to foo@bar.com<br/>
- http://foo.com/message/in -- makes an HTTP post with the message
+ http://foo.com/message/in -- HTTP post, %s expands to message<br/>
+ atom:http://blog.com/feed.atom -- posts new messages from Atom feed<br/>
+ rss:http://blog.com/feed.rss -- posts new messages from RSS feed
</td>
</tr>
<input type="submit" value="Add" />