libertree-model 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/lib/libertree/compat.rb +29 -0
  3. data/lib/libertree/console.rb +17 -0
  4. data/lib/libertree/db.rb +37 -0
  5. data/lib/libertree/embedder.rb +69 -0
  6. data/lib/libertree/embedding/custom-providers.rb +148 -0
  7. data/lib/libertree/model/account-settings.rb +14 -0
  8. data/lib/libertree/model/account.rb +487 -0
  9. data/lib/libertree/model/chat-message.rb +75 -0
  10. data/lib/libertree/model/comment-like.rb +68 -0
  11. data/lib/libertree/model/comment.rb +164 -0
  12. data/lib/libertree/model/contact-list.rb +81 -0
  13. data/lib/libertree/model/embed-cache.rb +6 -0
  14. data/lib/libertree/model/file.rb +6 -0
  15. data/lib/libertree/model/forest.rb +82 -0
  16. data/lib/libertree/model/group-member.rb +13 -0
  17. data/lib/libertree/model/group.rb +47 -0
  18. data/lib/libertree/model/has-display-text.rb +34 -0
  19. data/lib/libertree/model/has-searchable-text.rb +19 -0
  20. data/lib/libertree/model/ignored-member.rb +13 -0
  21. data/lib/libertree/model/invitation.rb +6 -0
  22. data/lib/libertree/model/is-remote-or-local.rb +30 -0
  23. data/lib/libertree/model/job.rb +93 -0
  24. data/lib/libertree/model/member.rb +222 -0
  25. data/lib/libertree/model/message.rb +154 -0
  26. data/lib/libertree/model/node.rb +22 -0
  27. data/lib/libertree/model/node_affiliation.rb +14 -0
  28. data/lib/libertree/model/node_subscription.rb +23 -0
  29. data/lib/libertree/model/notification.rb +71 -0
  30. data/lib/libertree/model/pool-post.rb +17 -0
  31. data/lib/libertree/model/pool.rb +172 -0
  32. data/lib/libertree/model/post-hidden.rb +21 -0
  33. data/lib/libertree/model/post-like.rb +72 -0
  34. data/lib/libertree/model/post-revision.rb +9 -0
  35. data/lib/libertree/model/post.rb +735 -0
  36. data/lib/libertree/model/profile.rb +25 -0
  37. data/lib/libertree/model/remote-storage-connection.rb +6 -0
  38. data/lib/libertree/model/river.rb +249 -0
  39. data/lib/libertree/model/server.rb +35 -0
  40. data/lib/libertree/model/session-account.rb +10 -0
  41. data/lib/libertree/model/url-expansion.rb +6 -0
  42. data/lib/libertree/model.rb +48 -0
  43. data/lib/libertree/query.rb +167 -0
  44. data/lib/libertree/render.rb +28 -0
  45. metadata +198 -0
@@ -0,0 +1,25 @@
1
+ module Libertree
2
+ module Model
3
+ class Profile < Sequel::Model(:profiles)
4
+ def after_update
5
+ super
6
+ if self.member.local?
7
+ Libertree::Model::Job.create_for_forests(
8
+ {
9
+ task: 'request:MEMBER',
10
+ params: { 'username' => self.member.account.username, }
11
+ }
12
+ )
13
+ end
14
+ end
15
+
16
+ def member
17
+ @member ||= Member[ self.member_id ]
18
+ end
19
+
20
+ def self.search(query)
21
+ self.where("(to_tsvector('simple', description) || to_tsvector('english', description)) @@ plainto_tsquery(?)", query).or("name_display ILIKE '%' || ? || '%'", query)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ module Libertree
2
+ module Model
3
+ class RemoteStorageConnection < Sequel::Model(:remote_storage_connections)
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,249 @@
1
+ module Libertree
2
+ module Model
3
+ class River < Sequel::Model(:rivers)
4
+ def account
5
+ @account ||= Account[self.account_id]
6
+ end
7
+
8
+ def should_contain?( post )
9
+ ! self.contains?(post) && ! post.hidden_by?(self.account) && self.matches_post?(post)
10
+ end
11
+
12
+ def contains?( post )
13
+ Libertree::DB.dbh[ "SELECT river_contains_post(?, ?)", self.id, post.id ].single_value
14
+ end
15
+
16
+ def add_post( post )
17
+ Libertree::DB.dbh[ "INSERT INTO river_posts ( river_id, post_id ) VALUES ( ?, ? )", self.id, post.id ].get
18
+ end
19
+
20
+ def posts( opts = {} )
21
+ time = Time.at( opts.fetch(:time, Time.now.to_f) ).strftime("%Y-%m-%d %H:%M:%S.%6N%z")
22
+ Post.s(%{SELECT * FROM posts_in_river(?,?,?,?,?,?)},
23
+ self.id,
24
+ self.account.id,
25
+ time,
26
+ opts[:newer],
27
+ opts[:order_by] == :comment,
28
+ opts.fetch(:limit, 30))
29
+ end
30
+
31
+ def parsed_query(override_cache=false)
32
+ return @parsed_query if @parsed_query && ! override_cache
33
+
34
+ full_query = self.query
35
+ if ! self.appended_to_all
36
+ full_query += ' ' + self.account.rivers_appended.map(&:query).join(' ')
37
+ full_query.strip!
38
+ end
39
+
40
+ @parsed_query = Libertree::Query.new(full_query, self.account.id, self.id).parsed
41
+ end
42
+
43
+ def term_matches_post?(term, post, data)
44
+ case term
45
+ when 'flag'
46
+ # TODO: most of these are slow
47
+ case data
48
+ when 'forest'
49
+ true # Every post is a post in the forest. :forest is sort of a no-op term
50
+ when 'tree'
51
+ post.local?
52
+ when 'unread'
53
+ ! post.read_by?(self.account)
54
+ when 'liked'
55
+ post.liked_by? self.account.member
56
+ when 'commented'
57
+ post.commented_on_by? self.account.member
58
+ when 'subscribed'
59
+ self.account.subscribed_to? post
60
+ end
61
+ when 'contact-list'
62
+ data.last.include? post.member_id
63
+ when 'from'
64
+ post.member_id == data
65
+ when 'river'
66
+ data.matches_post?(post)
67
+ when 'visibility'
68
+ post.visibility == data
69
+ when 'word-count'
70
+ case data
71
+ when /^< ?([0-9]+)$/
72
+ n = $1.to_i
73
+ post.text.scan(/\S+/).count < n
74
+ when /^> ?([0-9]+)$/
75
+ n = $1.to_i
76
+ post.text.scan(/\S+/).count > n
77
+ end
78
+ when 'spring'
79
+ data.includes?(post)
80
+ when 'via'
81
+ post.via == data
82
+ when 'tag'
83
+ post.hashtags.include? data
84
+ when 'phrase', 'word'
85
+ /(?:^|\b|\s)#{Regexp.escape(data)}(?:\b|\s|$)/i === post.text
86
+ end
87
+ end
88
+
89
+ def matches_post?(post, ignore_keys=[])
90
+ # Negations: Must not satisfy any of the conditions
91
+ # Requirements: Must satisfy every required condition
92
+ # Regular terms: Must satisfy at least one condition
93
+
94
+ conditions = {
95
+ negations: [],
96
+ requirements: [],
97
+ regular: []
98
+ }
99
+
100
+ query = self.parsed_query
101
+ keys = query.keys - ignore_keys
102
+ keys.each do |term|
103
+ test = lambda {|data| term_matches_post?(term, post, data)}
104
+ query[term].keys.each do |group|
105
+ conditions[group] += query[term][group].map(&test)
106
+ end
107
+ end
108
+
109
+ conditions[:negations].none? &&
110
+ conditions[:requirements].all? &&
111
+ (conditions[:regular].count > 0 ? conditions[:regular].any? : true)
112
+ end
113
+
114
+ def refresh_posts( n = 512 )
115
+ # delete posts early to avoid confusion about posts that don't
116
+ # match the new query
117
+ DB.dbh[ "DELETE FROM river_posts WHERE river_id = ?", self.id ].get
118
+
119
+ # TODO: this is slow despite indices.
120
+ #posts = Post.where{|p| ~Sequel.function(:post_hidden_by_account, p.id, account.id)}
121
+
122
+ # get posts that are not hidden by account and get cracking
123
+ posts = Post.filter_by_query(self.parsed_query, self.account, Post.not_hidden_by(account))
124
+
125
+ # get up to n posts
126
+ # this is faster than using find_all on the set
127
+ count = 0
128
+ matching = []
129
+ posts.reverse_order(:id).each do |post|
130
+ break if count >= n
131
+
132
+ if res = self.matches_post?(post, ['flag', 'word', 'phrase', 'tag', 'visibility', 'from', 'via'])
133
+ count += 1
134
+ matching << post
135
+ end
136
+ end
137
+
138
+ if matching.any?
139
+ DB.dbh[ "INSERT INTO river_posts SELECT ?, id FROM posts WHERE id IN ?", self.id, matching.map(&:id)].get
140
+ end
141
+ end
142
+
143
+ # @param params Untrusted parameter Hash. Be careful, this input usually comes from the outside world.
144
+ def revise( params )
145
+ self.label = params['label'].to_s
146
+ self.query = params['query'].to_s
147
+
148
+ n = River.num_appended_to_all
149
+ self.appended_to_all = !! params['appended_to_all']
150
+ if River.num_appended_to_all != n || self.appended_to_all
151
+ job_data = {
152
+ task: 'river:refresh-all',
153
+ params: {
154
+ 'account_id' => self.account_id,
155
+ }.to_json
156
+ }
157
+ existing_jobs = Job.pending_where(
158
+ %{
159
+ task = ?
160
+ AND params = ?
161
+ },
162
+ job_data[:task],
163
+ job_data[:params]
164
+ )
165
+ if existing_jobs.empty?
166
+ Job.create job_data
167
+ end
168
+ end
169
+
170
+ if ! self.appended_to_all
171
+ Libertree::Model::Job.create(
172
+ task: 'river:refresh',
173
+ params: {
174
+ 'river_id' => self.id,
175
+ }.to_json
176
+ )
177
+ end
178
+ self.save
179
+ end
180
+
181
+ def delete_cascade(force=false)
182
+ if ! force && self.appended_to_all
183
+ Libertree::Model::Job.create(
184
+ task: 'river:refresh-all',
185
+ params: {
186
+ 'account_id' => self.account_id,
187
+ }.to_json
188
+ )
189
+ end
190
+ DB.dbh["SELECT delete_cascade_river(?)", self.id].get
191
+ end
192
+
193
+ def self.num_appended_to_all
194
+ self.where(:appended_to_all).count
195
+ end
196
+
197
+ def self.create(*args)
198
+ n = River.num_appended_to_all
199
+ river = super
200
+
201
+ if River.num_appended_to_all != n
202
+ Libertree::Model::Job.create(
203
+ task: 'river:refresh-all',
204
+ params: {
205
+ 'account_id' => river.account_id,
206
+ }.to_json
207
+ )
208
+ end
209
+
210
+ if ! river.appended_to_all
211
+ Libertree::Model::Job.create(
212
+ task: 'river:refresh',
213
+ params: {
214
+ 'river_id' => river.id,
215
+ }.to_json
216
+ )
217
+ end
218
+
219
+ river
220
+ end
221
+
222
+ def home?
223
+ self.home
224
+ end
225
+
226
+ def to_hash
227
+ {
228
+ 'id' => self.id,
229
+ 'label' => self.label,
230
+ 'query' => self.query,
231
+ }
232
+ end
233
+
234
+ def being_processed?
235
+ !! Job[
236
+ task: 'river:refresh',
237
+ params: %|{"river_id":#{self.id}}|,
238
+ time_finished: nil
239
+ ]
240
+ end
241
+
242
+ def mark_all_posts_as_read
243
+ DB.dbh[ %{SELECT mark_all_posts_in_river_as_read_by(?,?)},
244
+ self.id,
245
+ self.account.id ].get
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,35 @@
1
+ module Libertree
2
+ module Model
3
+ class Server < Sequel::Model(:servers)
4
+ @@own_domain = nil
5
+
6
+ def self.own_domain=(domain)
7
+ @@own_domain = domain
8
+ end
9
+
10
+ def self.own_domain
11
+ @@own_domain
12
+ end
13
+
14
+ def name_display
15
+ self.domain || self.ip || "(unknown)"
16
+ end
17
+
18
+ def forests
19
+ Forest.s(
20
+ %{
21
+ SELECT
22
+ f.*
23
+ FROM
24
+ forests f
25
+ , forests_servers fs
26
+ WHERE
27
+ fs.server_id = ?
28
+ AND f.id = fs.forest_id
29
+ },
30
+ self.id
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,10 @@
1
+ module Libertree
2
+ module Model
3
+ class SessionAccount < Sequel::Model(:sessions_accounts)
4
+ set_primary_key [:sid]
5
+ def account
6
+ @account ||= Account[self.account_id]
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ module Libertree
2
+ module Model
3
+ class UrlExpansion < Sequel::Model(:url_expansions)
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,48 @@
1
+ require 'libertree/db'
2
+ require_relative 'compat'
3
+ Sequel::Model.plugin :dirty
4
+ Sequel::Model.plugin :compat
5
+ Sequel::Model.unrestrict_primary_key
6
+ Sequel::Model.plugin :json_serializer
7
+
8
+ Libertree::DB.dbh.extension :pg_array
9
+ Sequel.extension :pg_array_ops
10
+
11
+ require_relative 'query'
12
+
13
+ require_relative 'model/is-remote-or-local'
14
+ require_relative 'model/has-searchable-text'
15
+ require_relative 'model/has-display-text'
16
+
17
+ require_relative 'model/account'
18
+ require_relative 'model/account-settings'
19
+ require_relative 'model/chat-message'
20
+ require_relative 'model/comment'
21
+ require_relative 'model/comment-like'
22
+ require_relative 'model/contact-list'
23
+ require_relative 'model/file'
24
+ require_relative 'model/forest'
25
+ require_relative 'model/embed-cache'
26
+ require_relative 'model/ignored-member'
27
+ require_relative 'model/invitation'
28
+ require_relative 'model/group'
29
+ require_relative 'model/group-member'
30
+ require_relative 'model/job'
31
+ require_relative 'model/member'
32
+ require_relative 'model/message'
33
+ require_relative 'model/node'
34
+ require_relative 'model/node_affiliation'
35
+ require_relative 'model/node_subscription'
36
+ require_relative 'model/notification'
37
+ require_relative 'model/pool'
38
+ require_relative 'model/pool-post'
39
+ require_relative 'model/post'
40
+ require_relative 'model/post-hidden'
41
+ require_relative 'model/post-like'
42
+ require_relative 'model/post-revision'
43
+ require_relative 'model/profile'
44
+ require_relative 'model/river'
45
+ require_relative 'model/remote-storage-connection'
46
+ require_relative 'model/server'
47
+ require_relative 'model/session-account'
48
+ require_relative 'model/url-expansion'
@@ -0,0 +1,167 @@
1
+ module Libertree
2
+ class Query
3
+ private
4
+ class ParseError < StandardError; end
5
+
6
+ def patterns
7
+ {
8
+ 'phrase' => /(?<sign>[+-])?"(?<arg>[^"]+)"/,
9
+ 'from' => /(?<sign>[+-])?:from ("(?<arg>.+?)"|(?<arg>[^ ]+))/,
10
+ 'river' => /(?<sign>[+-])?:river "(?<arg>.+?)"/,
11
+ 'contact-list' => /(?<sign>[+-])?:contact-list "(?<arg>.+?)"/,
12
+ 'via' => /(?<sign>[+-])?:via "(?<arg>.+?)"/,
13
+ 'visibility' => /(?<sign>[+-])?:visibility (?<arg>[a-z-]+)/,
14
+ 'word-count' => /(?<sign>[+-])?:word-count ?(?<arg>(?<comp>[<>]) ?(?<num>[0-9]+))/,
15
+ 'spring' => /(?<sign>[+-])?:spring (?<arg>"(?<spring_name>.+?)" "(?<handle>.+?)")/,
16
+ 'flag' => /(?<sign>[+-])?:(?<arg>forest|tree|unread|liked|commented|subscribed)/,
17
+ 'tag' => /(?<sign>[+-])?#(?<arg>\S+)/,
18
+ 'word' => /(?<sign>[+-])?(?<arg>\S+)/,
19
+ }
20
+ end
21
+
22
+ def check_resource(res, term, &block)
23
+ err = if res.is_a? Array
24
+ res.any?(&:nil?)
25
+ else
26
+ res.nil?
27
+ end
28
+ if err
29
+ if @fail_on_error
30
+ fail ParseError, term
31
+ end
32
+ else
33
+ yield(*res)
34
+ end
35
+ end
36
+
37
+ # We need the river id only to prevent self-referential river
38
+ # queries. For general purpose queries this is not required.
39
+ def initialize(query, account_id, river_id=nil, fail_on_error=false)
40
+ @fail_on_error = fail_on_error
41
+ @parsed_query = Hash.new
42
+ @parsed_query.default_proc = proc do |hash,key|
43
+ hash[key] = {
44
+ :negations => [],
45
+ :requirements => [],
46
+ :regular => []
47
+ }
48
+ end
49
+
50
+ scanner = StringScanner.new(query)
51
+ until scanner.eos? do
52
+ scanner.skip(/\s+/)
53
+ patterns.each_pair do |key, pattern|
54
+ if term = scanner.scan(pattern)
55
+ match = term.match(pattern)
56
+ group = case match[:sign]
57
+ when '+'
58
+ :requirements
59
+ when '-'
60
+ :negations
61
+ else
62
+ :regular
63
+ end
64
+
65
+ case key
66
+ when
67
+ 'phrase',
68
+ 'via',
69
+ 'visibility',
70
+ 'word-count',
71
+ 'flag',
72
+ 'tag'
73
+ @parsed_query[key][group] << match[:arg]
74
+ when 'from'
75
+ # TODO: eventually remove with_display_name check
76
+ member = (Model::Member.with_handle(match[:arg]) || Model::Member.with_display_name(match[:arg]))
77
+ check_resource(member, term) do |member|
78
+ @parsed_query[key][group] << member.id
79
+ end
80
+ when 'river'
81
+ river = Model::River[label: match[:arg], account_id: account_id]
82
+ check_resource(river, term) do |river|
83
+ @parsed_query[key][group] << river if river_id && river.id != river_id
84
+ end
85
+ when 'contact-list'
86
+ list = Model::ContactList[ account_id: account_id, name: match[:arg] ]
87
+ check_resource(list, term) do |list|
88
+ ids = list.member_ids
89
+ @parsed_query[key][group] << [list.id, ids] unless ids.empty?
90
+ end
91
+ when 'spring'
92
+ # TODO: eventually remove with_display_name check
93
+ member = (Model::Member.with_handle(match[:handle]) || Model::Member.with_display_name(match[:handle]))
94
+ pool = Model::Pool[ member_id: member.id, name: match[:spring_name], sprung: true ] if member
95
+ check_resource([member, pool], term) do |list, pool|
96
+ @parsed_query[key][group] << pool
97
+ end
98
+ when 'word'
99
+ # Only treat a matched word as a simple word if it consists only of word
100
+ # characters. This excludes URLs or other terms with special characters.
101
+ if match[:arg] =~ /^[[:word:]]+$/
102
+ @parsed_query['word'][group] << match[:arg]
103
+ else
104
+ @parsed_query['phrase'][group] << match[:arg]
105
+ end
106
+ end
107
+
108
+ # move on to the next term
109
+ next @parsed_query
110
+ end
111
+ end
112
+ end
113
+ @parsed_query
114
+ end
115
+
116
+ public
117
+ def parsed
118
+ @parsed_query.dup
119
+ end
120
+
121
+ def simple
122
+ tags = @parsed_query['tag'][:regular].map {|t| "##{t}"}
123
+ rest = @parsed_query.
124
+ select {|k| ['phrase', 'word'].include? k}.
125
+ flat_map {|h| h.last[:regular]}
126
+ (tags + rest).join(' ')
127
+ end
128
+
129
+ def to_s
130
+ res = []
131
+ apply_template = lambda do |template, groups|
132
+ res += groups[:negations].map {|v| '-' + template.call(v)}
133
+ res += groups[:requirements].map {|v| '+' + template.call(v)}
134
+ res += groups[:regular].map {|v| template.call(v)}
135
+ end
136
+
137
+ @parsed_query.each_pair do |key, groups|
138
+ template = case key
139
+ when 'phrase'
140
+ lambda {|v| "\"%s\"" % v }
141
+ when 'via'
142
+ lambda {|v| ":via \"%s\"" % v }
143
+ when 'visibility'
144
+ lambda {|v| ":visibility %s" % v }
145
+ when 'word-count'
146
+ lambda {|v| ":word-count %s" % v }
147
+ when 'flag'
148
+ lambda {|v| ":%s" % v }
149
+ when 'word'
150
+ lambda {|v| v }
151
+ when 'tag'
152
+ lambda {|v| "#%s" % v }
153
+ when 'from'
154
+ lambda {|v| ":from %s" % Model::Member[v.to_i].handle }
155
+ when 'river'
156
+ lambda {|v| ":river \"%s\"" % Model::River[v.id.to_i].label }
157
+ when 'contact-list'
158
+ template = lambda {|v| ":contact-list \"%s\"" % Model::ContactList[v.first.to_i].name }
159
+ when 'spring'
160
+ lambda {|v| pool = Model::Pool[v.id.to_i]; ":spring \"%s\" \"%s\"" % [pool.name, pool.member.handle] }
161
+ end
162
+ apply_template.call(template, groups)
163
+ end
164
+ res.join(' ')
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+ require 'nokogiri'
3
+ require 'markdown'
4
+
5
+ module Libertree
6
+ module Render
7
+ Options = [ :filter_html,
8
+ :smart,
9
+ :strike,
10
+ :autolink,
11
+ :hard_wrap,
12
+ :notes,
13
+ :codeblock,
14
+ :hashtags,
15
+ :usernames,
16
+ :spoilerblock
17
+ ]
18
+
19
+ def self.to_html_string(s, opts=Options)
20
+ return '' if s.nil? or s.empty?
21
+ Markdown.new( s, *opts ).to_html.force_encoding('utf-8')
22
+ end
23
+
24
+ def self.to_html_nodeset(s, opts=Options)
25
+ Nokogiri::HTML.fragment(self.to_html_string(s, opts))
26
+ end
27
+ end
28
+ end