]> www.wagner.pp.ru Git - oss/ljdump.git/blob - ljdump.py
add comment functionality
[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.1
6 #
7 # $Id$
8 #
9 # This program reads the journal entries from a livejournal (or compatible)
10 # blog site and archives them in a subdirectory named after the journal name.
11 #
12 # The configuration is read from "ljdump.config". A sample configuration is
13 # provided in "ljdump.config.sample", which should be copied and then edited.
14 # The configuration settings are:
15 #
16 #   server - The XMLRPC server URL. This should only need to be changed
17 #            if you are dumping a journal that is livejournal-compatible
18 #            but is not livejournal itself.
19 #
20 #   username - The livejournal user name. A subdirectory will be created
21 #              with this same name to store the journal entries.
22 #
23 #   password - The account password. This password is never sent in the
24 #              clear; the livejournal "challenge" password mechanism is used.
25 #
26 # This program may be run as often as needed to bring the backup copy up
27 # to date. Both new and updated items are downloaded.
28 #
29 # LICENSE
30 #
31 # This software is provided 'as-is', without any express or implied
32 # warranty.  In no event will the author be held liable for any damages
33 # arising from the use of this software.
34 #
35 # Permission is granted to anyone to use this software for any purpose,
36 # including commercial applications, and to alter it and redistribute it
37 # freely, subject to the following restrictions:
38 #
39 # 1. The origin of this software must not be misrepresented; you must not
40 #    claim that you wrote the original software. If you use this software
41 #    in a product, an acknowledgment in the product documentation would be
42 #    appreciated but is not required.
43 # 2. Altered source versions must be plainly marked as such, and must not be
44 #    misrepresented as being the original software.
45 # 3. This notice may not be removed or altered from any source distribution.
46 #
47 # Copyright (c) 2005-2006 Greg Hewgill
48
49 import codecs, md5, os, pickle, pprint, sys, urllib2, xml.dom.minidom, xmlrpclib
50 from xml.sax import saxutils
51
52 def calcchallenge(challenge, password):
53     return md5.new(challenge+md5.new(password).hexdigest()).hexdigest()
54
55 def flatresponse(response):
56     r = {}
57     while True:
58         name = response.readline()
59         if len(name) == 0:
60             break
61         if name[-1] == '\n':
62             name = name[:len(name)-1]
63         value = response.readline()
64         if value[-1] == '\n':
65             value = value[:len(value)-1]
66         r[name] = value
67     return r
68
69 def getljsession(username, password):
70     r = urllib2.urlopen("http://livejournal.com/interface/flat", "mode=getchallenge")
71     response = flatresponse(r)
72     r.close()
73     r = urllib2.urlopen("http://livejournal.com/interface/flat", "mode=sessiongenerate&user=%s&auth_method=challenge&auth_challenge=%s&auth_response=%s" % (username, response['challenge'], calcchallenge(response['challenge'], password)))
74     response = flatresponse(r)
75     r.close()
76     return response['ljsession']
77
78 def dochallenge(params, password):
79     challenge = server.LJ.XMLRPC.getchallenge()
80     params.update({
81         'auth_method': "challenge",
82         'auth_challenge': challenge['challenge'],
83         'auth_response': calcchallenge(challenge['challenge'], password)
84     })
85     return params
86
87 def dumpelement(f, name, e):
88     f.write("<%s>\n" % name)
89     for k in e.keys():
90         if isinstance(e[k], {}.__class__):
91             dumpelement(f, k, e[k])
92         else:
93             s = unicode(str(e[k]), "UTF-8")
94             f.write("<%s>%s</%s>\n" % (k, saxutils.escape(s), k))
95     f.write("</%s>\n" % name)
96
97 def writedump(fn, event):
98     f = codecs.open(fn, "w", "UTF-8")
99     f.write("""<?xml version="1.0"?>\n""")
100     dumpelement(f, "event", event)
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 config = xml.dom.minidom.parse("ljdump.config")
117 Server = config.documentElement.getElementsByTagName("server")[0].childNodes[0].data
118 Username = config.documentElement.getElementsByTagName("username")[0].childNodes[0].data
119 Password = config.documentElement.getElementsByTagName("password")[0].childNodes[0].data
120
121 print "Fetching journal entries for: %s" % Username
122 try:
123     os.mkdir(Username)
124     print "Created subdirectory: %s" % Username
125 except:
126     pass
127
128 ljsession = getljsession(Username, Password)
129
130 server = xmlrpclib.ServerProxy(Server)
131
132 newentries = 0
133 newcomments = 0
134 errors = 0
135
136 lastsync = ""
137 lastmaxid = 0
138 try:
139     f = open("%s/.last" % Username, "r")
140     lastsync = f.readline()
141     if lastsync[-1] == '\n':
142         lastsync = lastsync[:len(lastsync)-1]
143     lastmaxid = f.readline()
144     if len(lastmaxid) > 0 and lastmaxid[-1] == '\n':
145         lastmaxid = lastmaxid[:len(lastmaxid)-1]
146     if lastmaxid == "":
147         lastmaxid = 0
148     else:
149         lastmaxid = int(lastmaxid)
150     f.close()
151 except:
152     pass
153 origlastsync = lastsync
154
155 while True:
156     r = server.LJ.XMLRPC.syncitems(dochallenge({
157         'username': Username,
158         'ver': 1,
159         'lastsync': lastsync,
160     }, Password))
161     #pprint.pprint(r)
162     if len(r['syncitems']) == 0:
163         break
164     for item in r['syncitems']:
165         if item['item'][0] == 'L':
166             print "Fetching journal entry %s (%s)" % (item['item'], item['action'])
167             try:
168                 e = server.LJ.XMLRPC.getevents(dochallenge({
169                     'username': Username,
170                     'ver': 1,
171                     'selecttype': "one",
172                     'itemid': item['item'][2:],
173                 }, Password))
174                 writedump("%s/%s" % (Username, item['item']), e['events'][0])
175                 newentries += 1
176             except xmlrpclib.Fault, x:
177                 print "Error getting item: %s" % item['item']
178                 pprint.pprint(x)
179                 errors += 1
180         lastsync = item['time']
181
182 # The following code doesn't work because the server rejects our repeated calls.
183 # http://www.livejournal.com/doc/server/ljp.csp.xml-rpc.getevents.html
184 # contains the statement "You should use the syncitems selecttype in
185 # conjuntions [sic] with the syncitems protocol mode", but provides
186 # no other explanation about how these two function calls should
187 # interact. Therefore we just do the above slow one-at-a-time method.
188
189 #while True:
190 #    r = server.LJ.XMLRPC.getevents(dochallenge({
191 #        'username': Username,
192 #        'ver': 1,
193 #        'selecttype': "syncitems",
194 #        'lastsync': lastsync,
195 #    }, Password))
196 #    pprint.pprint(r)
197 #    if len(r['events']) == 0:
198 #        break
199 #    for item in r['events']:
200 #        writedump("%s/L-%d" % (Username, item['itemid']), item)
201 #        newentries += 1
202 #        lastsync = item['eventtime']
203
204 print "Fetching journal comments for: %s" % Username
205
206 try:
207     f = open("%s/comment.meta" % Username)
208     metacache = pickle.load(f)
209     f.close()
210 except:
211     metacache = {}
212
213 try:
214     f = open("%s/user.map" % Username)
215     usermap = pickle.load(f)
216     f.close()
217 except:
218     usermap = {}
219
220 maxid = lastmaxid
221 while True:
222     r = urllib2.urlopen(urllib2.Request("http://livejournal.com/export_comments.bml?get=comment_meta&startid=%d" % (maxid+1), headers = {'Cookie': "ljsession="+ljsession}))
223     meta = xml.dom.minidom.parse(r)
224     r.close()
225     for c in meta.getElementsByTagName("comment"):
226         id = int(c.getAttribute("id"))
227         metacache[id] = {
228             'posterid': c.getAttribute("posterid"),
229             'state': c.getAttribute("state"),
230         }
231         if id > maxid:
232             maxid = id
233     for u in meta.getElementsByTagName("usermap"):
234         usermap[u.getAttribute("id")] = u.getAttribute("user")
235     if maxid >= int(meta.getElementsByTagName("maxid")[0].firstChild.nodeValue):
236         break
237
238 f = open("%s/comment.meta" % Username, "w")
239 pickle.dump(metacache, f)
240 f.close()
241
242 f = open("%s/user.map" % Username, "w")
243 pickle.dump(usermap, f)
244 f.close()
245
246 newmaxid = maxid
247 maxid = lastmaxid
248 while True:
249     r = urllib2.urlopen(urllib2.Request("http://livejournal.com/export_comments.bml?get=comment_body&startid=%d" % (maxid+1), headers = {'Cookie': "ljsession="+ljsession}))
250     meta = xml.dom.minidom.parse(r)
251     r.close()
252     for c in meta.getElementsByTagName("comment"):
253         id = int(c.getAttribute("id"))
254         jitemid = c.getAttribute("jitemid")
255         comment = {
256             'id': str(id),
257             'parentid': c.getAttribute("parentid"),
258             'subject': gettext(c.getElementsByTagName("subject")),
259             'date': gettext(c.getElementsByTagName("date")),
260             'body': gettext(c.getElementsByTagName("body")),
261             'state': metacache[id]['state'],
262         }
263         if usermap.has_key(c.getAttribute("posterid")):
264             comment["user"] = usermap[c.getAttribute("posterid")]
265         try:
266             entry = xml.dom.minidom.parse("%s/C-%s" % (Username, jitemid))
267         except:
268             entry = xml.dom.minidom.getDOMImplementation().createDocument(None, "comments", None)
269         found = False
270         for d in entry.getElementsByTagName("comment"):
271             if int(d.getElementsByTagName("id")[0].firstChild.nodeValue) == id:
272                 found = True
273                 break
274         if found:
275             print "Warning: downloaded duplicate comment id %d in jitemid %s" % (id, jitemid)
276         else:
277             entry.documentElement.appendChild(createxml(entry, "comment", comment))
278             f = codecs.open("%s/C-%s" % (Username, jitemid), "w", "UTF-8")
279             entry.writexml(f)
280             f.close()
281             newcomments += 1
282         if id > maxid:
283             maxid = id
284     if maxid >= newmaxid:
285         break
286
287 lastmaxid = maxid
288
289 f = open("%s/.last" % Username, "w")
290 f.write("%s\n" % lastsync)
291 f.write("%s\n" % lastmaxid)
292 f.close()
293
294 if origlastsync:
295     print "%d new entries, %d new comments (since %s)" % (newentries, newcomments, origlastsync)
296 else:
297     print "%d new entries, %d new comments" % (newentries, newcomments)
298 if errors > 0:
299     print "%d errors" % errors