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,308 @@
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 interacts with the sqlite export from LogJam.
26
+
27
+ require 'sqlite3'
28
+
29
+ module LiveJournal
30
+ # An interface for an SQLite database dump.
31
+ class Database
32
+ class Error < RuntimeError; end
33
+
34
+ EXPECTED_DATABASE_VERSION = "3"
35
+ SCHEMA = %q{
36
+ CREATE TABLE meta (
37
+ key TEXT PRIMARY KEY,
38
+ value TEXT
39
+ );
40
+ CREATE TABLE entry (
41
+ itemid INTEGER PRIMARY KEY,
42
+ anum INTEGER,
43
+ subject TEXT,
44
+ event TEXT,
45
+ moodid INTEGER, mood TEXT, music TEXT, location TEXT, taglist TEXT,
46
+ pickeyword TEXT, preformatted INTEGER, backdated INTEGER,
47
+ comments INTEGER, year INTEGER, month INTEGER, day INTEGER,
48
+ timestamp INTEGER, security INTEGER
49
+ );
50
+ CREATE INDEX dateindex ON entry (year, month, day);
51
+ CREATE INDEX timeindex ON entry (timestamp);
52
+ CREATE TABLE comment (
53
+ commentid INTEGER PRIMARY KEY,
54
+ posterid INTEGER,
55
+ itemid INTEGER,
56
+ parentid INTEGER,
57
+ state TEXT, -- screened/deleted/frozen/active
58
+ subject TEXT,
59
+ body TEXT,
60
+ timestamp INTEGER -- unix timestamp
61
+ );
62
+ CREATE INDEX commententry ON comment (itemid);
63
+ CREATE TABLE users (
64
+ userid INTEGER PRIMARY KEY,
65
+ username TEXT
66
+ );
67
+ CREATE TABLE commentprop (
68
+ commentid INTEGER, -- not primary key 'cause non-unique
69
+ key TEXT,
70
+ value TEXT
71
+ );
72
+ }.gsub(/^ /, '')
73
+
74
+ def self.optional_to_i(x) # :nodoc:
75
+ return nil if x.nil?
76
+ return x.to_i
77
+ end
78
+
79
+ # The underlying SQLite3 database.
80
+ attr_reader :db
81
+
82
+ def initialize(filename, create_if_necessary=false)
83
+ exists = FileTest::exists? filename
84
+ raise Errno::ENOENT if not create_if_necessary and not exists
85
+ @db = SQLite3::Database.new(filename)
86
+
87
+ # We'd like to use type translation, but it unfortunately fails on MAX()
88
+ # queries.
89
+ # @db.type_translation = true
90
+
91
+ if exists
92
+ # Existing database!
93
+ version = self.version
94
+ unless version == EXPECTED_DATABASE_VERSION
95
+ raise Error, "Database version mismatch -- db has #{version.inspect}, expected #{EXPECTED_DATABASE_VERSION.inspect}"
96
+ end
97
+ end
98
+
99
+ if create_if_necessary and not exists
100
+ # New database! Initialize it.
101
+ transaction do
102
+ @db.execute_batch(SCHEMA)
103
+ end
104
+ self.version = EXPECTED_DATABASE_VERSION
105
+ end
106
+ end
107
+
108
+ # Run a block within a single database transaction.
109
+ # Useful for bulk inserts.
110
+ def transaction
111
+ @db.transaction { yield }
112
+ end
113
+
114
+ # Close the underlying database. (Is this necessary? Not sure.)
115
+ def close
116
+ @db.close
117
+ end
118
+
119
+ def get_meta key # :nodoc:
120
+ return @db.get_first_value('SELECT value FROM meta WHERE key=?', key)
121
+ end
122
+ def set_meta key, value # :nodoc:
123
+ @db.execute('INSERT OR REPLACE INTO meta VALUES (?, ?)', key, value)
124
+ end
125
+ def self.db_value(name, sym) # :nodoc:
126
+ class_eval %{def #{sym}; get_meta(#{name.inspect}); end}
127
+ class_eval %{def #{sym}=(v); set_meta(#{name.inspect}, v); end}
128
+ end
129
+
130
+ db_value 'username', :username
131
+ db_value 'usejournal', :usejournal
132
+ db_value 'lastsync', :lastsync
133
+ db_value 'version', :version
134
+
135
+ # The the actual journal stored by this Database.
136
+ # (This is different than simply the username when usejournal is specified.)
137
+ def journal
138
+ usejournal || username
139
+ end
140
+
141
+ # Turn tracing on. Mostly useful for debugging.
142
+ def trace!
143
+ @db.trace() do |data, sql|
144
+ puts "SQL> #{sql.inspect}"
145
+ end
146
+ end
147
+
148
+ # Fetch a specific itemid.
149
+ def get_entry(itemid)
150
+ query_entry("select * from entry where itemid=?", itemid)
151
+ end
152
+
153
+ # Given SQL that selects an entry, return that Entry.
154
+ def query_entry(sql, *sqlargs)
155
+ row = @db.get_first_row(sql, *sqlargs)
156
+ return Entry.new.load_from_database_row(row)
157
+ end
158
+
159
+ # Given SQL that selects some entries, yield each Entry.
160
+ def query_entries(sql, *sqlargs) # :yields: entry
161
+ @db.execute(sql, *sqlargs) do |row|
162
+ yield Entry.new.load_from_database_row(row)
163
+ end
164
+ end
165
+
166
+ # Yield a set of entries, ordered by ascending itemid (first to last).
167
+ def each_entry(where=nil, &block)
168
+ sql = 'SELECT * FROM entry'
169
+ sql += " WHERE #{where}" if where
170
+ sql += ' ORDER BY itemid ASC'
171
+ query_entries sql, &block
172
+ end
173
+
174
+ # Return the total number of entries.
175
+ def total_entry_count
176
+ @db.get_first_value('SELECT COUNT(*) FROM entry').to_i
177
+ end
178
+
179
+ # Store an Entry.
180
+ def store_entry entry
181
+ sql = 'INSERT OR REPLACE INTO entry VALUES (' + ("?, " * 17) + '?)'
182
+ @db.execute(sql, *entry.to_database_row)
183
+ end
184
+
185
+ # Used for Sync::Comments.
186
+ def last_comment_meta
187
+ Database::optional_to_i(
188
+ @db.get_first_value('SELECT MAX(commentid) FROM comment'))
189
+ end
190
+ # Used for Sync::Comments.
191
+ def last_comment_full
192
+ Database::optional_to_i(
193
+ @db.get_first_value('SELECT MAX(commentid) FROM comment ' +
194
+ 'WHERE body IS NOT NULL'))
195
+ end
196
+
197
+ # Used for Sync::Comments.
198
+ def store_comments_meta(comments)
199
+ store_comments(comments, true)
200
+ end
201
+ # Used for Sync::Comments.
202
+ def store_comments_full(comments)
203
+ store_comments(comments, false)
204
+ end
205
+
206
+ # Used for Sync::Comments.
207
+ def store_usermap(usermap)
208
+ transaction do
209
+ sql = "INSERT OR REPLACE INTO users VALUES (?, ?)"
210
+ @db.prepare(sql) do |stmt|
211
+ usermap.each do |id, user|
212
+ stmt.execute(id, user)
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ private
219
+ def store_comments(comments, meta_only=true)
220
+ transaction do
221
+ sql = "INSERT OR REPLACE INTO comment "
222
+ if meta_only
223
+ sql += "(commentid, posterid, state) VALUES (?, ?, ?)"
224
+ else
225
+ sql += "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
226
+ end
227
+ @db.prepare(sql) do |stmt|
228
+ comments.each do |id, comment|
229
+ if meta_only
230
+ stmt.execute(comment.commentid, comment.posterid,
231
+ LiveJournal::Comment::state_to_string(comment.state))
232
+ else
233
+ stmt.execute(*comment.to_database_row)
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ class Entry
242
+ # Parse an entry from a row from the database.
243
+ def load_from_database_row row
244
+ @itemid, @anum = row[0].to_i, row[1].to_i
245
+ @subject, @event = row[2], row[3]
246
+ @moodid, @mood = row[4].nil? ? nil : row[4].to_i, row[5]
247
+ @music, @location, @taglist, @pickeyword = row[6], row[7], row[8], row[9]
248
+ @taglist = if @taglist then @taglist.split(/, /) else [] end
249
+ @preformatted, @backdated = !row[10].nil?, !row[11].nil?
250
+ @comments = case Database::optional_to_i(row[12])
251
+ when nil; :normal
252
+ when 1; :none
253
+ when 2; :noemail
254
+ else raise Database::Error, "Bad comments value: #{row[12].inspect}"
255
+ end
256
+
257
+ @time = Time.at(row[16].to_i).utc
258
+
259
+ case Database::optional_to_i(row[17])
260
+ when nil
261
+ @security = :public
262
+ when 0
263
+ @security = :private
264
+ when 1
265
+ @security = :friends
266
+ else
267
+ @security = :custom
268
+ @allowmask = row[17]
269
+ end
270
+
271
+ self
272
+ end
273
+ def to_database_row
274
+ comments = case @comments
275
+ when :normal; nil
276
+ when :none; 1
277
+ when :noemail; 2
278
+ end
279
+ security = case @security
280
+ when :public; nil
281
+ when :private; 0
282
+ when :friends; 1
283
+ when :custom; @allowmask
284
+ end
285
+ [@itemid, @anum, @subject, @event,
286
+ @moodid, @mood, @music, @location, @taglist.join(', '), @pickeyword,
287
+ @preformatted ? 1 : nil, @backdated ? 1 : nil, comments,
288
+ @time.year, @time.mon, @time.day, @time.to_i, security]
289
+ end
290
+ end
291
+ class Comment
292
+ def load_from_database_row row
293
+ @commentid, @posterid = row[0].to_i, row[1].to_i
294
+ @itemid, @parentid = row[2].to_i, row[3].to_i
295
+ @state = Comment::state_from_string row[4]
296
+ @subject, @body = row[5], row[6]
297
+ @time = Time.at(row[7]).utc
298
+ self
299
+ end
300
+ def to_database_row
301
+ state = Comment::state_to_string @state
302
+ [@commentid, @posterid, @itemid, @parentid,
303
+ state, @subject, @body, @time.to_i]
304
+ end
305
+ end
306
+ end
307
+
308
+ # vim: ts=2 sw=2 et :
@@ -0,0 +1,385 @@
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
+ # LiveJournal times have no time zone, as they are for display only:
29
+ # "I wrote this post at midnight". However, it's convenient to represent
30
+ # times with a Ruby time object. But when time zones get involved,
31
+ # everything gets confused; we'd like to store a Unix time in the database
32
+ # and those only make sense as GMT. To reduce confusion, then, we imagine
33
+ # all LiveJournal times are in GMT.
34
+ # This function takes a time in any time zone and just switches the
35
+ # timezone to GMT. That is, coerce_gmt of 11:12pm PST is 11:12 pm GMT.
36
+ # The entry time-setting functions require this GMT part to verify you're
37
+ # thinking carefully about time when you use Entry#time. If I want an
38
+ # entry that has a time that corresponds to what I feel is "now", I'd use
39
+ # LiveJournal::coerce_gmt Time.now
40
+ def self.coerce_gmt time
41
+ expanded = time.to_a
42
+ expanded[8] = false # dst flag
43
+ expanded[9] = 'GMT'
44
+ return Time.gm(*expanded)
45
+ end
46
+
47
+ class Entry
48
+ attr_accessor :itemid, :anum, :subject, :event, :moodid, :mood
49
+ attr_accessor :music, :location, :taglist, :pickeyword
50
+ attr_accessor :preformatted, :backdated
51
+ attr_accessor :comments # values: {:normal, :none, :noemail}
52
+ attr_reader :time # a Ruby Time object
53
+ attr_accessor :security # values: {:public, :private, :friends, :custom}
54
+ attr_accessor :allowmask
55
+ attr_accessor :screening # values {:default, :all, :anonymous, :nonfriends, :none}
56
+ attr_accessor :interface # values {:web, ...}
57
+ attr_accessor :give_features
58
+
59
+ # A hash of any leftover properties (including those in KNOWN_EXTRA_PROPS)
60
+ # that aren't explicitly supported by ljrb. (See the
61
+ # Request::GetEvents#new for details.)
62
+ attr_accessor :props
63
+ # A list of extra properties we're aware of but don't wrap explicitly.
64
+ # Upon retrieval stored in the props hash.
65
+ # See: http://www.livejournal.com/doc/server/ljp.csp.proplist.html
66
+ KNOWN_EXTRA_PROPS = %w{admin_content_flag adult_content commentalter
67
+ current_coords personifi_lang personifi_tags personifi_word_count
68
+ qotdid revnum revtime sms_msgid statusvis syn_id syn_link unknown8bit
69
+ unsuspend_supportid used_rte useragent verticals_list poster_ip uniq}
70
+
71
+ def initialize
72
+ @subject = nil
73
+ @event = nil
74
+ @moodid = nil
75
+ @mood = nil
76
+ @music = nil
77
+ @location = nil
78
+ @taglist = []
79
+ @pickeyword = nil
80
+ @preformatted = false
81
+ @backdated = false
82
+ @comments = :normal
83
+ @security = :public
84
+ @allowmask = nil
85
+ @screening = :default
86
+ @give_features = nil
87
+ @props = {}
88
+ end
89
+
90
+ def ==(other)
91
+ return false if self.class != other.class
92
+
93
+ [:subject, :event, :moodid,
94
+ :mood, :music, :location, :taglist, :pickeyword,
95
+ :preformatted, :backdated, :comments, :security, :allowmask,
96
+ :screening, :props].each do |attr|
97
+ return false if send(attr) != other.send(attr)
98
+ end
99
+ # compare time fields one-by-one because livejournal ignores the
100
+ # "seconds" field.
101
+ [:year, :mon, :day, :hour, :min, :zone].each do |attr|
102
+ return false if @time.send(attr) != other.time.send(attr)
103
+ end
104
+ return true
105
+ end
106
+
107
+ def time=(time)
108
+ raise RuntimeError, "Must use GMT times everywhere to reduce confusion. See LiveJournal::coerce_gmt for details." unless time.gmt?
109
+ @time = time
110
+ end
111
+
112
+ def from_request(req)
113
+ @itemid, @anum = req['itemid'].to_i, req['anum'].to_i
114
+ @subject, @event = req['subject'], CGI.unescape(req['event'])
115
+
116
+ case req['security']
117
+ when 'public'
118
+ @security = :public
119
+ when 'private'
120
+ @security = :private
121
+ when 'usemask'
122
+ if req['allowmask'] == '1'
123
+ @security = :friends
124
+ else
125
+ @security = :custom
126
+ @allowmask = req['allowmask'].to_i
127
+ end
128
+ end
129
+
130
+ @time = LiveJournal::Request::ljtime_to_time req['eventtime']
131
+
132
+ # further metadata is loaded via #load_prop
133
+
134
+ self
135
+ end
136
+
137
+ def load_prop(name, value, strict=false) #:nodoc:#
138
+ case name
139
+ when 'current_location'
140
+ @location = value
141
+ when 'current_mood'
142
+ @mood = value
143
+ when 'current_moodid'
144
+ @moodid = value.to_i
145
+ when 'current_music'
146
+ @music = value
147
+ when 'hasscreened'
148
+ @screened = value == '1'
149
+ when 'interface'
150
+ @interface = value.intern
151
+ when 'opt_backdated'
152
+ @backdated = value == '1'
153
+ when 'opt_nocomments'
154
+ @comments = :none
155
+ when 'opt_noemail'
156
+ @comments = :noemail
157
+ when 'opt_preformatted'
158
+ @preformatted = value == '1'
159
+ when 'opt_screening'
160
+ case value
161
+ when 'A'; @screening = :all
162
+ when 'R'; @screening = :anonymous
163
+ when 'F'; @screening = :nonfriends
164
+ when 'N'; @screening = :none
165
+ when "default"; @screening = :default
166
+ else
167
+ raise "Unknown opt_screening value: #{value.inspect}"
168
+ end
169
+ when 'picture_keyword'
170
+ @pickeyword = value
171
+ when 'taglist'
172
+ @taglist = value.split(/,\s/).sort
173
+ when 'give_features'
174
+ @give_features = value == '1'
175
+ else
176
+ # LJ keeps adding props, so we store all leftovers in a hash.
177
+ # Unfortunately, we don't know which of these need to be passed
178
+ # on to new entries. This may mean we drop some data when we
179
+ # round-trip.
180
+ #
181
+ # Some we've seen so far:
182
+ # revnum, revtime, commentalter, unknown8bit, useragent
183
+ @props[name] = value
184
+
185
+ unless KNOWN_EXTRA_PROPS.include? name or not strict
186
+ raise Request::ProtocolException, "unknown prop (#{name}, #{value})"
187
+ end
188
+ end
189
+ end
190
+
191
+ # Get the numeric id used in URLs (it's a function of the itemid and the
192
+ # anum).
193
+ def display_itemid
194
+ (@itemid << 8) + @anum
195
+ end
196
+
197
+ def url(user)
198
+ #raise NotImplementedError, "only works for lj.com" unless user.server == LiveJournal::DEFAULT_SERVER
199
+ "#{user.journal_url}/#{display_itemid}.html"
200
+ end
201
+
202
+ # Render LJ markup to an HTML simulation of what is displayed on LJ
203
+ # itself. (XXX this needs some work: polls, better preformatting, etc.)
204
+ #
205
+ # (The server to use is necessary for rendering links to other LJ users.)
206
+ def event_as_html server=LiveJournal::DEFAULT_SERVER
207
+ # I'd like to use REXML but the content isn't XML, so REs it is!
208
+ html = @event.dup
209
+ html.gsub!(/\n/, "<br/>\n") unless @preformatted
210
+ html.gsub!(%r{< \s* lj \s+ user \s* = \s*
211
+ ['"]? ([^\s'"]+) ['"]?
212
+ \s* /? \s* >}ix) do
213
+ user = $1
214
+ url = "#{server.url}/~#{user}/"
215
+ "<a href='#{url}'><b>#{user}</b></a>"
216
+ end
217
+ html
218
+ end
219
+
220
+ def add_to_request req
221
+ req['event'] = self.event
222
+ req['lineendings'] = 'unix'
223
+ req['subject'] = self.subject
224
+
225
+ case self.security
226
+ when :public
227
+ req['security'] = 'public'
228
+ when :friends
229
+ req['security'] = 'usemask'
230
+ req['allowmask'] = 1
231
+ when :private
232
+ req['security'] = 'private'
233
+ when :custom
234
+ req['security'] = 'usemask'
235
+ req['allowmask'] = self.allowmask
236
+ end
237
+
238
+ req['give_features'] = self.give_features ? 1 : 0
239
+
240
+ if req['mode'] == 'postevent' && self.time.nil?
241
+ self.time = Time.now.gmtime
242
+ end
243
+
244
+ if self.time
245
+ req['year'], req['mon'], req['day'] =
246
+ self.time.year, self.time.mon, self.time.day
247
+ req['hour'], req['min'] = self.time.hour, self.time.min
248
+ end
249
+
250
+ { 'current_mood' => self.mood,
251
+ 'current_moodid' => self.moodid,
252
+ 'current_music' => self.music,
253
+ 'current_location' => self.location,
254
+ 'picture_keyword' => self.pickeyword,
255
+ 'taglist' => self.taglist.join(', '),
256
+ 'opt_preformatted' => self.preformatted ? 1 : 0,
257
+ 'opt_nocomments' => self.comments == :none ? 1 : 0,
258
+ 'opt_noemail' => self.comments == :noemail ? 1 : 0,
259
+ 'opt_backdated' => self.backdated ? 1 : 0,
260
+ 'opt_screening' =>
261
+ case self.screening
262
+ when :all; 'A'
263
+ when :anonymous; 'R'
264
+ when :nonfriends; 'F'
265
+ when :none; 'N'
266
+ when :default; ''
267
+ end
268
+ }.each do |name, value|
269
+ req["prop_#{name}"] = value
270
+ end
271
+ end
272
+ end
273
+
274
+ module Request
275
+ class PostEvent < Req
276
+ def initialize(user, entry)
277
+ super(user, 'postevent')
278
+ entry.add_to_request @request
279
+ @entry = entry
280
+ end
281
+
282
+ # Post an #Entry as a new post. Fills in the <tt>itemid</tt> and
283
+ # <tt>anum</tt> fields on the #Entry, which are necessary for
284
+ # Entry#display_itemid and Entry#url.
285
+ def run
286
+ super
287
+ @entry.itemid = @result['itemid'].to_i
288
+ @entry.anum = @result['anum'].to_i
289
+ end
290
+ end
291
+
292
+ class GetEvents < Req
293
+ # We support three different types of GetEvents:
294
+ # * <tt>GetEvents.new(user, :itemid => itemid)</tt> (fetch a single item)
295
+ # * <tt>GetEvents.new(user, :recent => n)</tt> (fetch most recent n itemds)
296
+ # * <tt>GetEvents.new(user, :lastsync => lastsync)</tt> (for syncing)
297
+ #
298
+ # We support one final option called <tt>:strict</tt>, which requires
299
+ # a bit of explanation.
300
+ #
301
+ # Whenever LiveJournal adds new metadata to entries (such as the
302
+ # location field, which was introduced in 2006) it also exposes this
303
+ # metadata via the LJ protocol. However, ljrb can't know about future
304
+ # metadata and doesn't know how to handle it properly. Some metadata
305
+ # (like the current location) must be sent to the server to
306
+ # publish an entry correctly; others, like the last revision time,
307
+ # must not be.
308
+ #
309
+ # Normally, when we see a new property we abort with a ProtocolException.
310
+ # If the object is constructed with <tt>:strict => false</tt>, we'll
311
+ # skip over any new properties.
312
+ def initialize(user, opts)
313
+ super(user, 'getevents')
314
+ @request['lineendings'] = 'unix'
315
+
316
+ @strict = false
317
+ @strict = opts[:strict] if opts.has_key? :strict
318
+
319
+ if opts.has_key? :itemid
320
+ @request['selecttype'] = 'one'
321
+ @request['itemid'] = opts[:itemid]
322
+ elsif opts.has_key? :recent
323
+ @request['selecttype'] = 'lastn'
324
+ @request['howmany'] = opts[:recent]
325
+ elsif opts.has_key? :lastsync
326
+ @request['selecttype'] = 'syncitems'
327
+ @request['lastsync'] = opts[:lastsync] if opts[:lastsync]
328
+ else
329
+ raise ArgumentError, 'invalid options for GetEvents'
330
+ end
331
+ end
332
+
333
+ # Returns either a single #Entry or a hash of itemid => #Entry, depending
334
+ # on the mode this was constructed with.
335
+ def run
336
+ super
337
+
338
+ entries = {}
339
+ each_in_array('events') do |req|
340
+ entry = Entry.new.from_request(req)
341
+ entries[entry.itemid] = entry
342
+ end
343
+
344
+ each_in_array('prop') do |prop|
345
+ itemid = prop['itemid'].to_i
346
+ entries[itemid].load_prop(prop['name'], prop['value'], @strict)
347
+ end
348
+
349
+ if @request.has_key? 'itemid'
350
+ return entries[@request['itemid']]
351
+ else
352
+ return entries
353
+ end
354
+ end
355
+ end
356
+
357
+ class EditEvent < Req
358
+ # To edit an entry, pass in a #User and an #Entry to this and run it.
359
+ # To delete an entry, pass in <tt>:delete => true</tt> as the third
360
+ # parameter. (In this case, the Entry object only needs its
361
+ # <tt>itemid</tt> filled in.)
362
+ #
363
+ # The LiveJournal API for deletion is to "edit" an entry to have an
364
+ # empty event. To prevent accidentally deleting entries, if you pass
365
+ # in an entry with an empty event without passing the delete flag, this
366
+ # will raise the AccidentalDeleteError exception.
367
+ def initialize(user, entry, opts={})
368
+ super(user, 'editevent')
369
+
370
+ @request['itemid'] = entry.itemid
371
+ if opts.has_key? :delete
372
+ @request['event'] = ''
373
+ else
374
+ entry.add_to_request @request
375
+ end
376
+
377
+ if @request['event'].nil? or @request['event'].empty?
378
+ raise AccidentalDeleteError unless opts.has_key? :delete
379
+ end
380
+ end
381
+ end
382
+ end
383
+ end
384
+
385
+ # vim: ts=2 sw=2 et :