livejournal2 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/ruby
2
+ #--
3
+ # ljrb -- LiveJournal Ruby module
4
+ # Copyright (c) 2005 Evan Martin <martine@danga.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #++
24
+
25
+ require 'livejournal/request'
26
+
27
+ module LiveJournal
28
+ # Represents a LiveJournal friend relationship.
29
+ # See LiveJournal::Request::Friends to get an array of these.
30
+ class Friend
31
+ attr_accessor :username, :fullname
32
+ # as HTML color, like '#ff0000'
33
+ attr_accessor :background, :foreground
34
+ # bitfield of friend groups this friend is a member of
35
+ attr_accessor :groupmask
36
+ # friend type. possible values: :community, :news, :syndicated, :shared, :identity, :user
37
+ attr_accessor :type
38
+ def initialize
39
+ @username = nil
40
+ @fullname = nil
41
+ @background = nil
42
+ @foreground = nil
43
+ @groupmask = nil
44
+ @type = nil
45
+ end
46
+ def from_request(req)
47
+ @username = req['user']
48
+ @fullname = req['name']
49
+ @foreground = req['fg']
50
+ @background = req['bg']
51
+ @groupmask = req['groupmask']
52
+ @type =
53
+ case req['type']
54
+ when 'community'; :community
55
+ when 'news'; :news
56
+ when 'syndicated'; :syndicated
57
+ when 'shared'; :shared
58
+ when 'identity'; :identity
59
+ when nil; :user
60
+ else raise LiveJournal::Request::ProtocolException.new(
61
+ "unknown friend type: #{req['type']}")
62
+ end
63
+ self
64
+ end
65
+ def to_s
66
+ "#{@username}: #{@fullname}"
67
+ end
68
+ end
69
+
70
+ module Request
71
+ class Friends < Req
72
+ attr_reader :friends, :friendofs
73
+ # Allowed options:
74
+ # :include_friendofs => true:: also fill out @friendofs in single request
75
+ def initialize(user, opts={})
76
+ super(user, 'getfriends')
77
+ @friends = nil
78
+ @friendofs = nil
79
+ @request['includefriendof'] = true if opts.has_key? :include_friendofs
80
+ end
81
+ # Returns an array of LiveJournal::Friend.
82
+ def run
83
+ super
84
+ @friends = build_array('friend') { |r| Friend.new.from_request(r) }
85
+ @friendofs = build_array('friendof') { |r| Friend.new.from_request(r) }
86
+ @friends
87
+ end
88
+ end
89
+
90
+ # See Friends to fetch both friends and friend-ofs in one request.
91
+ class FriendOfs < Req
92
+ attr_reader :friendofs
93
+ def initialize(user)
94
+ super(user, 'friendof')
95
+ @friendofs = nil
96
+ end
97
+ # Returns an array of LiveJournal::Friend.
98
+ def run
99
+ super
100
+ @friends = build_array('friendof') { |r| Friend.new.from_request(r) }
101
+ @friends
102
+ end
103
+ end
104
+
105
+ # An example of polling for friends list updates.
106
+ # req = LiveJournal::Request::CheckFriends.new(user)
107
+ # req.run # always will return false on the first run.
108
+ # loop do
109
+ # puts "Waiting for new entries..."
110
+ # sleep req.interval # uses the server-recommended sleep time.
111
+ # break if req.run == true
112
+ # end
113
+ # puts "#{user.username}'s friends list has been updated!"
114
+ class CheckFriends < Req
115
+ # The server-recommended number of seconds to wait between running this.
116
+ attr_reader :interval
117
+ # If you want to keep your CheckFriends state without saving the object,
118
+ # save the #lastupdate field and pass it to a new object.
119
+ attr_reader :lastupdate
120
+ def initialize(user, lastupdate=nil)
121
+ super(user, 'checkfriends')
122
+ @lastupdate = lastupdate
123
+ @interval = 90 # reasonable default?
124
+ end
125
+ # Returns true if there are new posts available.
126
+ def run
127
+ @request['lastupdate'] = @lastupdate if @lastupdate
128
+ super
129
+ @lastupdate = @result['lastupdate']
130
+ @interval = @result['interval'].to_i
131
+ @result['new'] == '1'
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ # vim: ts=2 sw=2 et :
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/ruby
2
+ #--
3
+ # ljrb -- LiveJournal Ruby module
4
+ # Copyright (c) 2005 Evan Martin <martine@danga.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #++
24
+
25
+ require 'livejournal/request'
26
+
27
+ module LiveJournal
28
+ module Request
29
+ class Login < Req
30
+ def initialize(user)
31
+ super(user, 'login')
32
+ end
33
+ # Fills in the <tt>fullname</tt> of the #User this was created with.
34
+ # (XXX this sould be updated to also get the list of communities, etc.)
35
+ def run
36
+ super
37
+ u = @user # should we clone here?
38
+ u.fullname = @result['name']
39
+ u.journals = @result.select{|k,v| k=~/^access_\d+/}.collect{|k,v| v}.sort
40
+ u
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ # vim: ts=2 sw=2 et :
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/ruby
2
+ #--
3
+ # ljrb -- LiveJournal Ruby module
4
+ # Copyright (c) 2005 Evan Martin <martine@danga.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #++
24
+ #
25
+ # This module extends the LiveJournal module to work with LogJam's data.
26
+ # XXX this is currently not working due to database schema divergence
27
+
28
+ require 'rexml/document' # parsing logjam conf
29
+
30
+ module LiveJournal
31
+ # XXX this is currently not working due to database schema divergence
32
+ module LogJam
33
+ # Path to LogJam data.
34
+ def self.logjam_path
35
+ File.expand_path '~/.logjam'
36
+ end
37
+
38
+ def self.xml_fetch(file, path) #:nodoc:
39
+ doc = REXML::Document.new(File.open(file))
40
+ doc.elements.each(path) { |element| return element.text }
41
+ return nil
42
+ end
43
+
44
+ # Name of LogJam's current server.
45
+ def self.current_server
46
+ xml_fetch(logjam_path + '/conf.xml', '/configuration/currentserver')
47
+ end
48
+
49
+ # Path to LogJam's data for a given server.
50
+ def self.server_path servername
51
+ logjam_path + '/servers/' + servername # is escaping needed here?
52
+ end
53
+
54
+ # Username for a given server's current user.
55
+ def self.current_user servername
56
+ xml_fetch(server_path(servername) + '/conf.xml',
57
+ '/server/currentuser')
58
+ end
59
+
60
+ # Path to a given user's data.
61
+ def self.user_path servername, username
62
+ server_path(servername) + "/users/#{username}"
63
+ end
64
+
65
+ # Return [current_server, current_user].
66
+ def self.current_server_user
67
+ server = current_server
68
+ user = current_user server
69
+ [server, user]
70
+ end
71
+
72
+ def self.database_from_server_user servername, username
73
+ Database.new(LogJam::user_path(servername, username) + "/journal.db")
74
+ end
75
+ end
76
+ end
77
+
78
+ # vim: ts=2 sw=2 et :
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/ruby
2
+ #--
3
+ # ljrb -- LiveJournal Ruby module
4
+ # Copyright (c) 2005 Evan Martin <martine@danga.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #++
24
+
25
+
26
+ require 'livejournal/basic'
27
+ require 'cgi'
28
+ require 'net/http'
29
+ require 'date'
30
+ require 'digest/md5'
31
+
32
+ module LiveJournal
33
+ module Request
34
+ class ProtocolException < RuntimeError; end
35
+
36
+ # ljtimes look like 2005-12-04 10:24:00.
37
+ # Convert a Time to an ljtime.
38
+ def self.time_to_ljtime time
39
+ time.strftime '%Y-%m-%d %H:%M:%S'
40
+ end
41
+ # Convert an ljtime to a Time.
42
+ def self.ljtime_to_time str
43
+ dt = DateTime.strptime(str, '%Y-%m-%d %H:%M')
44
+ Time.gm(dt.year, dt.mon, dt.day, dt.hour, dt.min, 0, 0)
45
+ end
46
+
47
+ # wrapper around a given hash, prefixing all key lookups with base
48
+ class HashStrip #:nodoc:
49
+ def initialize(base, hash)
50
+ @base = base
51
+ @hash = hash
52
+ end
53
+ def [](key)
54
+ @hash[@base + key]
55
+ end
56
+ end
57
+
58
+ # Superclass for all LiveJournal requests.
59
+ class Req #:nodoc:
60
+ def initialize(user, mode, server = nil)
61
+ @server = server || (user && user.server) || LiveJournal::DEFAULT_SERVER
62
+ @user = user
63
+ @request = { 'mode' => mode,
64
+ 'clientversion' => 'Ruby',
65
+ 'ver' => 1 }
66
+ if user
67
+ challenge = GetChallenge.new(@server).run
68
+ response = Digest::MD5.hexdigest(challenge +
69
+ Digest::MD5.hexdigest(user.password))
70
+ @request.update({ 'user' => user.username,
71
+ 'auth_method' => 'challenge',
72
+ 'auth_challenge' => challenge,
73
+ 'auth_response' => response })
74
+ @request['usejournal'] = user.usejournal if user.usejournal
75
+ end
76
+ @result = {}
77
+ @verbose = false
78
+ @dryrun = false
79
+ end
80
+
81
+ def verbose!; @verbose = true; end
82
+ def dryrun!; @dryrun = true; end
83
+
84
+ def run
85
+ h = Net::HTTP.new(@server.host, @server.port)
86
+ h.use_ssl = @server.use_ssl
87
+ h.set_debug_output $stderr if @verbose
88
+ request = @request.collect { |key, value|
89
+ "#{CGI.escape(key)}=#{CGI.escape(value.to_s)}"
90
+ }.join("&")
91
+ p request if @verbose
92
+ return if @dryrun
93
+ response = h.post('/interface/flat', request, {"User-Agent" => "ljrb/0.3.x"})
94
+ data = response.read_body
95
+ parseresponse(data)
96
+ dumpresponse if @verbose
97
+ if @result['success'] != "OK"
98
+ raise ProtocolException, @result['errmsg']
99
+ end
100
+ end
101
+
102
+ def dumpresponse
103
+ @result.keys.sort.each { |key| puts "#{key} -> #{@result[key]}" }
104
+ end
105
+
106
+ protected
107
+ def parseresponse(data)
108
+ lines = data.split(/\r?\n/)
109
+ @result = {}
110
+ 0.step(lines.length-1, 2) do |i|
111
+ @result[lines[i]] = lines[i+1]
112
+ end
113
+ end
114
+
115
+ def each_in_array(base)
116
+ for i in 1..(@result["#{base}_count"].to_i) do
117
+ yield HashStrip.new("#{base}_#{i.to_s}_", @result)
118
+ end
119
+ end
120
+ def build_array(base)
121
+ array = []
122
+ each_in_array(base) { |x| array << yield(x) }
123
+ array
124
+ end
125
+ end
126
+
127
+ # Used for LiveJournal's challenge-response based authentication,
128
+ # and used by ljrb for all requests.
129
+ class GetChallenge < Req
130
+ def initialize(server = nil)
131
+ super(nil, 'getchallenge', server)
132
+ end
133
+ # Returns the challenge.
134
+ def run
135
+ super
136
+ return @result['challenge']
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ # vim: ts=2 sw=2 et cino=(0 :
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/ruby
2
+ #--
3
+ # ljrb -- LiveJournal Ruby module
4
+ # Copyright (c) 2005 Evan Martin <martine@danga.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #++
24
+
25
+ # A full sync involves both the LiveJournal flat protocol, for entries:
26
+ require 'livejournal/request'
27
+ # As well as a custom XML format via REST, for comments:
28
+ # http://www.livejournal.com/developer/exporting.bml
29
+ require 'open-uri'
30
+ require 'livejournal/comments-xml'
31
+
32
+ require 'livejournal/entry'
33
+ require 'livejournal/comment'
34
+
35
+ module LiveJournal
36
+ module Request
37
+ class SyncItems < Req
38
+ attr_reader :syncitems, :fetched, :total
39
+ def initialize(user, syncitems=nil, lastsync=nil)
40
+ super(user, 'syncitems')
41
+ @syncitems = syncitems || {}
42
+ @request['lastsync'] = lastsync if lastsync
43
+ end
44
+
45
+ def run
46
+ super
47
+ lasttime = nil
48
+ @fetched = 0
49
+ @total = @result['sync_total'].to_i
50
+ each_in_array('sync') do |item|
51
+ item, time = item['item'], item['time']
52
+ next if @syncitems.has_key? item
53
+ @fetched += 1
54
+ lasttime = time if lasttime.nil? or time > lasttime
55
+ @syncitems[item] = time
56
+ end
57
+ lasttime
58
+ end
59
+
60
+ def self.subset_items(syncitems, want_type='L')
61
+ items = {}
62
+ syncitems.each do |item, time|
63
+ next unless item =~ /^(.)-(\d+)$/
64
+ type, id = $1, $2.to_i
65
+ items[id] = time if type == want_type
66
+ end
67
+ items
68
+ end
69
+ end
70
+
71
+ # This is only used for generating sessions used for syncing comments.
72
+ # It is used by ljrb internally.
73
+ class SessionGenerate < Req
74
+ def initialize(user)
75
+ super(user, 'sessiongenerate')
76
+ end
77
+ # Returns the LJ session.
78
+ def run
79
+ super
80
+ @result['ljsession']
81
+ end
82
+ end
83
+ end
84
+
85
+ # Journal export. A full export involves syncing both entries and comments.
86
+ # See <tt>samples/export</tt> for a full example.
87
+ module Sync
88
+
89
+ # To run a sync, create a Sync::Entries object, then call
90
+ # Entries#run_syncitems to fetch the sync metadata, then call
91
+ # Entries#run_sync to get the actual entries.
92
+ class Entries
93
+ # To resume from a previous sync, pass in its lastsync value.
94
+ attr_reader :lastsync
95
+
96
+ def initialize(user, lastsync=nil)
97
+ @user = user
98
+ @logitems = {}
99
+ @lastsync = lastsync
100
+ end
101
+ def run_syncitems # :yields: cur, total
102
+ cur = 0
103
+ total = nil
104
+ items = {}
105
+ lastsync = @lastsync
106
+ while total.nil? or cur < total
107
+ req = Request::SyncItems.new(@user, items, lastsync)
108
+ lastsync = req.run
109
+ cur += req.fetched
110
+ total = req.total unless total
111
+ yield cur, total if block_given?
112
+ end
113
+ @logitems = Request::SyncItems::subset_items(items, 'L')
114
+ return (not @logitems.empty?)
115
+ end
116
+
117
+ def run_sync # :yields: entries_hash, lastsync, remaining_count
118
+ return if @logitems.empty?
119
+
120
+ lastsync = @lastsync
121
+ while @logitems.size > 0
122
+ req = Request::GetEvents.new(@user, :lastsync => lastsync)
123
+ entries = req.run
124
+ # pop off all items that we now have entries for
125
+ entries.each do |itemid, entry|
126
+ time = @logitems.delete itemid
127
+ lastsync = time if lastsync.nil? or time > lastsync
128
+ end
129
+ yield entries, lastsync, @logitems.size
130
+ end
131
+ end
132
+ end
133
+
134
+ class Comments
135
+ def initialize(user)
136
+ @user = user
137
+ @session = nil
138
+ @maxid = nil
139
+ end
140
+
141
+ def run_GET(mode, start)
142
+ unless @session
143
+ req = Request::SessionGenerate.new(@user)
144
+ @session = req.run
145
+ end
146
+
147
+ path = "/export_comments.bml?get=comment_#{mode}&startid=#{start}"
148
+ # authas: hooray for features discovered by reading source!
149
+ path += "&authas=#{@user.usejournal}" if @user.usejournal
150
+
151
+ data = nil
152
+ open(@user.server.url + path,
153
+ 'Cookie' => "ljsession=#{@session}") do |f|
154
+ # XXX stream this data to the XML parser.
155
+ data = f.read
156
+ end
157
+ return data
158
+ end
159
+ private :run_GET
160
+
161
+ def run_metadata(start=0) # :yields: cur, total, comments_hash
162
+ while @maxid.nil? or start < @maxid
163
+ data = run_GET('meta', start)
164
+ parsed = LiveJournal::Sync::CommentsXML::Parser.new(data)
165
+ @maxid ||= parsed.maxid
166
+ break if parsed.comments.empty?
167
+ cur = parsed.comments.keys.max
168
+ yield cur, @maxid, parsed
169
+ start = cur + 1
170
+ end
171
+ end
172
+
173
+ def run_body(start=0) # :yields: cur, total, comments_hash
174
+ while start < @maxid
175
+ data = run_GET('body', start)
176
+ parsed = LiveJournal::Sync::CommentsXML::Parser.new(data)
177
+ break if parsed.comments.empty?
178
+ cur = parsed.comments.keys.max
179
+ yield cur, @maxid, parsed
180
+ start = cur + 1
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ # vim: ts=2 sw=2 et :