]> www.wagner.pp.ru Git - oss/ljdump.git/blob - ljdump.py
604774edac41ab2cf174573414a5783827134c82
[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.3.2
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, md5, 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 def calcchallenge(challenge, password):
37     return md5.new(challenge+md5.new(password).hexdigest()).hexdigest()
38
39 def flatresponse(response):
40     r = {}
41     while True:
42         name = response.readline()
43         if len(name) == 0:
44             break
45         if name[-1] == '\n':
46             name = name[:len(name)-1]
47         value = response.readline()
48         if value[-1] == '\n':
49             value = value[:len(value)-1]
50         r[name] = value
51     return r
52
53 def getljsession(server, username, password):
54     r = urllib2.urlopen(server+"/interface/flat", "mode=getchallenge")
55     response = flatresponse(r)
56     r.close()
57     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)))
58     response = flatresponse(r)
59     r.close()
60     return response['ljsession']
61
62 def dochallenge(server, params, password):
63     challenge = server.LJ.XMLRPC.getchallenge()
64     params.update({
65         'auth_method': "challenge",
66         'auth_challenge': challenge['challenge'],
67         'auth_response': calcchallenge(challenge['challenge'], password)
68     })
69     return params
70
71 def dumpelement(f, name, e):
72     f.write("<%s>\n" % name)
73     for k in e.keys():
74         if isinstance(e[k], {}.__class__):
75             dumpelement(f, k, e[k])
76         else:
77             s = unicode(str(e[k]), "UTF-8")
78             f.write("<%s>%s</%s>\n" % (k, saxutils.escape(s), k))
79     f.write("</%s>\n" % name)
80
81 def writedump(fn, event):
82     f = codecs.open(fn, "w", "UTF-8")
83     f.write("""<?xml version="1.0"?>\n""")
84     dumpelement(f, "event", event)
85     f.close()
86
87 def writelast(username, lastsync, lastmaxid):
88     f = open("%s/.last" % username, "w")
89     f.write("%s\n" % lastsync)
90     f.write("%s\n" % lastmaxid)
91     f.close()
92
93 def createxml(doc, name, map):
94     e = doc.createElement(name)
95     for k in map.keys():
96         me = doc.createElement(k)
97         me.appendChild(doc.createTextNode(map[k]))
98         e.appendChild(me)
99     return e
100
101 def gettext(e):
102     if len(e) == 0:
103         return ""
104     return e[0].firstChild.nodeValue
105
106 def ljdump(Server, Username, Password):
107     m = re.search("(.*)/interface/xmlrpc", Server)
108     if m:
109         Server = m.group(1)
110
111     print "Fetching journal entries for: %s" % Username
112     try:
113         os.mkdir(Username)
114         print "Created subdirectory: %s" % Username
115     except:
116         pass
117
118     ljsession = getljsession(Server, Username, Password)
119
120     server = xmlrpclib.ServerProxy(Server+"/interface/xmlrpc")
121
122     newentries = 0
123     newcomments = 0
124     errors = 0
125
126     lastsync = ""
127     lastmaxid = 0
128     try:
129         f = open("%s/.last" % Username, "r")
130         lastsync = f.readline()
131         if lastsync[-1] == '\n':
132             lastsync = lastsync[:len(lastsync)-1]
133         lastmaxid = f.readline()
134         if len(lastmaxid) > 0 and lastmaxid[-1] == '\n':
135             lastmaxid = lastmaxid[:len(lastmaxid)-1]
136         if lastmaxid == "":
137             lastmaxid = 0
138         else:
139             lastmaxid = int(lastmaxid)
140         f.close()
141     except:
142         pass
143     origlastsync = lastsync
144
145     r = server.LJ.XMLRPC.login(dochallenge(server, {
146         'username': Username,
147         'ver': 1,
148         'getpickws': 1,
149         'getpickwurls': 1,
150     }, Password))
151     userpics = dict(zip(map(str, r['pickws']), r['pickwurls']))
152     userpics['*'] = r['defaultpicurl']
153
154     while True:
155         r = server.LJ.XMLRPC.syncitems(dochallenge(server, {
156             'username': Username,
157             'ver': 1,
158             'lastsync': lastsync,
159         }, Password))
160         #pprint.pprint(r)
161         if len(r['syncitems']) == 0:
162             break
163         for item in r['syncitems']:
164             if item['item'][0] == 'L':
165                 print "Fetching journal entry %s (%s)" % (item['item'], item['action'])
166                 try:
167                     e = server.LJ.XMLRPC.getevents(dochallenge(server, {
168                         'username': Username,
169                         'ver': 1,
170                         'selecttype': "one",
171                         'itemid': item['item'][2:],
172                     }, Password))
173                     if e['events']:
174                         writedump("%s/%s" % (Username, item['item']), e['events'][0])
175                         newentries += 1
176                     else:
177                         print "Unexpected empty item: %s" % item['item']
178                         errors += 1
179                 except xmlrpclib.Fault, x:
180                     print "Error getting item: %s" % item['item']
181                     pprint.pprint(x)
182                     errors += 1
183             lastsync = item['time']
184             writelast(Username, lastsync, lastmaxid)
185
186     # The following code doesn't work because the server rejects our repeated calls.
187     # http://www.livejournal.com/doc/server/ljp.csp.xml-rpc.getevents.html
188     # contains the statement "You should use the syncitems selecttype in
189     # conjuntions [sic] with the syncitems protocol mode", but provides
190     # no other explanation about how these two function calls should
191     # interact. Therefore we just do the above slow one-at-a-time method.
192
193     #while True:
194     #    r = server.LJ.XMLRPC.getevents(dochallenge(server, {
195     #        'username': Username,
196     #        'ver': 1,
197     #        'selecttype': "syncitems",
198     #        'lastsync': lastsync,
199     #    }, Password))
200     #    pprint.pprint(r)
201     #    if len(r['events']) == 0:
202     #        break
203     #    for item in r['events']:
204     #        writedump("%s/L-%d" % (Username, item['itemid']), item)
205     #        newentries += 1
206     #        lastsync = item['eventtime']
207
208     print "Fetching journal comments for: %s" % Username
209
210     try:
211         f = open("%s/comment.meta" % Username)
212         metacache = pickle.load(f)
213         f.close()
214     except:
215         metacache = {}
216
217     try:
218         f = open("%s/user.map" % Username)
219         usermap = pickle.load(f)
220         f.close()
221     except:
222         usermap = {}
223
224     maxid = lastmaxid
225     while True:
226         r = urllib2.urlopen(urllib2.Request(Server+"/export_comments.bml?get=comment_meta&startid=%d" % (maxid+1), headers = {'Cookie': "ljsession="+ljsession}))
227         meta = xml.dom.minidom.parse(r)
228         r.close()
229         for c in meta.getElementsByTagName("comment"):
230             id = int(c.getAttribute("id"))
231             metacache[id] = {
232                 'posterid': c.getAttribute("posterid"),
233                 'state': c.getAttribute("state"),
234             }
235             if id > maxid:
236                 maxid = id
237         for u in meta.getElementsByTagName("usermap"):
238             usermap[u.getAttribute("id")] = u.getAttribute("user")
239         if maxid >= int(meta.getElementsByTagName("maxid")[0].firstChild.nodeValue):
240             break
241
242     f = open("%s/comment.meta" % Username, "w")
243     pickle.dump(metacache, f)
244     f.close()
245
246     f = open("%s/user.map" % Username, "w")
247     pickle.dump(usermap, f)
248     f.close()
249
250     print "Fetching userpics for: %s" % Username
251     f = open("%s/userpics.xml" % Username, "w")
252     print >>f, """<?xml version="1.0"?>"""
253     print >>f, "<userpics>"
254     for p in userpics:
255         print >>f, """<userpic keyword="%s" url="%s" />""" % (p, userpics[p])
256         pic = urllib2.urlopen(userpics[p])
257         ext = MimeExtensions.get(pic.info()["Content-Type"], "")
258         picfn = re.sub(r"[\/]", "_", p)
259         try:
260             picfn = codecs.utf_8_decode(picfn)[0]
261             picf = open("%s/%s%s" % (Username, picfn, ext), "wb")
262         except:
263             # for installations where the above utf_8_decode doesn't work
264             picfn = "".join([ord(x) < 128 and x or "?" for x in picfn])
265             picf = open("%s/%s%s" % (Username, picfn, ext), "wb")
266         shutil.copyfileobj(pic, picf)
267         pic.close()
268         picf.close()
269     print >>f, "</userpics>"
270     f.close()
271
272     newmaxid = maxid
273     maxid = lastmaxid
274     while True:
275         r = urllib2.urlopen(urllib2.Request(Server+"/export_comments.bml?get=comment_body&startid=%d" % (maxid+1), headers = {'Cookie': "ljsession="+ljsession}))
276         meta = xml.dom.minidom.parse(r)
277         r.close()
278         for c in meta.getElementsByTagName("comment"):
279             id = int(c.getAttribute("id"))
280             jitemid = c.getAttribute("jitemid")
281             comment = {
282                 'id': str(id),
283                 'parentid': c.getAttribute("parentid"),
284                 'subject': gettext(c.getElementsByTagName("subject")),
285                 'date': gettext(c.getElementsByTagName("date")),
286                 'body': gettext(c.getElementsByTagName("body")),
287                 'state': metacache[id]['state'],
288             }
289             if usermap.has_key(c.getAttribute("posterid")):
290                 comment["user"] = usermap[c.getAttribute("posterid")]
291             try:
292                 entry = xml.dom.minidom.parse("%s/C-%s" % (Username, jitemid))
293             except:
294                 entry = xml.dom.minidom.getDOMImplementation().createDocument(None, "comments", None)
295             found = False
296             for d in entry.getElementsByTagName("comment"):
297                 if int(d.getElementsByTagName("id")[0].firstChild.nodeValue) == id:
298                     found = True
299                     break
300             if found:
301                 print "Warning: downloaded duplicate comment id %d in jitemid %s" % (id, jitemid)
302             else:
303                 entry.documentElement.appendChild(createxml(entry, "comment", comment))
304                 f = codecs.open("%s/C-%s" % (Username, jitemid), "w", "UTF-8")
305                 entry.writexml(f)
306                 f.close()
307                 newcomments += 1
308             if id > maxid:
309                 maxid = id
310         if maxid >= newmaxid:
311             break
312
313     lastmaxid = maxid
314
315     writelast(Username, lastsync, lastmaxid)
316
317     if origlastsync:
318         print "%d new entries, %d new comments (since %s)" % (newentries, newcomments, origlastsync)
319     else:
320         print "%d new entries, %d new comments" % (newentries, newcomments)
321     if errors > 0:
322         print "%d errors" % errors
323
324 if __name__ == "__main__":
325     config = xml.dom.minidom.parse("ljdump.config")
326     server = config.documentElement.getElementsByTagName("server")[0].childNodes[0].data
327     username = config.documentElement.getElementsByTagName("username")[0].childNodes[0].data
328     password = config.documentElement.getElementsByTagName("password")[0].childNodes[0].data
329     ljdump(server, username, password)