Return-Path: Delivered-To: apmail-incubator-esme-commits-archive@locus.apache.org Received: (qmail 17064 invoked from network); 16 Jan 2009 21:30:51 -0000 Received: from hermes.apache.org (HELO mail.apache.org) (140.211.11.2) by minotaur.apache.org with SMTP; 16 Jan 2009 21:30:51 -0000 Received: (qmail 52569 invoked by uid 500); 16 Jan 2009 21:30:51 -0000 Delivered-To: apmail-incubator-esme-commits-archive@incubator.apache.org Received: (qmail 52547 invoked by uid 500); 16 Jan 2009 21:30:51 -0000 Mailing-List: contact esme-commits-help@incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: esme-dev@incubator.apache.org Delivered-To: mailing list esme-commits@incubator.apache.org Received: (qmail 52536 invoked by uid 99); 16 Jan 2009 21:30:51 -0000 Received: from athena.apache.org (HELO athena.apache.org) (140.211.11.136) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 16 Jan 2009 13:30:51 -0800 X-ASF-Spam-Status: No, hits=-2000.0 required=10.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 16 Jan 2009 21:30:43 +0000 Received: by eris.apache.org (Postfix, from userid 65534) id AAA902388882; Fri, 16 Jan 2009 13:30:23 -0800 (PST) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: 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/ Date: Fri, 16 Jan 2009 21:30:23 -0000 To: esme-commits@incubator.apache.org From: vdichev@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20090116213023.AAA902388882@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org 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
#moo -- contains the #moo tag
50% -- success 50% of the time
- @foo & 50% -- half the time, something sent by @foo + @foo & 50% -- half the time, something sent by @foo
+ login -- user has logged in
+ followed -- user is being followed
+ unfollowed -- user is being unfollowed
+ profile -- user changed profile
+ every N mins -- repeat action, N is an integer @@ -149,7 +154,9 @@ filter -- not put in your timeline
resend -- sends the message to all your followers
mailto:foo@bar.com -- sends the message to foo@bar.com
- http://foo.com/message/in -- makes an HTTP post with the message + http://foo.com/message/in -- HTTP post, %s expands to message
+ atom:http://blog.com/feed.atom -- posts new messages from Atom feed
+ rss:http://blog.com/feed.rss -- posts new messages from RSS feed