livejournal 0.0.1
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.
- data/LICENSE +17 -0
- data/README +50 -0
- data/Rakefile +60 -0
- data/lib/livejournal/basic.rb +59 -0
- data/lib/livejournal/comment.rb +82 -0
- data/lib/livejournal/comments-xml.rb +158 -0
- data/lib/livejournal/database.rb +292 -0
- data/lib/livejournal/entry.rb +280 -0
- data/lib/livejournal/friends.rb +102 -0
- data/lib/livejournal/login.rb +43 -0
- data/lib/livejournal/logjam.rb +76 -0
- data/lib/livejournal/request.rb +126 -0
- data/lib/livejournal/sync.rb +185 -0
- data/sample/export +154 -0
- data/sample/lj +23 -0
- data/setup.rb +1585 -0
- data/test/checkfriends.rb +29 -0
- data/test/comments-xml.rb +108 -0
- data/test/database.rb +54 -0
- data/test/time.rb +49 -0
- metadata +60 -0
data/LICENSE
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
2
|
+
of this software and associated documentation files (the "Software"), to deal
|
3
|
+
in the Software without restriction, including without limitation the rights
|
4
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
5
|
+
copies of the Software, and to permit persons to whom the Software is
|
6
|
+
furnished to do so, subject to the following conditions:
|
7
|
+
|
8
|
+
The above copyright notice and this permission notice shall be included in
|
9
|
+
all copies or substantial portions of the Software.
|
10
|
+
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
12
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
13
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
14
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
15
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
16
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
17
|
+
SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
=ljrb: LiveJournal Ruby module
|
2
|
+
|
3
|
+
Copyright:: Copyright (C) 2005 Evan Martin <martine@danga.com>
|
4
|
+
Website:: http://neugierig.org/software/livejournal/ruby
|
5
|
+
|
6
|
+
Example usage:
|
7
|
+
require 'livejournal/login'
|
8
|
+
|
9
|
+
puts "Logging in..."
|
10
|
+
user = LiveJournal::User.new('test', 'test')
|
11
|
+
login = LiveJournal::Request::Login.new(user)
|
12
|
+
login.run
|
13
|
+
|
14
|
+
puts "Login response:"
|
15
|
+
login.dumpresponse
|
16
|
+
|
17
|
+
puts "User's full name: #{user.fullname}"
|
18
|
+
|
19
|
+
==LiveJournal Datatypes
|
20
|
+
* LiveJournal::Server
|
21
|
+
* LiveJournal::User
|
22
|
+
* LiveJournal::Entry
|
23
|
+
* LiveJournal::Comment
|
24
|
+
* LiveJournal::Friend
|
25
|
+
|
26
|
+
==Implemented Requests
|
27
|
+
===Login Requests
|
28
|
+
* LiveJournal::Request::Login
|
29
|
+
* LiveJournal::Request::SessionGenerate (only useful for syncing)
|
30
|
+
===Friend Requests
|
31
|
+
* LiveJournal::Request::Friends
|
32
|
+
* LiveJournal::Request::FriendOfs
|
33
|
+
* LiveJournal::Request::CheckFriends
|
34
|
+
===Entry Requests
|
35
|
+
* LiveJournal::Request::GetEvents
|
36
|
+
* LiveJournal::Request::EditEvent
|
37
|
+
|
38
|
+
==Journal Offline Synchronization
|
39
|
+
* LiveJournal::Sync::Entries
|
40
|
+
* LiveJournal::Sync::Comments
|
41
|
+
See samples/export for an example of how to use these.
|
42
|
+
|
43
|
+
==SQLite3 Support
|
44
|
+
* LiveJournal::Database -- storing/loading entries+comments with SQLite3
|
45
|
+
Integrates well with syncing. See samples/export.
|
46
|
+
|
47
|
+
==Other Features
|
48
|
+
* LiveJournal::LogJam -- interface with LogJam (http://logjam.danga.com) 's
|
49
|
+
journal exports
|
50
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rake/packagetask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
PKG_NAME = 'livejournal'
|
8
|
+
PKG_VERSION = '0.0.1'
|
9
|
+
|
10
|
+
FILES = FileList[
|
11
|
+
'Rakefile', 'README', 'LICENSE', 'setup.rb',
|
12
|
+
'lib/**/*', 'sample/*', 'test/*'
|
13
|
+
]
|
14
|
+
|
15
|
+
spec = Gem::Specification.new do |s|
|
16
|
+
s.name = PKG_NAME
|
17
|
+
s.version = PKG_VERSION
|
18
|
+
s.summary = 'module for interacting with livejournal'
|
19
|
+
s.description = %q{LiveJournal module. Post to livejournal, retrieve friends
|
20
|
+
lists, edit entries, sync journal to an offline database.}
|
21
|
+
s.author = 'Evan Martin'
|
22
|
+
s.email = 'martine@danga.com'
|
23
|
+
s.homepage = 'http://neugierig.org/software/livejournal/ruby/'
|
24
|
+
|
25
|
+
s.has_rdoc = true
|
26
|
+
s.files = FILES.to_a
|
27
|
+
end
|
28
|
+
|
29
|
+
desc 'Build Package'
|
30
|
+
Rake::GemPackageTask.new(spec) do |p|
|
31
|
+
p.need_tar = true
|
32
|
+
p.need_zip = true
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
desc 'Generate RDoc'
|
37
|
+
Rake::RDocTask.new :rdoc do |rd|
|
38
|
+
rd.title = "ljrb (LiveJournal Ruby module) Documentation"
|
39
|
+
rd.rdoc_dir = 'doc'
|
40
|
+
rd.rdoc_files.add 'lib', 'README', 'LICENSE'
|
41
|
+
rd.main = 'README'
|
42
|
+
end
|
43
|
+
|
44
|
+
desc 'Run Tests'
|
45
|
+
Rake::TestTask.new :test do |t|
|
46
|
+
t.test_files = FileList['test/*.rb']
|
47
|
+
end
|
48
|
+
|
49
|
+
desc 'Push data to my webspace'
|
50
|
+
task :pushweb => [:rdoc, :package] do
|
51
|
+
pkg = "pkg/#{PKG_NAME}-#{PKG_VERSION}"
|
52
|
+
target = 'neugierig.org:/home/martine/www/neugierig/htdocs/software/livejournal/ruby'
|
53
|
+
sh %{rsync -av --delete doc/* #{target}/doc/}
|
54
|
+
sh %{rsync -av #{pkg}.* #{target}/download/}
|
55
|
+
end
|
56
|
+
|
57
|
+
desc 'Push everything'
|
58
|
+
task :push => [:pushweb] # XXX push to rubyforge
|
59
|
+
|
60
|
+
# vim: set ts=2 sw=2 et :
|
@@ -0,0 +1,59 @@
|
|
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
|
+
module LiveJournal
|
26
|
+
# A LiveJournal server. name is currently unused.
|
27
|
+
class Server
|
28
|
+
attr_accessor :name, :url
|
29
|
+
|
30
|
+
def initialize(name, url)
|
31
|
+
@name = name
|
32
|
+
@url = url
|
33
|
+
end
|
34
|
+
end
|
35
|
+
DEFAULT_SERVER = Server.new("LiveJournal.com", "http://www.livejournal.com")
|
36
|
+
|
37
|
+
# A LiveJournal user. Given a username, password, and server, running a
|
38
|
+
# LiveJournal::Request::Login will fill in the other fields.
|
39
|
+
class User
|
40
|
+
# parameter when creating a User
|
41
|
+
attr_accessor :username, :password, :server
|
42
|
+
# Set usejournal to log in as user username but act as user usejournal.
|
43
|
+
# For example, to work with a community you own.
|
44
|
+
attr_accessor :usejournal
|
45
|
+
# User's self-reported name, as retrieved by LiveJournal::Request::Login
|
46
|
+
attr_accessor :fullname
|
47
|
+
def initialize(username=nil, password=nil, server=nil)
|
48
|
+
@username = username
|
49
|
+
@password = password
|
50
|
+
@usejournal = nil
|
51
|
+
@server = server || LiveJournal::DEFAULT_SERVER
|
52
|
+
end
|
53
|
+
def to_s
|
54
|
+
"#{@username}: '#{@fullname}'"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# vim: ts=2 sw=2 et :
|
@@ -0,0 +1,82 @@
|
|
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
|
+
# LiveJournal comments.
|
26
|
+
|
27
|
+
# http://www.livejournal.com/developer/exporting.bml
|
28
|
+
|
29
|
+
module LiveJournal
|
30
|
+
class Comment
|
31
|
+
attr_accessor :commentid, :posterid, :itemid, :parentid
|
32
|
+
# State of the comment. Possible values: {+:active+, +:screened+, +:deleted+}
|
33
|
+
attr_accessor :state
|
34
|
+
attr_accessor :subject, :body
|
35
|
+
# a Ruby Time object
|
36
|
+
attr_reader :time
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
@commentid = @posterid = @itemid = @parentid = nil
|
40
|
+
@subject = @body = nil
|
41
|
+
@time = nil
|
42
|
+
@state = :active
|
43
|
+
end
|
44
|
+
|
45
|
+
# Convert a state to the string representation used by LiveJournal.
|
46
|
+
def self.state_from_string(str)
|
47
|
+
case str
|
48
|
+
when nil; :active
|
49
|
+
when 'A'; :active
|
50
|
+
when 'D'; :deleted
|
51
|
+
when 'S'; :screened
|
52
|
+
else raise ArgumentError, "Invalid comment state: #{str.inspect}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Convert a state from the string representation used by LiveJournal.
|
57
|
+
def self.state_to_string state
|
58
|
+
case state
|
59
|
+
when nil; nil
|
60
|
+
when :active; nil
|
61
|
+
when :deleted; 'D'
|
62
|
+
when :screened; 'S'
|
63
|
+
else raise ArgumentError, "Invalid comment state: #{state.inspect}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def time=(time)
|
68
|
+
raise RuntimeError, "Must use GMT times everywhere to reduce confusion. See LiveJournal::coerce_gmt for details." unless time.gmt?
|
69
|
+
@time = time
|
70
|
+
end
|
71
|
+
|
72
|
+
def ==(other)
|
73
|
+
[:commentid, :posterid, :state, :itemid, :parentid,
|
74
|
+
:subject, :body, :time].each do |attr|
|
75
|
+
return false if send(attr) != other.send(attr)
|
76
|
+
end
|
77
|
+
return true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# vim: ts=2 sw=2 et :
|
@@ -0,0 +1,158 @@
|
|
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
|
+
# REXML is pleasant to use but hella slow, so we allow using the expat-based
|
26
|
+
# parser as well.
|
27
|
+
|
28
|
+
require 'livejournal/comment'
|
29
|
+
require 'time' # parsing xmlschema times
|
30
|
+
|
31
|
+
module LiveJournal
|
32
|
+
HAVE_XML_PARSER = true
|
33
|
+
|
34
|
+
require 'rexml/document'
|
35
|
+
require 'xml/parser' if HAVE_XML_PARSER
|
36
|
+
|
37
|
+
module Sync
|
38
|
+
module CommentsXML
|
39
|
+
def self.optional_int_string(x)
|
40
|
+
return nil unless x
|
41
|
+
x.to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.load_comment_from_attrs(comment, attrs)
|
45
|
+
comment.commentid = attrs['id'].to_i
|
46
|
+
comment.posterid = CommentsXML::optional_int_string attrs['posterid']
|
47
|
+
comment.itemid = CommentsXML::optional_int_string attrs['jitemid']
|
48
|
+
comment.parentid = CommentsXML::optional_int_string attrs['parentid']
|
49
|
+
statestr = attrs['state']
|
50
|
+
comment.state = LiveJournal::Comment::state_from_string(statestr) if statestr
|
51
|
+
end
|
52
|
+
|
53
|
+
class Base
|
54
|
+
attr_reader :maxid, :comments, :usermap
|
55
|
+
def initialize(data=nil)
|
56
|
+
@maxid = nil
|
57
|
+
@comments = {}
|
58
|
+
@usermap = {}
|
59
|
+
parse data if data
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class WithREXML < Base
|
64
|
+
def parse(data)
|
65
|
+
doc = REXML::Document.new(data)
|
66
|
+
root = doc.root
|
67
|
+
|
68
|
+
root.elements.each('maxid') { |e| @maxid = e.text.to_i }
|
69
|
+
|
70
|
+
root.elements.each('comments/comment') do |e|
|
71
|
+
id = e.attributes['id'].to_i
|
72
|
+
comment = @comments[id] || Comment.new
|
73
|
+
CommentsXML::load_comment_from_attrs(comment, e.attributes)
|
74
|
+
e.elements.each('subject') { |s| comment.subject = s.text }
|
75
|
+
e.elements.each('body') { |s| comment.body = s.text }
|
76
|
+
e.elements.each('date') { |s| comment.time = Time::xmlschema s.text }
|
77
|
+
@comments[id] = comment
|
78
|
+
end
|
79
|
+
|
80
|
+
root.elements.each('usermaps/usermap') do |e|
|
81
|
+
id = e.attributes['id'].to_i
|
82
|
+
user = e.attributes['user']
|
83
|
+
@usermap[id] = user
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
if HAVE_XML_PARSER
|
89
|
+
class WithExpat < Base
|
90
|
+
class Parser < XMLParser
|
91
|
+
attr_reader :maxid, :comments, :usermap
|
92
|
+
def initialize
|
93
|
+
super
|
94
|
+
@maxid = nil
|
95
|
+
@cur_comment = nil
|
96
|
+
@comments = {}
|
97
|
+
@usermap = {}
|
98
|
+
@content = nil
|
99
|
+
end
|
100
|
+
def startElement(name, attrs)
|
101
|
+
case name
|
102
|
+
when 'maxid'
|
103
|
+
@content = ''
|
104
|
+
when 'comment'
|
105
|
+
id = attrs['id'].to_i
|
106
|
+
@cur_comment = @comments[id] || Comment.new
|
107
|
+
@comments[id] = @cur_comment
|
108
|
+
CommentsXML::load_comment_from_attrs(@cur_comment, attrs)
|
109
|
+
when 'usermap'
|
110
|
+
id = attrs['id'].to_i
|
111
|
+
user = attrs['user']
|
112
|
+
@usermap[id] = user
|
113
|
+
when 'date'
|
114
|
+
@content = ''
|
115
|
+
when 'subject'
|
116
|
+
@content = ''
|
117
|
+
when 'body'
|
118
|
+
@content = ''
|
119
|
+
end
|
120
|
+
end
|
121
|
+
def character(data)
|
122
|
+
@content << data if @content
|
123
|
+
end
|
124
|
+
def endElement(name)
|
125
|
+
return unless @content
|
126
|
+
case name
|
127
|
+
when 'maxid'
|
128
|
+
@maxid = @content.to_i
|
129
|
+
when 'date'
|
130
|
+
@cur_comment.time = Time::xmlschema(@content)
|
131
|
+
when 'subject'
|
132
|
+
@cur_comment.subject = @content
|
133
|
+
when 'body'
|
134
|
+
@cur_comment.body = @content
|
135
|
+
end
|
136
|
+
@content = nil
|
137
|
+
end
|
138
|
+
end
|
139
|
+
def parse(data)
|
140
|
+
parser = Parser.new
|
141
|
+
parser.parse(data)
|
142
|
+
@maxid = parser.maxid
|
143
|
+
@comments = parser.comments
|
144
|
+
@usermap = parser.usermap
|
145
|
+
end
|
146
|
+
end # class WithExpat
|
147
|
+
end # if HAVE_XML_PARSER
|
148
|
+
|
149
|
+
if HAVE_XML_PARSER
|
150
|
+
Parser = WithExpat
|
151
|
+
else
|
152
|
+
Parser = WithREXML
|
153
|
+
end
|
154
|
+
end # module CommentsXML
|
155
|
+
end # module Sync
|
156
|
+
end # module LiveJournal
|
157
|
+
|
158
|
+
# vim: ts=2 sw=2 et :
|
@@ -0,0 +1,292 @@
|
|
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
|
+
# A SQLite database dump.
|
31
|
+
class DatabaseError < RuntimeError; end
|
32
|
+
class Database
|
33
|
+
EXPECTED_DATABASE_VERSION = "1"
|
34
|
+
SCHEMA = %q{
|
35
|
+
CREATE TABLE meta (
|
36
|
+
key STRING PRIMARY KEY,
|
37
|
+
value STRING
|
38
|
+
);
|
39
|
+
CREATE TABLE entry (
|
40
|
+
itemid INTEGER PRIMARY KEY,
|
41
|
+
anum INTEGER,
|
42
|
+
subject STRING,
|
43
|
+
event STRING,
|
44
|
+
moodid INTEGER, mood STRING, music STRING, taglist STRING,
|
45
|
+
pickeyword STRING, preformatted INTEGER, backdated INTEGER,
|
46
|
+
comments INTEGER, year INTEGER, month INTEGER, day INTEGER,
|
47
|
+
timestamp INTEGER, security INTEGER
|
48
|
+
);
|
49
|
+
CREATE INDEX dateindex ON entry (year, month, day);
|
50
|
+
CREATE INDEX timeindex ON entry (timestamp);
|
51
|
+
CREATE TABLE comment (
|
52
|
+
commentid INTEGER PRIMARY KEY,
|
53
|
+
posterid INTEGER,
|
54
|
+
itemid INTEGER,
|
55
|
+
parentid INTEGER,
|
56
|
+
state STRING, -- screened/deleted/active
|
57
|
+
subject STRING,
|
58
|
+
body STRING,
|
59
|
+
timestamp INTEGER -- unix timestamp
|
60
|
+
);
|
61
|
+
CREATE INDEX commententry ON comment (itemid);
|
62
|
+
CREATE TABLE users (
|
63
|
+
userid INTEGER PRIMARY KEY,
|
64
|
+
username STRING
|
65
|
+
);
|
66
|
+
CREATE TABLE commentprop (
|
67
|
+
commentid INTEGER, -- not primary key 'cause non-unique
|
68
|
+
key STRING,
|
69
|
+
value STRING
|
70
|
+
);
|
71
|
+
}.gsub(/^ /, '')
|
72
|
+
|
73
|
+
def self.optional_to_i(x)
|
74
|
+
return nil if x.nil?
|
75
|
+
return x.to_i
|
76
|
+
end
|
77
|
+
|
78
|
+
attr_reader :db
|
79
|
+
def initialize(filename, create=false)
|
80
|
+
exists = FileTest::exists? filename if create
|
81
|
+
@db = SQLite3::Database.new(filename)
|
82
|
+
|
83
|
+
# We'd like to use type translation, but it unfortunately fails on MAX()
|
84
|
+
# queries.
|
85
|
+
# @db.type_translation = true
|
86
|
+
|
87
|
+
if exists
|
88
|
+
version = self.version
|
89
|
+
unless version == EXPECTED_DATABASE_VERSION
|
90
|
+
raise DatabaseError, "Database version mismatch -- db has #{version.inspect}, expected #{EXPECTED_DATABASE_VERSION.inspect}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
#trace!
|
95
|
+
if create and not exists
|
96
|
+
run_schema!
|
97
|
+
self.version = EXPECTED_DATABASE_VERSION
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def transaction
|
102
|
+
@db.transaction { yield }
|
103
|
+
end
|
104
|
+
|
105
|
+
def close
|
106
|
+
@db.close
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.db_value(name, sym)
|
110
|
+
class_eval %{def #{sym}; get_meta(#{name.inspect}); end}
|
111
|
+
class_eval %{def #{sym}=(v); set_meta(#{name.inspect}, v); end}
|
112
|
+
end
|
113
|
+
|
114
|
+
db_value 'username', :username
|
115
|
+
db_value 'lastsync', :lastsync
|
116
|
+
db_value 'version', :version
|
117
|
+
|
118
|
+
def run_schema!
|
119
|
+
transaction do
|
120
|
+
@db.execute_batch(SCHEMA)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Turn tracing on.
|
125
|
+
def trace!
|
126
|
+
@db.trace() do |data, sql|
|
127
|
+
puts "SQL> #{sql.inspect}"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Fetch a specific itemid.
|
132
|
+
def get_entry(itemid)
|
133
|
+
query_entry("select * from entry where itemid=?", itemid)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Given SQL that selects an entry, return that entry.
|
137
|
+
def query_entry(sql, *sqlargs)
|
138
|
+
row = @db.get_first_row(sql, *sqlargs)
|
139
|
+
return Entry.new.load_from_database_row(row)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Given SQL that selects some entries, yield each entry.
|
143
|
+
def query_entries(sql, *sqlargs)
|
144
|
+
@db.execute(sql, *sqlargs) do |row|
|
145
|
+
yield Entry.new.load_from_database_row(row)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Yield most recent limit entries.
|
150
|
+
def each_entry(limit=nil, &block)
|
151
|
+
sql = 'SELECT * FROM entry ORDER BY itemid DESC'
|
152
|
+
sql += " LIMIT #{limit}" if limit
|
153
|
+
query_entries sql, &block
|
154
|
+
end
|
155
|
+
|
156
|
+
# Return the total number of entries.
|
157
|
+
def total_entry_count
|
158
|
+
@db.get_first_value('SELECT COUNT(*) FROM entry').to_i
|
159
|
+
end
|
160
|
+
|
161
|
+
def store_entry entry
|
162
|
+
sql = 'INSERT OR REPLACE INTO entry VALUES (' + ("?, " * 16) + '?)'
|
163
|
+
@db.execute(sql, *entry.to_database_row)
|
164
|
+
end
|
165
|
+
|
166
|
+
def last_comment_meta
|
167
|
+
Database::optional_to_i(
|
168
|
+
@db.get_first_value('SELECT MAX(commentid) FROM comment'))
|
169
|
+
end
|
170
|
+
def last_comment_full
|
171
|
+
Database::optional_to_i(
|
172
|
+
@db.get_first_value('SELECT MAX(commentid) FROM comment ' +
|
173
|
+
'WHERE body IS NOT NULL'))
|
174
|
+
end
|
175
|
+
|
176
|
+
def _store_comments(comments, meta_only=true)
|
177
|
+
transaction do
|
178
|
+
sql = "INSERT OR REPLACE INTO comment "
|
179
|
+
if meta_only
|
180
|
+
sql += "(commentid, posterid, state) VALUES (?, ?, ?)"
|
181
|
+
else
|
182
|
+
sql += "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
183
|
+
end
|
184
|
+
@db.prepare(sql) do |stmt|
|
185
|
+
comments.each do |id, comment|
|
186
|
+
if meta_only
|
187
|
+
stmt.execute(comment.commentid, comment.posterid,
|
188
|
+
LiveJournal::Comment::state_to_string(comment.state))
|
189
|
+
else
|
190
|
+
stmt.execute(*comment.to_database_row)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def store_comments_meta(comments)
|
198
|
+
_store_comments(comments, true)
|
199
|
+
end
|
200
|
+
def store_comments_full(comments)
|
201
|
+
_store_comments(comments, false)
|
202
|
+
end
|
203
|
+
|
204
|
+
def store_usermap(usermap)
|
205
|
+
transaction do
|
206
|
+
sql = "INSERT OR REPLACE INTO users VALUES (?, ?)"
|
207
|
+
@db.prepare(sql) do |stmt|
|
208
|
+
usermap.each do |id, user|
|
209
|
+
stmt.execute(id, user)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def get_meta key
|
216
|
+
return @db.get_first_value('SELECT value FROM meta WHERE key=?', key)
|
217
|
+
end
|
218
|
+
def set_meta key, value
|
219
|
+
db.transaction do
|
220
|
+
@db.execute('INSERT OR REPLACE INTO meta VALUES (?, ?)', key, value)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
class Entry
|
226
|
+
# Parse an entry from a row from the database.
|
227
|
+
def load_from_database_row row
|
228
|
+
@itemid, @anum = row[0].to_i, row[1].to_i
|
229
|
+
@subject, @event = row[2], row[3]
|
230
|
+
@moodid, @mood = row[4].nil? ? nil : row[4].to_i, row[5]
|
231
|
+
@music, @taglist, @pickeyword = row[6], row[7], row[8]
|
232
|
+
@taglist = if @taglist then @taglist.split(/, /) else [] end
|
233
|
+
@preformatted, @backdated = !row[9].nil?, !row[10].nil?
|
234
|
+
@comments = case Database::optional_to_i(row[11])
|
235
|
+
when nil; :normal
|
236
|
+
when 1; :none
|
237
|
+
when 2; :noemail
|
238
|
+
else raise DatabaseError, "Bad comments value: #{row[11].inspect}"
|
239
|
+
end
|
240
|
+
|
241
|
+
@time = Time.at(row[15].to_i).utc
|
242
|
+
|
243
|
+
case Database::optional_to_i(row[16])
|
244
|
+
when nil
|
245
|
+
@security = :public
|
246
|
+
when 0
|
247
|
+
@security = :private
|
248
|
+
when 1
|
249
|
+
@security = :friends
|
250
|
+
else
|
251
|
+
@security = :custom
|
252
|
+
@allowmask = row[16]
|
253
|
+
end
|
254
|
+
|
255
|
+
self
|
256
|
+
end
|
257
|
+
def to_database_row
|
258
|
+
comments = case @comments
|
259
|
+
when :normal; nil
|
260
|
+
when :none; 1
|
261
|
+
when :noemail; 2
|
262
|
+
end
|
263
|
+
security = case @security
|
264
|
+
when :public; nil
|
265
|
+
when :private; 0
|
266
|
+
when :friends; 1
|
267
|
+
when :custom; @allowmask
|
268
|
+
end
|
269
|
+
[@itemid, @anum, @subject, @event,
|
270
|
+
@moodid, @mood, @music, @taglist.join(', '), @pickeyword,
|
271
|
+
@preformatted ? 1 : nil, @backdated ? 1 : nil, comments,
|
272
|
+
@time.year, @time.mon, @time.day, @time.to_i, security]
|
273
|
+
end
|
274
|
+
end
|
275
|
+
class Comment
|
276
|
+
def load_from_database_row row
|
277
|
+
@commentid, @posterid = row[0].to_i, row[1].to_i
|
278
|
+
@itemid, @parentid = row[2].to_i, row[3].to_i
|
279
|
+
@state = Comment::state_from_string row[4]
|
280
|
+
@subject, @body = row[5], row[6]
|
281
|
+
@time = Time.at(row[7]).utc
|
282
|
+
self
|
283
|
+
end
|
284
|
+
def to_database_row
|
285
|
+
state = Comment::state_to_string @state
|
286
|
+
[@commentid, @posterid, @itemid, @parentid,
|
287
|
+
state, @subject, @body, @time.to_i]
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# vim: ts=2 sw=2 et :
|