1 # Copyright 2009 Google Inc.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
19 import wsgiref
.handlers
20 from google
.appengine
.api
import xmpp
21 from google
.appengine
.api
import users
22 from google
.appengine
.ext
import db
23 from google
.appengine
.ext
import webapp
24 from google
.appengine
.ext
.ereporter
import report_generator
25 from google
.appengine
.ext
.webapp
import template
26 from google
.appengine
.ext
.webapp
import xmpp_handlers
29 PONDER_MSG
= "Hmm. Let me think on that a bit."
30 TELLME_MSG
= "While I'm thinking, perhaps you can answer me this: %s"
31 SOMEONE_ANSWERED_MSG
= ("We seek those who are wise and fast. One out of two "
32 "is not enough. Another has answered my question.")
33 ANSWER_INTRO_MSG
= "You asked me: %s"
34 ANSWER_MSG
= "I have thought long and hard, and concluded: %s"
35 WAIT_MSG
= ("Please! One question at a time! You can ask me another once you "
36 "have an answer to your current question.")
37 THANKS_MSG
= "Thank you for your wisdom."
38 TELLME_THANKS_MSG
= ("Thank you for your wisdom."
39 " I'm still thinking about your question.")
40 EMPTYQ_MSG
= "Sorry, I don't have anything to ask you at the moment."
41 HELP_MSG
= ("I am the amazing Crowd Guru. Ask me a question by typing '/tellme "
42 "the meaning of life', and I will answer you forthwith! To learn "
47 class Question(db
.Model
):
48 question
= db
.TextProperty(required
=True)
49 asker
= db
.IMProperty(required
=True)
50 asked
= db
.DateTimeProperty(required
=True, auto_now_add
=True)
52 assignees
= db
.ListProperty(db
.IM
)
53 last_assigned
= db
.DateTimeProperty()
55 answer
= db
.TextProperty()
56 answerer
= db
.IMProperty()
57 answered
= db
.DateTimeProperty()
60 def _tryAssignTx(key
, user
, expiry
):
61 """Assigns and returns the question if it's not assigned already.
64 key: db.Key: The key of a Question to try and assign.
65 user: db.IM: The user to assign the question to.
67 The Question object. If it was already assigned, no change is made
69 question
= Question
.get(key
)
70 if not question
.last_assigned
or question
.last_assigned
< expiry
:
71 question
.assignees
.append(user
)
72 question
.last_assigned
= datetime
.datetime
.now()
77 def assignQuestion(user
):
78 """Gets an unanswered question and assigns it to a user to answer.
81 user: db.IM: The identity of the user to assign a question to.
83 The Question entity assigned to the user, or None if there are no
87 while question
== None or user
not in question
.assignees
:
88 # Assignments made before this timestamp have expired.
89 expiry
= (datetime
.datetime
.now()
90 - datetime
.timedelta(seconds
=MAX_ANSWER_TIME
))
92 # Find a candidate question
94 q
.filter("answerer =", None)
95 q
.filter("last_assigned <", expiry
).order("last_assigned")
96 # If a question has never been assigned, order by when it was asked
98 candidates
= [x
for x
in q
.fetch(2) if x
.asker
!= user
]
100 # No valid questions in queue.
104 question
= db
.run_in_transaction(Question
._tryAssignTx
,
105 candidates
[0].key(), user
, expiry
)
107 # Expire the assignment after a couple of minutes
110 def _unassignTx(self
, user
):
111 question
= Question
.get(self
.key())
112 if user
in question
.assignees
:
113 question
.assignees
.remove(user
)
116 def unassign(self
, user
):
117 """Unassigns the given user to this question.
120 user: db.IM: The user who will no longer be answering this question.
122 db
.run_in_transaction(self
._unassignTx
, user
)
125 class XmppHandler(xmpp_handlers
.CommandHandler
):
126 """Handler class for all XMPP activity."""
128 def _GetAsked(self
, user
):
129 """Returns the user's outstanding asked question, if any."""
131 q
.filter("asker =", user
)
132 q
.filter("answer =", None)
135 def _GetAnswering(self
, user
):
136 """Returns the question the user is answering, if any."""
138 q
.filter("assignees =", user
)
139 q
.filter("answer =", None)
142 def unhandled_command(self
, message
=None):
144 message
.reply(HELP_MSG
% (self
.request
.host_url
,))
146 def askme_command(self
, message
=None):
147 im_from
= db
.IM("xmpp", message
.sender
)
148 currently_answering
= self
._GetAnswering
(im_from
)
149 question
= Question
.assignQuestion(im_from
)
151 message
.reply(TELLME_MSG
% (question
.question
,))
153 message
.reply(EMPTYQ_MSG
)
154 # Don't unassign their current question until we've picked a new one.
155 if currently_answering
:
156 currently_answering
.unassign(im_from
)
158 def text_message(self
, message
=None):
159 im_from
= db
.IM("xmpp", message
.sender
)
160 question
= self
._GetAnswering
(im_from
)
162 other_assignees
= question
.assignees
163 other_assignees
.remove(im_from
)
165 # Answering a question
166 question
.answer
= message
.arg
167 question
.answerer
= im_from
168 question
.assignees
= []
169 question
.answered
= datetime
.datetime
.now()
172 # Send the answer to the asker
173 xmpp
.send_message([question
.asker
.address
],
174 ANSWER_INTRO_MSG
% (question
.question
,))
175 xmpp
.send_message([question
.asker
.address
], ANSWER_MSG
% (message
.arg
,))
177 # Send acknowledgement to the answerer
178 asked_question
= self
._GetAsked
(im_from
)
180 message
.reply(TELLME_THANKS_MSG
)
182 message
.reply(THANKS_MSG
)
184 # Tell any other assignees their help is no longer required
186 xmpp
.send_message([x
.address
for x
in other_assignees
],
187 SOMEONE_ANSWERED_MSG
)
189 self
.unhandled_command(message
)
191 def tellme_command(self
, message
=None):
192 im_from
= db
.IM("xmpp", message
.sender
)
193 asked_question
= self
._GetAsked
(im_from
)
194 currently_answering
= self
._GetAnswering
(im_from
)
197 # Already have a question
198 message
.reply(WAIT_MSG
)
201 asked_question
= Question(question
=message
.arg
, asker
=im_from
)
204 if not currently_answering
:
205 # Try and find one for them to answer
206 question
= Question
.assignQuestion(im_from
)
208 message
.reply(TELLME_MSG
% (question
.question
,))
210 message
.reply(PONDER_MSG
)
213 class LatestHandler(webapp
.RequestHandler
):
214 """Displays the most recently answered questions."""
216 def Render(self
, template_file
, template_values
):
217 path
= os
.path
.join(os
.path
.dirname(__file__
), 'templates', template_file
)
218 self
.response
.out
.write(template
.render(path
, template_values
))
221 q
= Question
.all().order('-answered').filter('answered >', None)
223 'questions': q
.fetch(20),
225 self
.Render("latest.html", template_values
)
229 app
= webapp
.WSGIApplication([
230 ('/', LatestHandler
),
231 ('/_ah/xmpp/message/chat/', XmppHandler
),
233 wsgiref
.handlers
.CGIHandler().run(app
)
236 if __name__
== '__main__':