livejournal 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 :
|