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
data/sample/export
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
#!/usr/bin/ruby -I../lib
|
2
|
+
|
3
|
+
require 'livejournal/sync'
|
4
|
+
require 'livejournal/database'
|
5
|
+
require 'optparse'
|
6
|
+
require 'progressbar'
|
7
|
+
|
8
|
+
create = false
|
9
|
+
username = nil
|
10
|
+
password = nil
|
11
|
+
usejournal = nil
|
12
|
+
dbfile = nil
|
13
|
+
get_entries = true
|
14
|
+
get_comments = true
|
15
|
+
opts = OptionParser.new do |opts|
|
16
|
+
opts.on('-c', '--create',
|
17
|
+
'Create a new database if necessary') { |create| }
|
18
|
+
opts.on('-u', '--user USERNAME',
|
19
|
+
'Login username') { |username| }
|
20
|
+
opts.on('-p', '--password PASSWORD',
|
21
|
+
'Login password') { |password| }
|
22
|
+
opts.on('-a', '--usejournal JOURNAL',
|
23
|
+
'Journal to sync (USERNAME must have access)') { |usejournal| }
|
24
|
+
opts.on('-d', '--db FILENAME',
|
25
|
+
'Filename for output database') { |dbfile| }
|
26
|
+
opts.on('--nocomments',
|
27
|
+
"Don't fetch comments (only entries)") { get_comments = false }
|
28
|
+
opts.on('--noentries',
|
29
|
+
"Don't fetch entries (only comments)") { get_entries = false }
|
30
|
+
end
|
31
|
+
opts.parse!(ARGV)
|
32
|
+
|
33
|
+
unless dbfile
|
34
|
+
puts opts
|
35
|
+
puts "ERROR: Must specify database file."
|
36
|
+
exit 1
|
37
|
+
end
|
38
|
+
|
39
|
+
begin
|
40
|
+
db = LiveJournal::Database.new(dbfile, create)
|
41
|
+
rescue Errno::ENOENT
|
42
|
+
puts "Use the --create flag to create a new database."
|
43
|
+
raise
|
44
|
+
end
|
45
|
+
username ||= db.username
|
46
|
+
usejournal ||= db.usejournal
|
47
|
+
|
48
|
+
unless username
|
49
|
+
puts opts
|
50
|
+
puts "ERROR: Must specify username."
|
51
|
+
exit 1
|
52
|
+
end
|
53
|
+
|
54
|
+
if usejournal
|
55
|
+
puts "Journal: #{usejournal} (syncing as #{username})."
|
56
|
+
else
|
57
|
+
puts "Journal: #{username}."
|
58
|
+
end
|
59
|
+
|
60
|
+
unless password
|
61
|
+
noecho = system('stty -echo')
|
62
|
+
print "Enter password"
|
63
|
+
print "(WARNING: echoed to screen)" unless noecho
|
64
|
+
print ": "
|
65
|
+
$stdout.flush
|
66
|
+
|
67
|
+
begin
|
68
|
+
password = gets.strip
|
69
|
+
ensure
|
70
|
+
if noecho
|
71
|
+
system('stty sane')
|
72
|
+
puts # since the user input didn't add the newline for us...
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
user = LiveJournal::User.new(username, password)
|
78
|
+
user.usejournal = usejournal
|
79
|
+
db.username = user.username
|
80
|
+
db.usejournal = usejournal if usejournal
|
81
|
+
|
82
|
+
if get_entries
|
83
|
+
puts "Fetching entries..."
|
84
|
+
lastsync = db.lastsync
|
85
|
+
puts "Resuming from #{lastsync}." if lastsync
|
86
|
+
sync = LiveJournal::Sync::Entries.new(user, lastsync)
|
87
|
+
|
88
|
+
ProgressBar::with_progress("Fetching metadata: ") do |bar|
|
89
|
+
sync.run_syncitems do |cur, total|
|
90
|
+
bar.update(cur, total)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
ProgressBar::with_progress("Fetching bodies: ") do |bar|
|
95
|
+
cur = 0
|
96
|
+
sync.run_sync do |entries, lastsync, remaining|
|
97
|
+
db.transaction do
|
98
|
+
entries.each do |itemid, entry|
|
99
|
+
db.store_entry entry
|
100
|
+
end
|
101
|
+
end
|
102
|
+
db.lastsync = lastsync
|
103
|
+
|
104
|
+
cur += entries.length
|
105
|
+
bar.update(cur, remaining+cur)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
if get_comments
|
111
|
+
puts "Fetching comments..."
|
112
|
+
|
113
|
+
cs = LiveJournal::Sync::Comments.new(user)
|
114
|
+
next_meta = db.last_comment_meta
|
115
|
+
if next_meta
|
116
|
+
next_meta += 1
|
117
|
+
else
|
118
|
+
next_meta = 0
|
119
|
+
end
|
120
|
+
|
121
|
+
ProgressBar::with_progress("Fetching metadata: ") do |bar|
|
122
|
+
cs.run_metadata(next_meta) do |cur, max, data|
|
123
|
+
db.store_comments_meta data.comments
|
124
|
+
db.store_usermap data.usermap
|
125
|
+
bar.update(cur, max)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
next_full = db.last_comment_full
|
130
|
+
if next_full
|
131
|
+
next_full += 1
|
132
|
+
else
|
133
|
+
next_full = 0
|
134
|
+
end
|
135
|
+
|
136
|
+
ProgressBar::with_progress("Fetching bodies: ") do |bar|
|
137
|
+
cs.run_body(next_full) do |cur, max, data|
|
138
|
+
bar.update(cur, max)
|
139
|
+
db.store_comments_full data.comments
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# vim: ts=2 sw=2 et :
|
data/sample/fuse
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
#!/usr/bin/ruby -I../lib
|
2
|
+
|
3
|
+
require 'fusefs'
|
4
|
+
require 'livejournal/database'
|
5
|
+
require 'livejournal/entry'
|
6
|
+
require 'stringio'
|
7
|
+
|
8
|
+
class DBQuery
|
9
|
+
def initialize
|
10
|
+
@db = $db
|
11
|
+
end
|
12
|
+
def has_any?(where, *args)
|
13
|
+
exists = @db.db.get_first_value("SELECT itemid #{where} LIMIT 1", *args)
|
14
|
+
exists != nil
|
15
|
+
end
|
16
|
+
def has_year? year
|
17
|
+
has_any?("FROM entry WHERE year=?", year)
|
18
|
+
end
|
19
|
+
def has_month? year, month
|
20
|
+
has_any?("FROM entry WHERE year=? AND month=?", year, month)
|
21
|
+
end
|
22
|
+
def has_day? year, month, day
|
23
|
+
has_any?("FROM entry WHERE year=? AND month=? AND day=?", year, month, day)
|
24
|
+
end
|
25
|
+
def has_entry? itemid
|
26
|
+
has_any?("FROM entry WHERE itemid=?", itemid)
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_array(sql, *args)
|
30
|
+
array = []
|
31
|
+
@db.db.execute("SELECT DISTINCT #{sql}", *args) do |row|
|
32
|
+
array << row[0]
|
33
|
+
end
|
34
|
+
array
|
35
|
+
end
|
36
|
+
|
37
|
+
def years
|
38
|
+
get_array('year FROM entry')
|
39
|
+
end
|
40
|
+
def months year
|
41
|
+
get_array('month FROM entry WHERE year=?', year).map { |m| "%02d" % m }
|
42
|
+
end
|
43
|
+
def days year, month
|
44
|
+
get_array('day FROM entry WHERE year=? AND month=?', year, month).map { |d| "%02d" % d }
|
45
|
+
end
|
46
|
+
|
47
|
+
def day_entries year, month, day
|
48
|
+
get_array('itemid FROM entry WHERE year=? AND month=? AND day=?',
|
49
|
+
year, month, day)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Dispatcher < FuseFS::FuseDir
|
54
|
+
def initialize
|
55
|
+
@matches = []
|
56
|
+
end
|
57
|
+
def register(match, target)
|
58
|
+
@matches << [match, target]
|
59
|
+
end
|
60
|
+
|
61
|
+
def dispatch(sym, path, *args)
|
62
|
+
path, rest = split_path path if path
|
63
|
+
if path
|
64
|
+
@matches.each do |match, target|
|
65
|
+
if path =~ match
|
66
|
+
return target.dispatch(sym, rest, *(args+[path]))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# otherwise, dispatch it to the current object
|
72
|
+
begin
|
73
|
+
s = self.send(sym, path, *args)
|
74
|
+
rescue
|
75
|
+
p $!
|
76
|
+
end
|
77
|
+
return s
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class DispatchDir < FuseFS::FuseDir
|
82
|
+
def initialize dispatcher
|
83
|
+
@dispatcher = dispatcher
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.add_dispatch(*args)
|
87
|
+
args.each do |sym|
|
88
|
+
class_eval %{def #{sym}(path, *args)
|
89
|
+
@dispatcher.dispatch(#{sym.inspect}, path, *args)
|
90
|
+
end}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
add_dispatch :directory?, :file?
|
95
|
+
add_dispatch :contents, :read_file
|
96
|
+
end
|
97
|
+
|
98
|
+
class LJFS < Dispatcher
|
99
|
+
class Day < Dispatcher
|
100
|
+
def contents path, year, mon, day
|
101
|
+
entries = $dbq.day_entries year, mon, day
|
102
|
+
entries.map do |itemid|
|
103
|
+
"#{itemid}.txt"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
def directory? path, year, mon, day
|
107
|
+
path == nil and $dbq.has_day? year, mon, day
|
108
|
+
end
|
109
|
+
def file? path, year, mon, day
|
110
|
+
return false unless $dbq.has_day? year, mon, day
|
111
|
+
return false unless path =~ /^(\d+)\.txt$/
|
112
|
+
itemid = $1
|
113
|
+
return false unless $dbq.has_entry? itemid
|
114
|
+
true
|
115
|
+
end
|
116
|
+
def read_file path, year, mon, day
|
117
|
+
return false unless $dbq.has_day? year, mon, day
|
118
|
+
return false unless path =~ /^(\d+)\.txt$/
|
119
|
+
itemid = $1
|
120
|
+
return false unless $dbq.has_entry? itemid
|
121
|
+
entry = $db.get_entry itemid
|
122
|
+
out = StringIO.new
|
123
|
+
if entry.subject
|
124
|
+
out.puts entry.subject
|
125
|
+
out.puts("=" * entry.subject.length)
|
126
|
+
out.puts
|
127
|
+
end
|
128
|
+
out.puts entry.event
|
129
|
+
out.rewind
|
130
|
+
return out.read
|
131
|
+
end
|
132
|
+
end
|
133
|
+
class Month < Dispatcher
|
134
|
+
def initialize
|
135
|
+
super
|
136
|
+
register(/^\d{2}/, Day.new)
|
137
|
+
end
|
138
|
+
def directory? path, year, mon
|
139
|
+
path == nil and $dbq.has_month? year, mon
|
140
|
+
end
|
141
|
+
def file? path, year, mon
|
142
|
+
false
|
143
|
+
end
|
144
|
+
def contents path, year, mon
|
145
|
+
$dbq.days year, mon
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class Year < Dispatcher
|
150
|
+
def initialize
|
151
|
+
super
|
152
|
+
register(/^\d{2}/, Month.new)
|
153
|
+
end
|
154
|
+
def directory? path, year
|
155
|
+
path == nil and $dbq.has_year? year
|
156
|
+
end
|
157
|
+
def file? path, year
|
158
|
+
false
|
159
|
+
end
|
160
|
+
def contents path, year
|
161
|
+
$dbq.months year
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def initialize
|
166
|
+
super
|
167
|
+
register(/^\d{4}$/, Year.new)
|
168
|
+
end
|
169
|
+
|
170
|
+
def directory? path
|
171
|
+
return false
|
172
|
+
end
|
173
|
+
def file? path
|
174
|
+
return path == 'username'
|
175
|
+
end
|
176
|
+
def read_file path
|
177
|
+
return $db.username
|
178
|
+
end
|
179
|
+
def contents *args
|
180
|
+
return ['username'] + $dbq.years
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
unless ARGV.length == 2
|
185
|
+
puts "usage: #{$0} dbfile mountpoint"
|
186
|
+
exit 1
|
187
|
+
end
|
188
|
+
dbfile, mountpoint = ARGV
|
189
|
+
|
190
|
+
$db = LiveJournal::Database.new dbfile
|
191
|
+
$dbq = DBQuery.new # temp hack
|
192
|
+
root = DispatchDir.new LJFS.new
|
193
|
+
|
194
|
+
FuseFS.set_root root
|
195
|
+
FuseFS.mount_under mountpoint
|
196
|
+
FuseFS.run
|
197
|
+
|
198
|
+
# vim: ts=2 sw=2 et :
|
data/sample/graph
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
#!/usr/bin/ruby -I../lib
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rubygems'
|
5
|
+
require 'gruff'
|
6
|
+
rescue LoadError => e
|
7
|
+
puts <<EOT
|
8
|
+
Error loading libraries: #{e}
|
9
|
+
Though ljrb itself doesn't require them, the graph sample uses
|
10
|
+
the gruff library: http://nubyonrails.com/pages/gruff ,
|
11
|
+
which itself requires rubygems.
|
12
|
+
EOT
|
13
|
+
exit 1
|
14
|
+
end
|
15
|
+
require 'livejournal/database'
|
16
|
+
require 'livejournal/entry'
|
17
|
+
require 'optparse'
|
18
|
+
require 'time'
|
19
|
+
|
20
|
+
class Options
|
21
|
+
# required options
|
22
|
+
attr_reader :dbfile, :outfile, :mode
|
23
|
+
# optional options
|
24
|
+
attr_reader :smooth, :normalize, :geom, :starttime, :endtime
|
25
|
+
# mode-specific options
|
26
|
+
attr_reader :tags
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@smooth = 1
|
30
|
+
@dbfile = nil
|
31
|
+
@outfile = nil
|
32
|
+
@normalize = false
|
33
|
+
@mode = nil
|
34
|
+
@geom = 800
|
35
|
+
@opts = OptionParser.new do |o|
|
36
|
+
o.banner = "usage: graph [options] [modeparam [modeparam ...]]"
|
37
|
+
o.on('-d', '--db=FILE', 'database (required)') { |@dbfile| }
|
38
|
+
o.on('-o', '--out=FILENAME.PNG', 'output file (required)') { |@outfile| }
|
39
|
+
o.on('-g', '--geom=WxH', 'output geometry') { |@geom| }
|
40
|
+
o.on('--smooth=N',
|
41
|
+
'merge every set of n consecutive datapoints') do |smooth|
|
42
|
+
@smooth = smooth.to_i
|
43
|
+
end
|
44
|
+
o.on('--normalize',
|
45
|
+
'normalize relative to entry counts') { |@normalize| }
|
46
|
+
o.on('--start=TIME', 'start time') { |t| @starttime = Time.parse t }
|
47
|
+
o.on('--end=TIME', 'end time') { |t| @endtime = Time.parse t }
|
48
|
+
o.on('-m', '--mode=MODE',
|
49
|
+
'mode (required) one of {tags, security}') do |mode|
|
50
|
+
case mode
|
51
|
+
when 'tags'
|
52
|
+
@mode = :tags
|
53
|
+
when 'security'
|
54
|
+
@mode = :security
|
55
|
+
else
|
56
|
+
die "bad mode #{mode}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
self
|
61
|
+
end
|
62
|
+
def parse!(argv)
|
63
|
+
@opts.parse! ARGV
|
64
|
+
check_params
|
65
|
+
self
|
66
|
+
end
|
67
|
+
def die(msg)
|
68
|
+
puts msg
|
69
|
+
puts @opts
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
def check_params
|
73
|
+
die "must specify database." unless @dbfile
|
74
|
+
die "must specify output file." unless @outfile
|
75
|
+
die "must specify mode." unless @mode
|
76
|
+
|
77
|
+
case @mode
|
78
|
+
when :tags
|
79
|
+
@tags = ARGV
|
80
|
+
unless @tags.length > 0
|
81
|
+
die "must specify at least one tag as an extra command-line parameter."
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
$opts = Options.new.parse! ARGV
|
87
|
+
|
88
|
+
class DataSet
|
89
|
+
# store a series of (time, class, count) observations.
|
90
|
+
|
91
|
+
def initialize
|
92
|
+
# data: klass -> time -> count
|
93
|
+
@data = Hash.new { |h,k| h[k] = Hash.new { |h,k| h[k] = 0 } }
|
94
|
+
@firsttime = @lasttime = nil
|
95
|
+
@bucketing = :month
|
96
|
+
end
|
97
|
+
|
98
|
+
def bucket(time)
|
99
|
+
case @bucketing
|
100
|
+
when :month
|
101
|
+
mon = time.month
|
102
|
+
if $opts.smooth != 1
|
103
|
+
# when smooth = 3,
|
104
|
+
# 1 -> 1
|
105
|
+
# 3 -> 1
|
106
|
+
# 4 -> 4
|
107
|
+
# 5 -> 4
|
108
|
+
# 12 -> 10
|
109
|
+
mon = ((mon-1)/$opts.smooth * $opts.smooth) + 1
|
110
|
+
end
|
111
|
+
"%d-%02d" % [time.year, mon]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def each_bucket
|
116
|
+
case @bucketing
|
117
|
+
when :month
|
118
|
+
time = Date.parse(@firsttime.strftime('%Y-%m-01'))
|
119
|
+
lasttime = Date.parse(@lasttime.strftime('%Y-%m-01'))
|
120
|
+
lastbucket = nil
|
121
|
+
while time < lasttime
|
122
|
+
b = bucket(time)
|
123
|
+
yield b if b != lastbucket
|
124
|
+
lastbucket = b
|
125
|
+
time = time >> 1
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def labels(count=5)
|
131
|
+
total = 0
|
132
|
+
each_bucket { total += 1 }
|
133
|
+
skip = (total / count.to_f).round
|
134
|
+
|
135
|
+
labels = {}
|
136
|
+
i = 0
|
137
|
+
each_bucket do |bucket|
|
138
|
+
labels[i] = bucket if i % skip == 0
|
139
|
+
i += 1
|
140
|
+
end
|
141
|
+
labels
|
142
|
+
end
|
143
|
+
|
144
|
+
def get_class(klass)
|
145
|
+
buckets = []
|
146
|
+
data = @data[klass]
|
147
|
+
each_bucket do |bucket|
|
148
|
+
buckets << if data.has_key? bucket
|
149
|
+
if $opts.normalize
|
150
|
+
data[bucket] / @data[:total][bucket].to_f
|
151
|
+
else
|
152
|
+
data[bucket]
|
153
|
+
end
|
154
|
+
else
|
155
|
+
0
|
156
|
+
end
|
157
|
+
end
|
158
|
+
buckets
|
159
|
+
end
|
160
|
+
|
161
|
+
def add(klass, time, count=1)
|
162
|
+
@firsttime = time if @firsttime.nil? or time < @firsttime
|
163
|
+
@lasttime = time if @lasttime.nil? or time > @lasttime
|
164
|
+
@data[klass][bucket(time)] += count
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
dataset = DataSet.new
|
169
|
+
db = LiveJournal::Database.new $opts.dbfile
|
170
|
+
total_entries = 0
|
171
|
+
|
172
|
+
db.each_entry do |entry|
|
173
|
+
next if $opts.starttime and $opts.starttime > entry.time
|
174
|
+
next if $opts.endtime and $opts.endtime < entry.time
|
175
|
+
|
176
|
+
dataset.add(:total, entry.time, 1)
|
177
|
+
case $opts.mode
|
178
|
+
when :tags
|
179
|
+
entry.taglist.each do |tag|
|
180
|
+
dataset.add(tag, entry.time)
|
181
|
+
end
|
182
|
+
when :security
|
183
|
+
case entry.security
|
184
|
+
when :public; dataset.add(:public, entry.time)
|
185
|
+
when :friends; dataset.add(:friends, entry.time)
|
186
|
+
else; dataset.add(:other, entry.time)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
total_entries += 1
|
190
|
+
end
|
191
|
+
|
192
|
+
g = Gruff::Line.new($opts.geom)
|
193
|
+
g.labels = dataset.labels
|
194
|
+
case $opts.mode
|
195
|
+
when :tags
|
196
|
+
g.title = 'Tag Rate'
|
197
|
+
$opts.tags.each do |tag|
|
198
|
+
g.data(tag, dataset.get_class(tag))
|
199
|
+
end
|
200
|
+
when :security
|
201
|
+
g.title = 'Entry Security'
|
202
|
+
[:public, :friends, :other].each do |sec|
|
203
|
+
g.data(sec.to_s, dataset.get_class(sec))
|
204
|
+
end
|
205
|
+
end
|
206
|
+
g.write $opts.outfile
|
207
|
+
|
208
|
+
# vim: ts=2 sw=2 et :
|