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.
- checksums.yaml +7 -0
- data/Changes +24 -0
- data/LICENSE +17 -0
- data/README.md +92 -0
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/lib/livejournal.rb +6 -0
- data/lib/livejournal/basic.rb +98 -0
- data/lib/livejournal/comment.rb +84 -0
- data/lib/livejournal/comments-xml.rb +161 -0
- data/lib/livejournal/database.rb +308 -0
- data/lib/livejournal/entry.rb +385 -0
- data/lib/livejournal/friends.rb +137 -0
- data/lib/livejournal/login.rb +46 -0
- data/lib/livejournal/logjam.rb +78 -0
- data/lib/livejournal/request.rb +142 -0
- data/lib/livejournal/sync.rb +187 -0
- data/sample/export +144 -0
- data/sample/fuse +198 -0
- data/sample/graph +208 -0
- data/sample/progressbar.rb +45 -0
- data/setup.rb +1585 -0
- data/test/checkfriends.rb +29 -0
- data/test/comments-xml.rb +110 -0
- data/test/database.rb +54 -0
- data/test/login.rb +19 -0
- data/test/roundtrip.rb +46 -0
- data/test/time.rb +49 -0
- metadata +74 -0
@@ -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 :
|