|
1 #!/usr/bin/env python |
|
2 # |
|
3 # Copyright 2007 Google Inc. |
|
4 # |
|
5 # Licensed under the Apache License, Version 2.0 (the "License"); |
|
6 # you may not use this file except in compliance with the License. |
|
7 # You may obtain a copy of the License at |
|
8 # |
|
9 # http://www.apache.org/licenses/LICENSE-2.0 |
|
10 # |
|
11 # Unless required by applicable law or agreed to in writing, software |
|
12 # distributed under the License is distributed on an "AS IS" BASIS, |
|
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
14 # See the License for the specific language governing permissions and |
|
15 # limitations under the License. |
|
16 # |
|
17 |
|
18 """XMPP API. |
|
19 |
|
20 This module allows AppEngine apps to interact with a bot representing that app |
|
21 on the Google Talk network. |
|
22 |
|
23 Functions defined in this module: |
|
24 get_presence: Gets the presence for a JID. |
|
25 send_message: Sends a chat message to any number of JIDs. |
|
26 send_invite: Sends an invitation to chat to a JID. |
|
27 |
|
28 Classes defined in this module: |
|
29 Message: A class to encapsulate received messages. |
|
30 """ |
|
31 |
|
32 |
|
33 |
|
34 from google.appengine.api import apiproxy_stub_map |
|
35 from google.appengine.api.xmpp import xmpp_service_pb |
|
36 from google.appengine.runtime import apiproxy_errors |
|
37 |
|
38 |
|
39 NO_ERROR = xmpp_service_pb.XmppMessageResponse.NO_ERROR |
|
40 INVALID_JID = xmpp_service_pb.XmppMessageResponse.INVALID_JID |
|
41 OTHER_ERROR = xmpp_service_pb.XmppMessageResponse.OTHER_ERROR |
|
42 |
|
43 |
|
44 MESSAGE_TYPE_NONE = "" |
|
45 MESSAGE_TYPE_CHAT = "chat" |
|
46 MESSAGE_TYPE_ERROR = "error" |
|
47 MESSAGE_TYPE_GROUPCHAT = "groupchat" |
|
48 MESSAGE_TYPE_HEADLINE = "headline" |
|
49 MESSAGE_TYPE_NORMAL = "normal" |
|
50 |
|
51 _VALID_MESSAGE_TYPES = frozenset([MESSAGE_TYPE_NONE, MESSAGE_TYPE_CHAT, |
|
52 MESSAGE_TYPE_ERROR, MESSAGE_TYPE_GROUPCHAT, |
|
53 MESSAGE_TYPE_HEADLINE, MESSAGE_TYPE_NORMAL]) |
|
54 |
|
55 |
|
56 class Error(Exception): |
|
57 """Base error class for this module.""" |
|
58 |
|
59 |
|
60 class InvalidJidError(Error): |
|
61 """Error that indicates a request for an invalid JID.""" |
|
62 |
|
63 |
|
64 class InvalidTypeError(Error): |
|
65 """Error that indicates a send message request has an invalid type.""" |
|
66 |
|
67 |
|
68 class InvalidXmlError(Error): |
|
69 """Error that indicates a send message request has invalid XML.""" |
|
70 |
|
71 |
|
72 class NoBodyError(Error): |
|
73 """Error that indicates a send message request has no body.""" |
|
74 |
|
75 |
|
76 class InvalidMessageError(Error): |
|
77 """Error that indicates a received message was invalid or incomplete.""" |
|
78 |
|
79 |
|
80 def get_presence(jid, from_jid=None): |
|
81 """Gets the presence for a JID. |
|
82 |
|
83 Args: |
|
84 jid: The JID of the contact whose presence is requested. |
|
85 from_jid: The optional custom JID to use for sending. Currently, the default |
|
86 is <appid>@appspot.com. This is supported as a value. Custom JIDs can be |
|
87 of the form <anything>@<appid>.appspotchat.com. |
|
88 |
|
89 Returns: |
|
90 bool, Whether the user is online. |
|
91 |
|
92 Raises: |
|
93 InvalidJidError if any of the JIDs passed are invalid. |
|
94 Error if an unspecified error happens processing the request. |
|
95 """ |
|
96 if not jid: |
|
97 raise InvalidJidError() |
|
98 |
|
99 request = xmpp_service_pb.PresenceRequest() |
|
100 response = xmpp_service_pb.PresenceResponse() |
|
101 |
|
102 request.set_jid(_to_str(jid)) |
|
103 if from_jid: |
|
104 request.set_from_jid(_to_str(from_jid)) |
|
105 |
|
106 try: |
|
107 apiproxy_stub_map.MakeSyncCall("xmpp", |
|
108 "GetPresence", |
|
109 request, |
|
110 response) |
|
111 except apiproxy_errors.ApplicationError, e: |
|
112 if (e.application_error == |
|
113 xmpp_service_pb.XmppServiceError.INVALID_JID): |
|
114 raise InvalidJidError() |
|
115 else: |
|
116 raise Error() |
|
117 |
|
118 return bool(response.is_available()) |
|
119 |
|
120 |
|
121 def send_invite(jid, from_jid=None): |
|
122 """Sends an invitation to chat to a JID. |
|
123 |
|
124 Args: |
|
125 jid: The JID of the contact to invite. |
|
126 from_jid: The optional custom JID to use for sending. Currently, the default |
|
127 is <appid>@appspot.com. This is supported as a value. Custom JIDs can be |
|
128 of the form <anything>@<appid>.appspotchat.com. |
|
129 |
|
130 Raises: |
|
131 InvalidJidError if the JID passed is invalid. |
|
132 Error if an unspecified error happens processing the request. |
|
133 """ |
|
134 if not jid: |
|
135 raise InvalidJidError() |
|
136 |
|
137 request = xmpp_service_pb.XmppInviteRequest() |
|
138 response = xmpp_service_pb.XmppInviteResponse() |
|
139 |
|
140 request.set_jid(_to_str(jid)) |
|
141 if from_jid: |
|
142 request.set_from_jid(_to_str(from_jid)) |
|
143 |
|
144 try: |
|
145 apiproxy_stub_map.MakeSyncCall("xmpp", |
|
146 "SendInvite", |
|
147 request, |
|
148 response) |
|
149 except apiproxy_errors.ApplicationError, e: |
|
150 if (e.application_error == |
|
151 xmpp_service_pb.XmppServiceError.INVALID_JID): |
|
152 raise InvalidJidError() |
|
153 else: |
|
154 raise Error() |
|
155 |
|
156 return |
|
157 |
|
158 |
|
159 def send_message(jids, body, from_jid=None, message_type=MESSAGE_TYPE_CHAT, |
|
160 raw_xml=False): |
|
161 """Sends a chat message to a list of JIDs. |
|
162 |
|
163 Args: |
|
164 jids: A list of JIDs to send the message to, or a single JID to send the |
|
165 message to. |
|
166 from_jid: The optional custom JID to use for sending. Currently, the default |
|
167 is <appid>@appspot.com. This is supported as a value. Custom JIDs can be |
|
168 of the form <anything>@<appid>.appspotchat.com. |
|
169 body: The body of the message. |
|
170 message_type: Optional type of the message. Should be one of the types |
|
171 specified in RFC 3921, section 2.1.1. An empty string will result in a |
|
172 message stanza without a type attribute. For convenience, all of the |
|
173 valid types are in the MESSAGE_TYPE_* constants in this file. The |
|
174 default is MESSAGE_TYPE_CHAT. Anything else will throw an exception. |
|
175 raw_xml: Optionally specifies that the body should be interpreted as XML. If |
|
176 this is false, the contents of the body will be escaped and placed inside |
|
177 of a body element inside of the message. If this is true, the contents |
|
178 will be made children of the message. |
|
179 |
|
180 Returns: |
|
181 list, A list of statuses, one for each JID, corresponding to the result of |
|
182 sending the message to that JID. Or, if a single JID was passed in, |
|
183 returns the status directly. |
|
184 |
|
185 Raises: |
|
186 InvalidJidError if there is no valid JID in the list. |
|
187 InvalidTypeError if the type argument is invalid. |
|
188 InvalidXmlError if the body is malformed XML and raw_xml is True. |
|
189 NoBodyError if there is no body. |
|
190 Error if another error occurs processing the request. |
|
191 """ |
|
192 request = xmpp_service_pb.XmppMessageRequest() |
|
193 response = xmpp_service_pb.XmppMessageResponse() |
|
194 |
|
195 if not body: |
|
196 raise NoBodyError() |
|
197 |
|
198 if not jids: |
|
199 raise InvalidJidError() |
|
200 |
|
201 if not message_type in _VALID_MESSAGE_TYPES: |
|
202 raise InvalidTypeError() |
|
203 |
|
204 single_jid = False |
|
205 if isinstance(jids, basestring): |
|
206 single_jid = True |
|
207 jids = [jids] |
|
208 |
|
209 for jid in jids: |
|
210 if not jid: |
|
211 raise InvalidJidError() |
|
212 request.add_jid(_to_str(jid)) |
|
213 |
|
214 request.set_body(_to_str(body)) |
|
215 request.set_type(_to_str(message_type)) |
|
216 request.set_raw_xml(raw_xml) |
|
217 if from_jid: |
|
218 request.set_from_jid(_to_str(from_jid)) |
|
219 |
|
220 try: |
|
221 apiproxy_stub_map.MakeSyncCall("xmpp", |
|
222 "SendMessage", |
|
223 request, |
|
224 response) |
|
225 except apiproxy_errors.ApplicationError, e: |
|
226 if (e.application_error == |
|
227 xmpp_service_pb.XmppServiceError.INVALID_JID): |
|
228 raise InvalidJidError() |
|
229 elif (e.application_error == |
|
230 xmpp_service_pb.XmppServiceError.INVALID_TYPE): |
|
231 raise InvalidTypeError() |
|
232 elif (e.application_error == |
|
233 xmpp_service_pb.XmppServiceError.INVALID_XML): |
|
234 raise InvalidXmlError() |
|
235 elif (e.application_error == |
|
236 xmpp_service_pb.XmppServiceError.NO_BODY): |
|
237 raise NoBodyError() |
|
238 raise Error() |
|
239 |
|
240 if single_jid: |
|
241 return response.status_list()[0] |
|
242 return response.status_list() |
|
243 |
|
244 |
|
245 class Message(object): |
|
246 """Encapsulates an XMPP message received by the application.""" |
|
247 |
|
248 def __init__(self, vars): |
|
249 """Constructs a new XMPP Message from an HTTP request. |
|
250 |
|
251 Args: |
|
252 vars: A dict-like object to extract message arguments from. |
|
253 """ |
|
254 try: |
|
255 self.__sender = vars["from"] |
|
256 self.__to = vars["to"] |
|
257 self.__body = vars["body"] |
|
258 except KeyError, e: |
|
259 raise InvalidMessageError(e[0]) |
|
260 self.__command = None |
|
261 self.__arg = None |
|
262 |
|
263 @property |
|
264 def sender(self): |
|
265 return self.__sender |
|
266 |
|
267 @property |
|
268 def to(self): |
|
269 return self.__to |
|
270 |
|
271 @property |
|
272 def body(self): |
|
273 return self.__body |
|
274 |
|
275 def __parse_command(self): |
|
276 if self.__arg != None: |
|
277 return |
|
278 |
|
279 body = self.__body |
|
280 if body.startswith('\\'): |
|
281 body = '/' + body[1:] |
|
282 |
|
283 self.__arg = '' |
|
284 if body.startswith('/'): |
|
285 parts = body.split(' ', 1) |
|
286 self.__command = parts[0][1:] |
|
287 if len(parts) > 1: |
|
288 self.__arg = parts[1].strip() |
|
289 else: |
|
290 self.__arg = self.__body.strip() |
|
291 |
|
292 @property |
|
293 def command(self): |
|
294 self.__parse_command() |
|
295 return self.__command |
|
296 |
|
297 @property |
|
298 def arg(self): |
|
299 self.__parse_command() |
|
300 return self.__arg |
|
301 |
|
302 def reply(self, body, message_type=MESSAGE_TYPE_CHAT, raw_xml=False, |
|
303 send_message=send_message): |
|
304 """Convenience function to reply to a message. |
|
305 |
|
306 Args: |
|
307 body: str: The body of the message |
|
308 message_type, raw_xml: As per send_message. |
|
309 send_message: Used for testing. |
|
310 |
|
311 Returns: |
|
312 A status code as per send_message. |
|
313 |
|
314 Raises: |
|
315 See send_message. |
|
316 """ |
|
317 return send_message([self.sender], body, from_jid=self.to, |
|
318 message_type=message_type, raw_xml=raw_xml) |
|
319 |
|
320 |
|
321 def _to_str(value): |
|
322 """Helper function to make sure unicode values converted to utf-8 |
|
323 |
|
324 Args: |
|
325 value: str or unicode to convert to utf-8. |
|
326 |
|
327 Returns: |
|
328 UTF-8 encoded str of value, otherwise value unchanged. |
|
329 """ |
|
330 if isinstance(value, unicode): |
|
331 return value.encode('utf-8') |
|
332 return value |