Mercurial > eldonilo > avatar
annotate avatar.js @ 8:e0cd5ede76af
Better documentation.
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Thu, 19 May 2011 20:52:34 +0200 |
parents | 51cda3a6e1c3 |
children | 9360b78c7a5b |
rev | line source |
---|---|
0 | 1 #!/usr/bin/env node |
2 | |
3 /* | |
4 * Copyright (C) 2011 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> | |
5 * | |
6 * This file is the source code of an XMPP avatar retriever. | |
7 * | |
8 * This program is free software: you can redistribute it and/or modify | |
9 * it under the terms of the GNU Affero General Public License as | |
10 * published by the Free Software Foundation, version 3 of the License. | |
11 * | |
12 * This program is distributed in the hope that it will be useful, | |
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 * GNU Affero General Public License for more details. | |
16 * | |
17 * You should have received a copy of the GNU Affero General Public License | |
18 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 */ | |
20 | |
21 'use strict'; | |
22 | |
23 var config = require('./configuration'); | |
24 | |
25 try { | |
26 var xmpp = require('node-xmpp'); | |
4
8acaa0a575c7
Allow configuration of the host.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
3
diff
changeset
|
27 var conn = new xmpp.Component(config); |
0 | 28 |
29 conn.on('stanza', function (stanza) { | |
30 if (stanza.is('iq')) | |
31 onIq(stanza); | |
32 else | |
33 onError(stanza); | |
34 }); | |
35 | |
36 conn._uniqueId = 42; | |
37 conn.getUniqueId = function(suffix) { | |
38 return ++this._uniqueId + (suffix?(":"+suffix):""); | |
39 }; | |
40 | |
41 var Element = require('ltx').Element; | |
42 } catch (e) { | |
43 var xmpp = require('xmpp'); | |
4
8acaa0a575c7
Allow configuration of the host.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
3
diff
changeset
|
44 var conn = new xmpp.Connection(config.host, config.port); |
0 | 45 |
46 conn.log = function (_, m) { console.log(m); }; | |
47 | |
48 conn.connect(config.jid, config.password, function (status, condition) { | |
49 if(status == xmpp.Status.CONNECTED) { | |
50 conn.addHandler(onIq, null, 'iq', null, null, null); | |
51 conn.addHandler(onError, null, 'message', null, null, null); | |
52 conn.addHandler(onError, null, 'presence', null, null, null); | |
53 } else | |
54 conn.log(xmpp.LogLevel.DEBUG, "New connection status: " + status + (condition?(" ("+condition+")"):"")); | |
55 }); | |
56 | |
57 xmpp.StanzaBuilder.prototype.cnode = function (stanza) | |
58 { | |
59 var parent = this.last_node[this.last_node.length-1]; | |
60 parent.tags.push(stanza); | |
61 parent.children.push(stanza); | |
62 this.last_node.push(stanza); | |
63 return this; | |
64 }; | |
65 | |
66 var Element = xmpp.StanzaBuilder; | |
67 } | |
68 | |
69 var fs = require('fs'); | |
70 var http = require('http'); | |
71 | |
72 process.addListener('uncaughtException', function (err) { | |
73 console.log('\x1b[41;1mUncaught exception (' + err + '), this should never happen:\x1b[0m\n' + err.stack); | |
74 }); | |
75 | |
76 var extensions = { | |
77 png: 'image/png', | |
78 svg: 'image/svg+xml', | |
79 jpg: 'image/jpeg', | |
80 gif: 'image/gif' | |
81 } | |
82 | |
83 var jids = {}; | |
84 | |
85 var sent = {}; | |
86 | |
2
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
87 var svgError = function(res, message) { |
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
88 res.writeHead(500, {'Content-Type': 'image/svg+xml'}); |
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
89 res.write('<?xml version="1.0" encoding="UTF-8"?>\n'); |
6
3b799c33ab16
More avatar-like error.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
5
diff
changeset
|
90 res.write('<svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 -36 64 64">\n'); |
3b799c33ab16
More avatar-like error.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
5
diff
changeset
|
91 res.write('\t<desc>'+message+'</desc>\n'); |
3b799c33ab16
More avatar-like error.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
5
diff
changeset
|
92 res.write('\t<rect x="-32" y="-36" width="64" height="64" fill="white"/>\n'); |
3b799c33ab16
More avatar-like error.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
5
diff
changeset
|
93 res.write('\t<text font-family="sans-serif" font-weight="bold" text-anchor="middle">Error</text>\n'); |
2
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
94 res.end('</svg>\n'); |
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
95 } |
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
96 |
0 | 97 var makeError = function(response) { |
98 response.attr.type = 'error'; | |
99 | |
100 var error = new Element('error', {type: 'cancel'}); | |
101 error.c('feature-not-implemented', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up(); | |
102 response.cnode(error); | |
103 | |
104 return response; | |
105 } | |
106 | |
107 function onIq(stanza) { | |
108 var type = stanza.getAttribute('type'); | |
109 var from = stanza.getAttribute('to'); | |
110 var to = stanza.getAttribute('from'); | |
111 var id = stanza.getAttribute('id'); | |
112 | |
113 var response; | |
114 if (id) | |
115 response = new Element('iq', {to: to, from: from, type: 'result', id: id}); | |
116 else | |
117 response = new Element('iq', {to: to, from: from, type: 'result'}); | |
118 | |
119 if (!sent[id]) { | |
120 conn.send(makeError(response)); | |
121 return; | |
122 } | |
123 | |
124 var res = sent[id]; | |
125 delete sent[id]; | |
126 | |
127 if (type == 'error') { | |
128 try { | |
129 var err = stanza.getChild('error').getChild().name; | |
130 } catch (e) { | |
131 var err = 'none'; | |
132 } | |
2
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
133 svgError(res, 'Error during query of this user’s vCard: “'+err+'”.'); |
0 | 134 return; |
135 } | |
136 | |
137 var vCard = stanza.getChild('vCard', 'vcard-temp'); | |
138 if (!vCard) { | |
2
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
139 svgError(res, 'Error: this user doesn’t have a vCard.'); |
0 | 140 return; |
141 } | |
142 | |
143 try { | |
144 var photo = vCard.getChild('PHOTO', 'vcard-temp'); | |
145 var base64 = photo.getChild('BINVAL', 'vcard-temp').getText(); | |
146 | |
1
0c19fadfc12d
Serve the right error message when the TYPE of the PHOTO is missing.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
0
diff
changeset
|
147 try { |
0c19fadfc12d
Serve the right error message when the TYPE of the PHOTO is missing.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
0
diff
changeset
|
148 var type = photo.getChild('TYPE', 'vcard-temp').getText(); |
0c19fadfc12d
Serve the right error message when the TYPE of the PHOTO is missing.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
0
diff
changeset
|
149 } catch (e) { |
5
9b2f17ea1594
Add an option to guess the type of an avatar even if it is unspecified.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
4
diff
changeset
|
150 if (config.guessType) |
9b2f17ea1594
Add an option to guess the type of an avatar even if it is unspecified.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
4
diff
changeset
|
151 type = 'image/png'; // FIXME: use magic. |
9b2f17ea1594
Add an option to guess the type of an avatar even if it is unspecified.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
4
diff
changeset
|
152 else { |
9b2f17ea1594
Add an option to guess the type of an avatar even if it is unspecified.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
4
diff
changeset
|
153 svgError(res, 'Error: this user’s vCard doesn’t specify the MIME type of its avatar.'); |
9b2f17ea1594
Add an option to guess the type of an avatar even if it is unspecified.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
4
diff
changeset
|
154 return; |
9b2f17ea1594
Add an option to guess the type of an avatar even if it is unspecified.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
4
diff
changeset
|
155 } |
1
0c19fadfc12d
Serve the right error message when the TYPE of the PHOTO is missing.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
0
diff
changeset
|
156 } |
0c19fadfc12d
Serve the right error message when the TYPE of the PHOTO is missing.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
0
diff
changeset
|
157 |
0 | 158 var ext; |
159 for (var i in extensions) | |
160 if (type == extensions[i]) | |
161 ext = i; | |
162 | |
5
9b2f17ea1594
Add an option to guess the type of an avatar even if it is unspecified.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
4
diff
changeset
|
163 // Here we don’t try to guess the extension even if the option is set. |
0 | 164 if (ext === undefined) { |
5
9b2f17ea1594
Add an option to guess the type of an avatar even if it is unspecified.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
4
diff
changeset
|
165 console.log('Unknown MIME type: '+type); |
2
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
166 svgError(res, 'Error: this user’s avatar is in an unknown format.'); |
0 | 167 return; |
168 } | |
169 | |
170 var binval = new Buffer(base64.replace(/\n/g, ''), 'base64'); | |
171 | |
172 fs.writeFile(config.directory+'/'+to+'.'+ext, binval, function() { | |
173 jids[to] = ext; | |
174 showImage(to, res); | |
175 }); | |
176 } catch (e) { | |
2
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
177 svgError(res, 'Error: this user doesn’t have an avatar in his/her vCard.'); |
0 | 178 } |
179 } | |
180 | |
181 function onError(stanza) { | |
182 if (stanza.getAttribute('type') == 'error') | |
183 return; | |
184 | |
185 var from = stanza.getAttribute('to'); | |
186 var to = stanza.getAttribute('from'); | |
187 var id = stanza.getAttribute('id'); | |
188 | |
189 var response; | |
190 if (id) | |
191 response = new Element(stanza.name, {to: to, from: from, id: id}); | |
192 else | |
193 response = new Element(stanza.name, {to: to, from: from}); | |
194 | |
195 conn.send(makeError(response)); | |
196 } | |
197 | |
3
805f24754ff1
Don’t delete avatar on error, only allow update.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
2
diff
changeset
|
198 var getVCard = function(jid, res) { |
0 | 199 var id = conn.getUniqueId(); |
200 | |
201 var toSend = new Element('iq', {to: jid, from: config.jid, type: 'get', id: id}) | |
202 .c('vCard', {xmlns: 'vcard-temp'}); | |
203 | |
204 conn.send(toSend); | |
205 | |
3
805f24754ff1
Don’t delete avatar on error, only allow update.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
2
diff
changeset
|
206 sent[id] = res; |
0 | 207 } |
208 | |
209 var showImage = function(jid, res) { | |
210 var extension = jids[jid]; | |
211 var file = config.directory+'/'+jid+'.'+extension; | |
212 res.writeHead(200, {'Content-Type': extensions[extension]}); | |
213 fs.readFile(file, function(err, data) { | |
214 res.end(data); | |
215 }); | |
216 fs.stat(file, function(err, stats) { | |
217 if (err) { | |
218 console.log('Error when stat on “'+file+'”.'); | |
219 return; | |
220 } | |
221 | |
222 var last = new Date(stats.mtime); | |
223 var now = new Date(); | |
224 | |
3
805f24754ff1
Don’t delete avatar on error, only allow update.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
2
diff
changeset
|
225 if (now - last > 24*60*60*1000) |
805f24754ff1
Don’t delete avatar on error, only allow update.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
2
diff
changeset
|
226 getVCard(jid, res); |
0 | 227 }); |
228 return; | |
229 } | |
230 | |
231 fs.readdir('data', function(err, files) { | |
232 if (err) | |
233 process.exit('1'); | |
234 | |
2
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
235 for (var i in files) { |
0 | 236 var tab = /(.*)\.([a-z]{3})/.exec(files[i]); |
237 jids[tab[1]] = tab[2]; | |
238 } | |
239 }); | |
240 | |
241 http.createServer(function (req, res) { | |
7
51cda3a6e1c3
Better handling of proxy connections; add an option to specify the root on the webserver.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
6
diff
changeset
|
242 console.log('Connection from ' + (req.headers['x-forwarded-for'] || req.client.remoteAddress) + ' (' + req.headers['user-agent'] + ') to get “' + req.url + '”.'); |
0 | 243 |
244 var easterEggs = { | |
245 source: { | |
7
51cda3a6e1c3
Better handling of proxy connections; add an option to specify the root on the webserver.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
6
diff
changeset
|
246 re: new RegExp(config.webRoot + 'source/code$'), |
0 | 247 file: process.argv[1], |
248 mime: 'application/ecmascript', | |
249 error: 'source code unavailable! oO' | |
250 }, | |
251 README: {}, | |
252 COPYING: {}, | |
253 }; | |
254 | |
255 req.setEncoding('utf-8'); | |
256 | |
257 for (var i in easterEggs) { | |
258 var ee = easterEggs[i]; | |
259 var file = ee.file || i; | |
7
51cda3a6e1c3
Better handling of proxy connections; add an option to specify the root on the webserver.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
6
diff
changeset
|
260 var re = ee.re || new RegExp(config.webRoot + file + '$'); |
0 | 261 if (re.test(req.url)) { |
262 fs.readFile(file, function(err, content) { | |
263 if (err) { | |
2
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
264 svgError(res, 'Error: ' + (ee.error || file + ' unavailable.')); |
0 | 265 return; |
266 } | |
267 res.writeHead(200, {'Content-Type': ee.mime || 'text/plain'}); | |
268 res.end(content); | |
269 }); | |
270 return; | |
271 } | |
272 } | |
273 | |
7
51cda3a6e1c3
Better handling of proxy connections; add an option to specify the root on the webserver.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
6
diff
changeset
|
274 var jid = unescape(req.url.replace(new RegExp(config.webRoot), '')); |
0 | 275 |
276 if (jid === '') { | |
277 res.writeHead(200, {'Content-Type': 'application/xhtml+xml'}); | |
278 res.write('<?xml version="1.0" encoding="utf-8"?>\n'); | |
279 res.write('<!DOCTYPE html>\n'); | |
280 res.write('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n'); | |
281 res.write('\t<head>\n'); | |
282 res.write('\t\t<title>JavaScript XMPP Avatar Retriever</title>\n'); | |
283 res.write('\t</head>\n'); | |
284 res.write('\t<body>\n'); | |
285 res.write('\t\t<header><h1>JavaScript XMPP Avatar Retriever</h1></header>\n'); | |
286 res.write('\t\t<p>Put any JID and get its avatar. :)</p>\n'); | |
287 res.write('\t\t<form action="redirect" method="post">\n'); | |
288 res.write('\t\t\t<p>\n'); | |
289 res.write('\t\t\t\t<input name="jid" type="text" placeholder="you@yourserver.tld"/>\n'); | |
290 res.write('\t\t\t\t<input type="submit"/>\n'); | |
291 res.write('\t\t\t</p>\n'); | |
292 res.write('\t\t</form>\n'); | |
2
ad496e8a5e66
Use an image for errors instead of text (unusable in <img/>).
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
293 res.write('\t\t<footer><p>(<a href="README">README</a>, <a href="source/code">source code</a>, <a href="http://hg.linkmauve.fr/avatar">Mercurial repository</a>)</p></footer>\n'); |
0 | 294 res.write('\t</body>\n'); |
295 res.end('</html>\n'); | |
296 return; | |
297 } | |
298 | |
299 if (jid === 'redirect') { | |
300 if (req.method !== 'POST') { | |
301 res.writeHead(404, {'Content-Type': 'text/plain'}); | |
302 res.end('Error: redirect unavailable.'); | |
303 return; | |
304 } | |
305 | |
306 req.on('data', function(content) { | |
307 console.log(content); | |
308 var jid = unescape(content.toString()).replace(/^jid=/, ''); | |
309 res.writeHead(301, {'Location': jid}); | |
310 res.end(); | |
311 }); | |
312 return; | |
313 } | |
314 | |
315 if (jid in jids) { | |
316 showImage(jid, res); | |
317 return; | |
318 } | |
319 | |
3
805f24754ff1
Don’t delete avatar on error, only allow update.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
2
diff
changeset
|
320 getVCard(jid, res); |
8
e0cd5ede76af
Better documentation.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
7
diff
changeset
|
321 }).listen(config.webPort); |