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/16 22:30:23 UTC

svn commit: r735148 - in /incubator/esme/trunk/server/src/main/scala: bootstrap/liftweb/ us/esme/actor/ us/esme/external/ us/esme/lib/ us/esme/model/ us/esme/view/

Author: vdichev
Date: Fri Jan 16 13:30:22 2009
New Revision: 735148

URL: http://svn.apache.org/viewvc?rev=735148&view=rev
Log:
Huge merge- 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/trunk/server/src/main/scala/us/esme/actor/SchedulerActor.scala
      - copied unchanged from r732847, incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/actor/SchedulerActor.scala
    incubator/esme/trunk/server/src/main/scala/us/esme/external/AtomFeed.scala
      - copied unchanged from r732847, incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/AtomFeed.scala
    incubator/esme/trunk/server/src/main/scala/us/esme/external/Feed.scala
      - copied unchanged from r732847, incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/Feed.scala
    incubator/esme/trunk/server/src/main/scala/us/esme/external/RssFeed.scala
      - copied unchanged from r732847, incubator/esme/branches/hooks-actions/server/src/main/scala/us/esme/external/RssFeed.scala
Modified:
    incubator/esme/trunk/server/src/main/scala/bootstrap/liftweb/Boot.scala
    incubator/esme/trunk/server/src/main/scala/us/esme/actor/HttpSender.scala
    incubator/esme/trunk/server/src/main/scala/us/esme/actor/MessagePullActor.scala
    incubator/esme/trunk/server/src/main/scala/us/esme/actor/UserActor.scala
    incubator/esme/trunk/server/src/main/scala/us/esme/lib/MsgParser.scala
    incubator/esme/trunk/server/src/main/scala/us/esme/model/Action.scala
    incubator/esme/trunk/server/src/main/scala/us/esme/model/Mailbox.scala
    incubator/esme/trunk/server/src/main/scala/us/esme/model/User.scala
    incubator/esme/trunk/server/src/main/scala/us/esme/view/ActionView.scala

Modified: incubator/esme/trunk/server/src/main/scala/bootstrap/liftweb/Boot.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/bootstrap/liftweb/Boot.scala?rev=735148&r1=735147&r2=735148&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/bootstrap/liftweb/Boot.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/bootstrap/liftweb/Boot.scala Fri Jan 16 13:30:22 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/trunk/server/src/main/scala/us/esme/actor/HttpSender.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/us/esme/actor/HttpSender.scala?rev=735148&r1=735147&r2=735148&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/us/esme/actor/HttpSender.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/us/esme/actor/HttpSender.scala Fri Jan 16 13:30:22 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/trunk/server/src/main/scala/us/esme/actor/MessagePullActor.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/us/esme/actor/MessagePullActor.scala?rev=735148&r1=735147&r2=735148&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/us/esme/actor/MessagePullActor.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/us/esme/actor/MessagePullActor.scala Fri Jan 16 13:30:22 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
 

Modified: incubator/esme/trunk/server/src/main/scala/us/esme/actor/UserActor.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/us/esme/actor/UserActor.scala?rev=735148&r1=735147&r2=735148&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/us/esme/actor/UserActor.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/us/esme/actor/UserActor.scala Fri Jan 16 13:30:22 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
           }
         }

Modified: incubator/esme/trunk/server/src/main/scala/us/esme/lib/MsgParser.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/us/esme/lib/MsgParser.scala?rev=735148&r1=735147&r2=735148&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/us/esme/lib/MsgParser.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/us/esme/lib/MsgParser.scala Fri Jan 16 13:30:22 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/trunk/server/src/main/scala/us/esme/model/Action.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/us/esme/model/Action.scala?rev=735148&r1=735147&r2=735148&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/us/esme/model/Action.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/us/esme/model/Action.scala Fri Jan 16 13:30:22 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/trunk/server/src/main/scala/us/esme/model/Mailbox.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/us/esme/model/Mailbox.scala?rev=735148&r1=735147&r2=735148&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/us/esme/model/Mailbox.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/us/esme/model/Mailbox.scala Fri Jan 16 13:30:22 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/trunk/server/src/main/scala/us/esme/model/User.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/us/esme/model/User.scala?rev=735148&r1=735147&r2=735148&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/us/esme/model/User.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/us/esme/model/User.scala Fri Jan 16 13:30:22 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/trunk/server/src/main/scala/us/esme/view/ActionView.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/us/esme/view/ActionView.scala?rev=735148&r1=735147&r2=735148&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/us/esme/view/ActionView.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/us/esme/view/ActionView.scala Fri Jan 16 13:30:22 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 &amp; 50% -- half the time, something sent by @foo
+                  @foo &amp; 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" />