]> www.wagner.pp.ru Git - oss/ljdump.git/blob - ljdump.py
a6326bac23474234d9b796a2498cbd8005193612
[oss/ljdump.git] / ljdump.py
1 #!/usr/bin/python
2 #
3 # ljdump.py - livejournal archiver
4 # Greg Hewgill <greg@hewgill.com> http://hewgill.com
5 # Version 1.5
6 #
7 # LICENSE
8 #
9 # This software is provided 'as-is', without any express or implied
10 # warranty.  In no event will the author be held liable for any damages
11 # arising from the use of this software.
12 #
13 # Permission is granted to anyone to use this software for any purpose,
14 # including commercial applications, and to alter it and redistribute it
15 # freely, subject to the following restrictions:
16 #
17 # 1. The origin of this software must not be misrepresented; you must not
18 #    claim that you wrote the original software. If you use this software
19 #    in a product, an acknowledgment in the product documentation would be
20 #    appreciated but is not required.
21 # 2. Altered source versions must be plainly marked as such, and must not be
22 #    misrepresented as being the original software.
23 # 3. This notice may not be removed or altered from any source distribution.
24 #
25 # Copyright (c) 2005-2009 Greg Hewgill
26
27 import codecs, os, pickle, pprint, re, shutil, sys, urllib2, xml.dom.minidom, xmlrpclib
28 from xml.sax import saxutils
29
30 MimeExtensions = {
31     "image/gif": ".gif",
32     "image/jpeg": ".jpg",
33     "image/png": ".png",
34 }
35
36 try:
37     from hashlib import md5
38 except ImportError:
39     import md5 as _md5
40     md5 = _md5.new
41
42 def calcchallenge(challenge, password):
43     return md5(challenge+md5(password).hexdigest()).hexdigest()
44
45 def flatresponse(response):
46     r = {}
47     while True:
48         name = response.readline()
49         if len(name) == 0:
50             break
51         if name[-1] == '\n':
52             name = name[:len(name)-1]
53         value = response.readline()
54         if value[-1] == '\n':
55             value = value[:len(value)-1]
56         r[name] = value
57     return r
58
59 def getljsession(server, username, password):
60     r = urllib2.urlopen(server+"/interface/flat", "mode=getchallenge")
61     response = flatresponse(r)
62     r.close()
63     r = urllib2.urlopen(server+"/interface/flat", "mode=sessiongenerate&user=%s&auth_method=challenge&auth_challenge=%s&auth_response=%s" % (username, response['challenge'], calcchallenge(response['challenge'], password)))
64     response = flatresponse(r)
65     r.close()
66     return response['ljsession']
67
68 def dochallenge(server, params, password):
69     challenge = server.LJ.XMLRPC.getchallenge()
70     params.update({
71         'auth_method': "challenge",
72         'auth_challenge': challenge['challenge'],
73         'auth_response': calcchallenge(challenge['challenge'], password)
74     })
75     return params
76
77 def dumpelement(f, name, e):
78     f.write("<%s>\n" % name)
79     for k in e.keys():
80         if isinstance(e[k], {}.__class__):
81             dumpelement(f, k, e[k])
82         else:
83             try:
84                 s = unicode(str(e[k]), "UTF-8")
85             except UnicodeDecodeError:
86                 # fall back to Latin-1 for old entries that aren't UTF-8
87                 s = unicode(str(e[k]), "cp1252")
88             f.write("<%s>%s</%s>\n" % (k, saxutils.escape(s), k))
89     f.write("</%s>\n" % name)
90
91 def writedump(fn, event):
92     f = codecs.open(fn, "w", "UTF-8")
93     f.write("""<?xml version="1.0"?>\n""")
94     dumpelement(f, "event", event)
95     f.close()
96
97 def writelast(journal, lastsync, lastmaxid):
98     f = open("%s/.last" % journal, "w")
99     f.write("%s\n" % lastsync)
100     f.write("%s\n" % lastmaxid)
101     f.close()
102
103 def createxml(doc, name, map):
104     e = doc.createElement(name)
105     for k in map.keys():
106         me = doc.createElement(k)
107         me.appendChild(doc.createTextNode(map[k]))
108         e.appendChild(me)
109     return e
110
111 def gettext(e):
112     if len(e) == 0:
113         return ""
114     return e[0].firstChild.nodeValue
115
116 def ljdump(Server, Username, Password, Journal):
117     m = re.search("(.*)/interface/xmlrpc", Server)
118     if m:
119         Server = m.group(1)
120     if Username != Journal:
121         authas = "&authas=%s" % Journal
122     else:
123         authas = ""
124
125     print "Fetching journal entries for: %s" % Journal
126     try:
127         os.mkdir(Journal)
128         print "Created subdirectory: %s" % Journal
129     except:
130         pass
131
132     ljsession = getljsession(Server, Username, Password)
133
134     server = xmlrpclib.ServerProxy(Server+"/interface/xmlrpc")
135
136     newentries = 0
137     newcomments = 0
138     errors = 0
139
140     lastsync = ""
141     lastmaxid = 0
142     try:
143         f = open("%s/.last" % Journal, "r")
144         lastsync = f.readline()
145         if lastsync[-1] == '\n':
146             lastsync = lastsync[:len(lastsync)-1]
147         lastmaxid = f.readline()
148         if len(lastmaxid) > 0 and lastmaxid[-1] == '\n':
149             lastmaxid = lastmaxid[:len(lastmaxid)-1]
150         if lastmaxid == "":
151             lastmaxid = 0
152         else:
153             lastmaxid = int(lastmaxid)
154         f.close()
155     except:
156         pass
157     origlastsync = lastsync
158
159     r = server.LJ.XMLRPC.login(dochallenge(server, {
160         'username': Username,
161         'ver': 1,
162         'getpickws': 1,
163         'getpickwurls': 1,
164     }, Password))
165     userpics = dict(zip(map(str, r['pickws']), r['pickwurls']))
166     if r['defaultpicurl']:
167         userpics['*'] = r['defaultpicurl']
168
169     while True:
170         r = server.LJ.XMLRPC.syncitems(dochallenge(server, {
171             'username': Username,
172             'ver': 1,
173             'lastsync': lastsync,
174             'usejournal': Journal,
175         }, Password))
176         #pprint.pprint(r)
177         if len(r['syncitems']) == 0:
178             break
179         for item in r['syncitems']:
180             if item['item'][0] == 'L':
181                 print "Fetching journal entry %s (%s)" % (item['item'], item['action'])
182                 try:
183                     e = server.LJ.XMLRPC.getevents(dochallenge(server, {
184                         'username': Username,
185                         'ver': 1,
186                         'selecttype': "one",
187                         'itemid': item['item'][2:],
188                         'usejournal': Journal,
189                     }, Password))
190                     if e['events']:
191                         writedump("%s/%s" % (Journal, item['item']), e['events'][0])
192                         newentries += 1
193                     else:
194                         print "Unexpected empty item: %s" % item['item']
195                         errors += 1
196                 except xmlrpclib.Fault, x:
197                     print "Error getting item: %s" % item['item']
198                     pprint.pprint(x)
199                     errors += 1
200             lastsync = item['time']
201             writelast(Journal, lastsync, lastmaxid)
202
203     # The following code doesn't work because the server rejects our repeated calls.
204     # http://www.livejournal.com/doc/server/ljp.csp.xml-rpc.getevents.html
205     # contains the statement "You should use the syncitems selecttype in
206     # conjuntions [sic] with the syncitems protocol mode", but provides
207     # no other explanation about how these two function calls should
208     # interact. Therefore we just do the above slow one-at-a-time method.
209
210     #while True:
211     #    r = server.LJ.XMLRPC.getevents(dochallenge(server, {
212     #        'username': Username,
213     #        'ver': 1,
214     #        'selecttype': "syncitems",
215     #        'lastsync': lastsync,
216     #    }, Password))
217     #    pprint.pprint(r)
218     #    if len(r['events']) == 0:
219     #        break
220     #    for item in r['events']:
221     #        writedump("%s/L-%d" % (Journal, item['itemid']), item)
222     #        newentries += 1
223     #        lastsync = item['eventtime']
224
225     print "Fetching journal comments for: %s" % Journal
226
227     try:
228         f = open("%s/comment.meta" % Journal)
229         metacache = pickle.load(f)
230         f.close()
231     except:
232         metacache = {}
233
234     try:
235         f = open("%s/user.map" % Journal)
236         usermap = pickle.load(f)
237         f.close()
238     except:
239         usermap = {}
240
241     maxid = lastmaxid
242     while True:
243         try:
244             try:
245                 r = urllib2.urlopen(urllib2.Request(Server+"/export_comments.bml?get=comment_meta&startid=%d%s" % (maxid+1, authas), headers = {'Cookie': "ljsession="+ljsession}))
246                 meta = xml.dom.minidom.parse(r)
247             except Exception, x:
248                 print "*** Error fetching comment meta, possibly not community maintainer?"
249                 print "***", x
250                 break
251         finally:
252             try:
253                 r.close()
254             except AttributeError: # r is sometimes a dict for unknown reasons
255                 pass
256         for c in meta.getElementsByTagName("comment"):
257             id = int(c.getAttribute("id"))
258             metacache[id] = {
259                 'posterid': c.getAttribute("posterid"),
260                 'state': c.getAttribute("state"),
261             }
262             if id > maxid:
263                 maxid = id
264         for u in meta.getElementsByTagName("usermap"):
265             usermap[u.getAttribute("id")] = u.getAttribute("user")
266         if maxid >= int(meta.getElementsByTagName("maxid")[0].firstChild.nodeValue):
267             break
268
269     f = open("%s/comment.meta" % Journal, "w")
270     pickle.dump(metacache, f)
271     f.close()
272
273     f = open("%s/user.map" % Journal, "w")
274     pickle.dump(usermap, f)
275     f.close()
276
277     newmaxid = maxid
278     maxid = lastmaxid
279     while True:
280         try:
281             try:
282                 r = urllib2.urlopen(urllib2.Request(Server+"/export_comments.bml?get=comment_body&startid=%d%s" % (maxid+1, authas), headers = {'Cookie': "ljsession="+ljsession}))
283                 meta = xml.dom.minidom.parse(r)
284             except Exception, x:
285                 print "*** Error fetching comment body, possibly not community maintainer?"
286                 print "***", x
287                 break
288         finally:
289             r.close()
290         for c in meta.getElementsByTagName("comment"):
291             id = int(c.getAttribute("id"))
292             jitemid = c.getAttribute("jitemid")
293             comment = {
294                 'id': str(id),
295                 'parentid': c.getAttribute("parentid"),
296                 'subject': gettext(c.getElementsByTagName("subject")),
297                 'date': gettext(c.getElementsByTagName("date")),
298                 'body': gettext(c.getElementsByTagName("body")),
299                 'state': metacache[id]['state'],
300             }
301             if usermap.has_key(c.getAttribute("posterid")):
302                 comment["user"] = usermap[c.getAttribute("posterid")]
303             try:
304                 entry = xml.dom.minidom.parse("%s/C-%s" % (Journal, jitemid))
305             except:
306                 entry = xml.dom.minidom.getDOMImplementation().createDocument(None, "comments", None)
307             found = False
308             for d in entry.getElementsByTagName("comment"):
309                 if int(d.getElementsByTagName("id")[0].firstChild.nodeValue) == id:
310                     found = True
311                     break
312             if found:
313                 print "Warning: downloaded duplicate comment id %d in jitemid %s" % (id, jitemid)
314             else:
315                 entry.documentElement.appendChild(createxml(entry, "comment", comment))
316                 f = codecs.open("%s/C-%s" % (Journal, jitemid), "w", "UTF-8")
317                 entry.writexml(f)
318                 f.close()
319                 newcomments += 1
320             if id > maxid:
321                 maxid = id
322         if maxid >= newmaxid:
323             break
324
325     lastmaxid = maxid
326
327     writelast(Journal, lastsync, lastmaxid)
328
329     if Username == Journal:
330         print "Fetching userpics for: %s" % Username
331         f = open("%s/userpics.xml" % Username, "w")
332         print >>f, """<?xml version="1.0"?>"""
333         print >>f, "<userpics>"
334         for p in userpics:
335             print >>f, """<userpic keyword="%s" url="%s" />""" % (p, userpics[p])
336             pic = urllib2.urlopen(userpics[p])
337             ext = MimeExtensions.get(pic.info()["Content-Type"], "")
338             picfn = re.sub(r'[*?\\/:<>"|]', "_", p)
339             try:
340                 picfn = codecs.utf_8_decode(picfn)[0]
341                 picf = open("%s/%s%s" % (Username, picfn, ext), "wb")
342             except:
343                 # for installations where the above utf_8_decode doesn't work
344                 picfn = "".join([ord(x) < 128 and x or "_" for x in picfn])
345                 picf = open("%s/%s%s" % (Username, picfn, ext), "wb")
346             shutil.copyfileobj(pic, picf)
347             pic.close()
348             picf.close()
349         print >>f, "</userpics>"
350         f.close()
351
352     if origlastsync:
353         print "%d new entries, %d new comments (since %s)" % (newentries, newcomments, origlastsync)
354     else:
355         print "%d new entries, %d new comments" % (newentries, newcomments)
356     if errors > 0:
357         print "%d errors" % errors
358
359 if __name__ == "__main__":
360     if os.access("ljdump.config", os.F_OK):
361         config = xml.dom.minidom.parse("ljdump.config")
362         server = config.documentElement.getElementsByTagName("server")[0].childNodes[0].data
363         username = config.documentElement.getElementsByTagName("username")[0].childNodes[0].data
364         password = config.documentElement.getElementsByTagName("password")[0].childNodes[0].data
365         journals = config.documentElement.getElementsByTagName("journal")
366         if journals:
367             for e in journals:
368                 ljdump(server, username, password, e.childNodes[0].data)
369         else:
370             ljdump(server, username, password, username)
371     else:
372         from getpass import getpass
373         print "ljdump - livejournal archiver"
374         print
375         print "Enter your Livejournal username and password."
376         print
377         server = "http://livejournal.com"
378         username = raw_input("Username: ")
379         password = getpass("Password: ")
380         print
381         print "You may back up either your own journal, or a community."
382         print "If you are a community maintainer, you can back up both entries and comments."
383         print "If you are not a maintainer, you can back up only entries."
384         print
385         journal = raw_input("Journal to back up (or hit return to back up '%s'): " % username)
386         print
387         if journal:
388             ljdump(server, username, password, journal)
389         else:
390             ljdump(server, username, password, username)
391 # vim:ts=4 et: