libertree-model 0.9.11

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.
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