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.
- checksums.yaml +7 -0
- data/lib/libertree/compat.rb +29 -0
- data/lib/libertree/console.rb +17 -0
- data/lib/libertree/db.rb +37 -0
- data/lib/libertree/embedder.rb +69 -0
- data/lib/libertree/embedding/custom-providers.rb +148 -0
- data/lib/libertree/model/account-settings.rb +14 -0
- data/lib/libertree/model/account.rb +487 -0
- data/lib/libertree/model/chat-message.rb +75 -0
- data/lib/libertree/model/comment-like.rb +68 -0
- data/lib/libertree/model/comment.rb +164 -0
- data/lib/libertree/model/contact-list.rb +81 -0
- data/lib/libertree/model/embed-cache.rb +6 -0
- data/lib/libertree/model/file.rb +6 -0
- data/lib/libertree/model/forest.rb +82 -0
- data/lib/libertree/model/group-member.rb +13 -0
- data/lib/libertree/model/group.rb +47 -0
- data/lib/libertree/model/has-display-text.rb +34 -0
- data/lib/libertree/model/has-searchable-text.rb +19 -0
- data/lib/libertree/model/ignored-member.rb +13 -0
- data/lib/libertree/model/invitation.rb +6 -0
- data/lib/libertree/model/is-remote-or-local.rb +30 -0
- data/lib/libertree/model/job.rb +93 -0
- data/lib/libertree/model/member.rb +222 -0
- data/lib/libertree/model/message.rb +154 -0
- data/lib/libertree/model/node.rb +22 -0
- data/lib/libertree/model/node_affiliation.rb +14 -0
- data/lib/libertree/model/node_subscription.rb +23 -0
- data/lib/libertree/model/notification.rb +71 -0
- data/lib/libertree/model/pool-post.rb +17 -0
- data/lib/libertree/model/pool.rb +172 -0
- data/lib/libertree/model/post-hidden.rb +21 -0
- data/lib/libertree/model/post-like.rb +72 -0
- data/lib/libertree/model/post-revision.rb +9 -0
- data/lib/libertree/model/post.rb +735 -0
- data/lib/libertree/model/profile.rb +25 -0
- data/lib/libertree/model/remote-storage-connection.rb +6 -0
- data/lib/libertree/model/river.rb +249 -0
- data/lib/libertree/model/server.rb +35 -0
- data/lib/libertree/model/session-account.rb +10 -0
- data/lib/libertree/model/url-expansion.rb +6 -0
- data/lib/libertree/model.rb +48 -0
- data/lib/libertree/query.rb +167 -0
- data/lib/libertree/render.rb +28 -0
- metadata +198 -0
@@ -0,0 +1,735 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'set'
|
5
|
+
require_relative '../embedder'
|
6
|
+
|
7
|
+
module Libertree
|
8
|
+
class VisibilityExpansionError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
module Model
|
12
|
+
class Post < Sequel::Model(:posts)
|
13
|
+
VISIBILITY_RANK = {
|
14
|
+
'tree' => 0,
|
15
|
+
'forest' => 1,
|
16
|
+
'internet' => 2,
|
17
|
+
}
|
18
|
+
|
19
|
+
include IsRemoteOrLocal
|
20
|
+
extend HasSearchableText
|
21
|
+
include HasDisplayText
|
22
|
+
|
23
|
+
def before_create
|
24
|
+
self.hashtags = self.extract_hashtags
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def after_create
|
29
|
+
super
|
30
|
+
if self.local? && self.distribute?
|
31
|
+
Libertree::Model::Job.create_for_forests(
|
32
|
+
{
|
33
|
+
task: 'request:POST',
|
34
|
+
params: { 'post_id' => self.id, }
|
35
|
+
},
|
36
|
+
*self.forests
|
37
|
+
)
|
38
|
+
end
|
39
|
+
Libertree::Embedder.autoembed(self.text)
|
40
|
+
self.notify_mentioned
|
41
|
+
self.notify_group_members
|
42
|
+
end
|
43
|
+
|
44
|
+
def before_update
|
45
|
+
self.hashtags = self.extract_hashtags
|
46
|
+
super
|
47
|
+
end
|
48
|
+
|
49
|
+
def after_update
|
50
|
+
super
|
51
|
+
has_distributable_difference = (
|
52
|
+
self.previous_changes.include?(:text) ||
|
53
|
+
self.previous_changes.include?(:visibility)
|
54
|
+
)
|
55
|
+
|
56
|
+
# TODO: deny change of visibility to 'tree' visibility?
|
57
|
+
# or trigger deletion on remotes?
|
58
|
+
if self.local? && self.distribute? && has_distributable_difference
|
59
|
+
Libertree::Model::Job.create_for_forests(
|
60
|
+
{
|
61
|
+
task: 'request:POST',
|
62
|
+
params: { 'post_id' => self.id, }
|
63
|
+
},
|
64
|
+
*self.forests
|
65
|
+
)
|
66
|
+
end
|
67
|
+
Libertree::Embedder.autoembed(self.text)
|
68
|
+
end
|
69
|
+
|
70
|
+
# TODO: DB: replace with association
|
71
|
+
def member
|
72
|
+
@member ||= Member[self.member_id]
|
73
|
+
end
|
74
|
+
|
75
|
+
def time_updated_overall
|
76
|
+
[time_commented, time_updated].compact.max
|
77
|
+
end
|
78
|
+
|
79
|
+
def read_by?(account)
|
80
|
+
DB.dbh[ "SELECT EXISTS( SELECT 1 FROM posts_read WHERE post_id = ? AND account_id = ? LIMIT 1 )", self.id, account.id ].single_value
|
81
|
+
end
|
82
|
+
|
83
|
+
def mark_as_read_by(account)
|
84
|
+
DB.dbh[ "SELECT mark_post_as_read_by( ?, ? )", self.id, account.id ].get
|
85
|
+
end
|
86
|
+
|
87
|
+
def mark_as_unread_by(account)
|
88
|
+
DB.dbh[ "DELETE FROM posts_read WHERE post_id = ? AND account_id = ?", self.id, account.id ].get
|
89
|
+
account.rivers.each do |river|
|
90
|
+
if river.should_contain? self
|
91
|
+
DB.dbh[ "INSERT INTO river_posts ( river_id, post_id ) VALUES ( ?, ? )", river.id, self.id ].get
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def mark_as_unread_by_all( options = {} )
|
97
|
+
except_accounts = options.fetch(:except, [])
|
98
|
+
if except_accounts.any?
|
99
|
+
DB.dbh[:posts_read].where('post_id = ? AND NOT account_id IN ?', self.id, except_accounts.map(&:id)).delete
|
100
|
+
else
|
101
|
+
DB.dbh[ "DELETE FROM posts_read WHERE post_id = ?", self.id ].get
|
102
|
+
end
|
103
|
+
|
104
|
+
Libertree::Model::Job.create(
|
105
|
+
task: 'post:add-to-rivers',
|
106
|
+
params: { 'post_id' => self.id, }.to_json
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.mark_all_as_read_by(account)
|
111
|
+
DB.dbh[ %{SELECT mark_all_posts_as_read_by(?)}, account.id ].get
|
112
|
+
end
|
113
|
+
|
114
|
+
# @param [Hash] opt options for restricting the comment set returned. See Comment.on_post .
|
115
|
+
def comments(opt = nil)
|
116
|
+
opt ||= {} # We put this here instead of in the method signature because sometimes nil is literally sent
|
117
|
+
Comment.on_post(self, opt)
|
118
|
+
end
|
119
|
+
|
120
|
+
def commented_on_by?(member)
|
121
|
+
DB.dbh[
|
122
|
+
%{
|
123
|
+
SELECT EXISTS(
|
124
|
+
SELECT 1
|
125
|
+
FROM comments
|
126
|
+
WHERE
|
127
|
+
post_id = ?
|
128
|
+
AND member_id = ?
|
129
|
+
)
|
130
|
+
},
|
131
|
+
self.id,
|
132
|
+
member.id
|
133
|
+
].single_value
|
134
|
+
end
|
135
|
+
|
136
|
+
def likes
|
137
|
+
return @likes if @likes
|
138
|
+
@likes = PostLike.where(post_id: self.id).reverse_order(:id)
|
139
|
+
end
|
140
|
+
|
141
|
+
def notify_about_comment(comment)
|
142
|
+
notification_attributes = {
|
143
|
+
'type' => 'comment',
|
144
|
+
'comment_id' => comment.id,
|
145
|
+
}
|
146
|
+
accounts = comment.post.subscribers
|
147
|
+
if comment.member.account
|
148
|
+
accounts = accounts.select {|a| a.id != comment.member.account.id }
|
149
|
+
end
|
150
|
+
accounts.each do |account|
|
151
|
+
if ! comment.post.hidden_by?(account) && ! account.ignoring?(comment.member)
|
152
|
+
account.notify_about notification_attributes
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def notify_about_like(like)
|
158
|
+
notification_attributes = {
|
159
|
+
'type' => 'post-like',
|
160
|
+
'post_like_id' => like.id,
|
161
|
+
}
|
162
|
+
local_post_author = like.post.member.account
|
163
|
+
like_author = like.member.account
|
164
|
+
|
165
|
+
if(
|
166
|
+
local_post_author &&
|
167
|
+
(!like_author || local_post_author.id != like_author.id) &&
|
168
|
+
! local_post_author.ignoring?(like.member)
|
169
|
+
)
|
170
|
+
local_post_author.notify_about notification_attributes
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def notify_mentioned
|
175
|
+
notification_attributes = {
|
176
|
+
'type' => 'mention',
|
177
|
+
'post_id' => self.id,
|
178
|
+
}
|
179
|
+
|
180
|
+
mentioned_accounts.each do |a|
|
181
|
+
if ! a.ignoring?(self.member)
|
182
|
+
a.notify_about notification_attributes
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def notify_group_members
|
188
|
+
return if self.group.nil?
|
189
|
+
|
190
|
+
notification_attributes = {
|
191
|
+
'type' => 'group-post',
|
192
|
+
'post_id' => self.id,
|
193
|
+
}
|
194
|
+
|
195
|
+
self.group.members.each do |member|
|
196
|
+
if(
|
197
|
+
member.account &&
|
198
|
+
member != self.member &&
|
199
|
+
! member.account.ignoring?(self.member)
|
200
|
+
)
|
201
|
+
member.account.notify_about notification_attributes
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def mentioned_accounts
|
207
|
+
accounts = Set.new
|
208
|
+
|
209
|
+
# find all JIDs first
|
210
|
+
# TODO: username matching (left here for compatibility reasons) is deprecated
|
211
|
+
self.text_as_html.xpath('.//span[@rel="username"]').each do |n|
|
212
|
+
handle = n.content[1..-1].downcase
|
213
|
+
if account = Account[ username: handle ]
|
214
|
+
accounts << account
|
215
|
+
elsif member = Member.with_handle(handle)
|
216
|
+
accounts << member.account if member.account
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# remove post author from result set
|
221
|
+
accounts.delete(self.member.account)
|
222
|
+
accounts.to_a
|
223
|
+
end
|
224
|
+
|
225
|
+
def before_destroy
|
226
|
+
if self.local? && self.distribute?
|
227
|
+
Libertree::Model::Job.create_for_forests(
|
228
|
+
{
|
229
|
+
task: 'request:POST-DELETE',
|
230
|
+
params: { 'post_id' => self.id, }
|
231
|
+
},
|
232
|
+
*self.forests
|
233
|
+
)
|
234
|
+
end
|
235
|
+
super
|
236
|
+
end
|
237
|
+
|
238
|
+
# TODO: the correct method to call is "destroy"
|
239
|
+
def delete
|
240
|
+
self.before_destroy
|
241
|
+
super
|
242
|
+
end
|
243
|
+
|
244
|
+
# NOTE: deletion is NOT distributed when force=true
|
245
|
+
def delete_cascade(force=false)
|
246
|
+
self.before_destroy unless force
|
247
|
+
|
248
|
+
DB.dbh[ "SELECT delete_cascade_post(?)", self.id ].get
|
249
|
+
end
|
250
|
+
|
251
|
+
def self.create(*args)
|
252
|
+
post = super
|
253
|
+
Libertree::Model::Job.create(
|
254
|
+
task: 'post:add-to-rivers',
|
255
|
+
params: { 'post_id' => post.id, }.to_json
|
256
|
+
)
|
257
|
+
if post.member.account
|
258
|
+
post.mark_as_read_by post.member.account
|
259
|
+
post.member.account.subscribe_to post
|
260
|
+
end
|
261
|
+
post
|
262
|
+
end
|
263
|
+
|
264
|
+
# This is a search, not a create
|
265
|
+
def like_by(member)
|
266
|
+
PostLike[ member_id: member.id, post_id: self.id ]
|
267
|
+
end
|
268
|
+
def liked_by?(member)
|
269
|
+
!! like_by(member)
|
270
|
+
end
|
271
|
+
|
272
|
+
# TODO: Optionally restrict by account, so as not to reveal too much to browser/client
|
273
|
+
# i.e. rivers not belonging to current account
|
274
|
+
def rivers_belonged_to(account = nil)
|
275
|
+
query_params = [self.id]
|
276
|
+
|
277
|
+
if account
|
278
|
+
account_clause = "AND r.account_id = ?"
|
279
|
+
query_params << account.id
|
280
|
+
end
|
281
|
+
|
282
|
+
River.s(
|
283
|
+
%{
|
284
|
+
SELECT
|
285
|
+
r.*
|
286
|
+
FROM
|
287
|
+
rivers r
|
288
|
+
, river_posts rp
|
289
|
+
WHERE
|
290
|
+
rp.river_id = r.id
|
291
|
+
AND rp.post_id = ?
|
292
|
+
#{ account_clause }
|
293
|
+
},
|
294
|
+
*query_params
|
295
|
+
)
|
296
|
+
end
|
297
|
+
|
298
|
+
def extract_hashtags
|
299
|
+
self.text_as_html.xpath('.//span[@rel="hashtag"]').map do |n|
|
300
|
+
n.content[1..-1] =~ /([\p{Word}_-]+)/i
|
301
|
+
$1.downcase if $1
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def self.with_tag( opts = {} )
|
306
|
+
return [] if opts[:tag].nil? || opts[:tag].empty?
|
307
|
+
tags = Sequel.pg_array(Array(opts[:tag]).map(&:downcase))
|
308
|
+
time = Time.at( opts.fetch(:time, Time.now.to_f) ).strftime("%Y-%m-%d %H:%M:%S.%6N%z")
|
309
|
+
Post.s(%{SELECT * FROM tagged_posts(?, ?, ?, ?, ?)},
|
310
|
+
tags,
|
311
|
+
time,
|
312
|
+
opts[:newer],
|
313
|
+
opts[:order_by] == :comment,
|
314
|
+
opts.fetch(:limit, 30))
|
315
|
+
end
|
316
|
+
|
317
|
+
def subscribers
|
318
|
+
Account.s(%{
|
319
|
+
SELECT a.*
|
320
|
+
FROM accounts a, post_subscriptions ps
|
321
|
+
WHERE ps.post_id = ? AND a.id = ps.account_id}, self.id)
|
322
|
+
end
|
323
|
+
|
324
|
+
def revise(text_new, visibility = self.visibility)
|
325
|
+
if VISIBILITY_RANK[visibility] > VISIBILITY_RANK[self.visibility] && ! visibility_expandable?
|
326
|
+
raise VisibilityExpansionError
|
327
|
+
end
|
328
|
+
|
329
|
+
PostRevision.create(
|
330
|
+
post_id: self.id,
|
331
|
+
text: self.text
|
332
|
+
)
|
333
|
+
self.update(
|
334
|
+
text: text_new,
|
335
|
+
visibility: visibility,
|
336
|
+
time_updated: Time.now
|
337
|
+
)
|
338
|
+
|
339
|
+
mark_as_unread_by_all
|
340
|
+
end
|
341
|
+
|
342
|
+
private def visibility_expandable?
|
343
|
+
comments.empty? && likes.empty?
|
344
|
+
end
|
345
|
+
|
346
|
+
def hidden_by?(account)
|
347
|
+
DB.dbh[ "SELECT post_hidden_by_account(?, ?)", self.id, account.id ].single_value
|
348
|
+
end
|
349
|
+
|
350
|
+
def self.not_hidden_by(account, posts=self)
|
351
|
+
posts.
|
352
|
+
qualify.
|
353
|
+
left_outer_join(:posts_hidden,
|
354
|
+
:posts_hidden__post_id => :posts__id,
|
355
|
+
:posts_hidden__account_id => account.id).
|
356
|
+
where(:posts_hidden__post_id => nil)
|
357
|
+
end
|
358
|
+
|
359
|
+
def self.read_by(account, posts=self)
|
360
|
+
posts.
|
361
|
+
qualify.
|
362
|
+
join(:posts_read,
|
363
|
+
:posts_read__post_id => :posts__id,
|
364
|
+
:posts_read__account_id => account.id)
|
365
|
+
end
|
366
|
+
|
367
|
+
def self.unread_by(account, posts=self)
|
368
|
+
posts.
|
369
|
+
qualify.
|
370
|
+
left_outer_join(:posts_read,
|
371
|
+
:posts_read__post_id => :posts__id,
|
372
|
+
:posts_read__account_id => account.id).
|
373
|
+
where(:posts_read__post_id => nil)
|
374
|
+
end
|
375
|
+
|
376
|
+
def self.liked_by(member, posts=self)
|
377
|
+
posts.
|
378
|
+
qualify.
|
379
|
+
join(:post_likes,
|
380
|
+
:post_likes__post_id => :posts__id,
|
381
|
+
:post_likes__member_id => member.id)
|
382
|
+
end
|
383
|
+
|
384
|
+
def self.without_liked_by(member, posts=self)
|
385
|
+
posts.
|
386
|
+
qualify.
|
387
|
+
join(:post_likes,
|
388
|
+
:post_likes__post_id => :posts__id,
|
389
|
+
:post_likes__member_id => member.id)
|
390
|
+
end
|
391
|
+
|
392
|
+
def self.commented_on_by(member, posts=self)
|
393
|
+
posts.
|
394
|
+
where(:posts__id => Comment.
|
395
|
+
select(:post_id).
|
396
|
+
distinct(:post_id).
|
397
|
+
where(:member_id => member.id))
|
398
|
+
end
|
399
|
+
|
400
|
+
def self.without_commented_on_by(member, posts=self)
|
401
|
+
posts.
|
402
|
+
exclude(:posts__id => Comment.
|
403
|
+
select(:post_id).
|
404
|
+
distinct(:post_id).
|
405
|
+
where(:member_id => member.id))
|
406
|
+
end
|
407
|
+
|
408
|
+
def self.subscribed_to_by(account, posts=self)
|
409
|
+
posts.
|
410
|
+
qualify.
|
411
|
+
join(:post_subscriptions,
|
412
|
+
:post_subscriptions__post_id => :posts__id,
|
413
|
+
:post_subscriptions__account_id => account.id)
|
414
|
+
end
|
415
|
+
|
416
|
+
def self.without_subscribed_to_by(account, posts=self)
|
417
|
+
posts.
|
418
|
+
qualify.
|
419
|
+
left_outer_join(:post_subscriptions,
|
420
|
+
:post_subscriptions__post_id => :posts__id,
|
421
|
+
:post_subscriptions__account_id => account.id).
|
422
|
+
where(:post_subscriptions__post_id => nil)
|
423
|
+
end
|
424
|
+
|
425
|
+
def self.filter_by_query(parsed_query, account, posts=self)
|
426
|
+
flags = parsed_query['flag']
|
427
|
+
if flags
|
428
|
+
flags[:negations].each do |flag|
|
429
|
+
case flag
|
430
|
+
when 'tree'
|
431
|
+
posts = posts.exclude(:remote_id => nil)
|
432
|
+
when 'unread'
|
433
|
+
posts = self.read_by(account, posts)
|
434
|
+
when 'liked'
|
435
|
+
posts = self.without_liked_by(account.member, posts)
|
436
|
+
when 'commented'
|
437
|
+
posts = self.without_commented_on_by(account.member, posts)
|
438
|
+
when 'subscribed'
|
439
|
+
posts = self.without_subscribed_to_by(account, posts)
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
flags[:requirements].each do |flag|
|
444
|
+
case flag
|
445
|
+
when 'tree'
|
446
|
+
posts = posts.where(:remote_id => nil)
|
447
|
+
when 'unread'
|
448
|
+
posts = self.unread_by(account, posts)
|
449
|
+
when 'liked'
|
450
|
+
posts = self.liked_by(account.member, posts)
|
451
|
+
when 'commented'
|
452
|
+
posts = self.commented_on_by(account.member, posts)
|
453
|
+
when 'subscribed'
|
454
|
+
posts = self.subscribed_to_by(account, posts)
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
sets = flags[:regular].map do |flag|
|
459
|
+
case flag
|
460
|
+
when 'tree'
|
461
|
+
posts.where(:remote_id => nil)
|
462
|
+
when 'unread'
|
463
|
+
self.unread_by(account, posts)
|
464
|
+
when 'liked'
|
465
|
+
self.liked_by(account.member, posts)
|
466
|
+
when 'commented'
|
467
|
+
self.commented_on_by(account.member, posts)
|
468
|
+
when 'subscribed'
|
469
|
+
self.subscribed_to_by(account, posts)
|
470
|
+
end
|
471
|
+
end.compact
|
472
|
+
|
473
|
+
unless sets.empty?
|
474
|
+
posts = sets.reduce do |res, set|
|
475
|
+
res.union(set)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
tags = parsed_query['tag']
|
481
|
+
if tags.values.flatten.count > 0
|
482
|
+
# Careful! Don't trust user input!
|
483
|
+
# remove any tag that includes array braces
|
484
|
+
excluded = tags[:negations].delete_if {|t| t =~ /[{}]/ }
|
485
|
+
required = tags[:requirements].delete_if {|t| t =~ /[{}]/ }
|
486
|
+
regular = tags[:regular].delete_if {|t| t =~ /[{}]/ }
|
487
|
+
|
488
|
+
posts = posts.exclude(Sequel.pg_array(:hashtags).cast('text[]').pg_array.overlaps(excluded)) unless excluded.empty?
|
489
|
+
posts = posts.where (Sequel.pg_array(:hashtags).cast('text[]').pg_array.contains(required)) unless required.empty?
|
490
|
+
posts = posts.where (Sequel.pg_array(:hashtags).cast('text[]').pg_array.overlaps(regular)) unless regular.empty?
|
491
|
+
end
|
492
|
+
|
493
|
+
phrases = parsed_query['phrase']
|
494
|
+
if phrases.values.flatten.count > 0
|
495
|
+
req_patterns = phrases[:requirements].map {|phrase| /(^|\b|\s)#{Regexp.escape(phrase)}(\b|\s|$)/ }
|
496
|
+
neg_patterns = phrases[:negations].map {|phrase| /(^|\b|\s)#{Regexp.escape(phrase)}(\b|\s|$)/ }
|
497
|
+
reg_patterns = phrases[:regular].map {|phrase| /(^|\b|\s)#{Regexp.escape(phrase)}(\b|\s|$)/ }
|
498
|
+
|
499
|
+
unless req_patterns.empty?
|
500
|
+
posts = posts.grep(:text, req_patterns, { all_patterns: true, case_insensitive: true })
|
501
|
+
end
|
502
|
+
|
503
|
+
unless neg_patterns.empty?
|
504
|
+
# NOTE: using Regexp.union results in a postgresql error when the phrase includes '#', or '?'
|
505
|
+
pattern = "(#{neg_patterns.map(&:source).join('|')})"
|
506
|
+
posts = posts.exclude(Sequel.ilike(:text, pattern))
|
507
|
+
end
|
508
|
+
|
509
|
+
unless reg_patterns.empty?
|
510
|
+
posts = posts.grep(:text, reg_patterns, { all_patterns: false, case_insensitive: true })
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
words = parsed_query['word']
|
515
|
+
if words.values.flatten.count > 0
|
516
|
+
# strip query characters
|
517
|
+
words.each_pair {|k,v| words[k].each {|word| word.gsub!(/[\(\)&|!]/, '')}}
|
518
|
+
|
519
|
+
# filter by simple terms first to avoid having to check so many posts
|
520
|
+
# TODO: prevent empty arguments to to_tsquery
|
521
|
+
posts = posts.where(%{to_tsvector('simple', text)
|
522
|
+
@@ (to_tsquery('simple', ?)
|
523
|
+
&& to_tsquery('simple', ?)
|
524
|
+
&& to_tsquery('simple', ?))},
|
525
|
+
words[:negations].map{|w| "!#{w}" }.join(' & '),
|
526
|
+
words[:requirements].join(' & '),
|
527
|
+
words[:regular].join(' | '))
|
528
|
+
end
|
529
|
+
|
530
|
+
{ 'visibility' => :visibility,
|
531
|
+
'from' => :member_id,
|
532
|
+
'via' => :via,
|
533
|
+
}.each_pair do |key, column|
|
534
|
+
set = parsed_query[key]
|
535
|
+
if set.values.flatten.count > 0
|
536
|
+
excluded = set[:negations]
|
537
|
+
required = set[:requirements]
|
538
|
+
regular = set[:regular]
|
539
|
+
|
540
|
+
posts = posts.exclude(column => excluded) unless excluded.empty?
|
541
|
+
posts = posts.where(column => required) unless required.empty?
|
542
|
+
|
543
|
+
unless regular.empty?
|
544
|
+
posts = regular.
|
545
|
+
map {|value| posts.where(column => value)}.
|
546
|
+
reduce {|res, set| res.union(set)}
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
posts
|
552
|
+
end
|
553
|
+
|
554
|
+
def collected_by?(account)
|
555
|
+
DB.dbh[ "SELECT account_collected_post(?, ?)", account.id, self.id ].single_value
|
556
|
+
end
|
557
|
+
|
558
|
+
def to_hash
|
559
|
+
{
|
560
|
+
'id' => self.id,
|
561
|
+
'time_created' => self.time_created,
|
562
|
+
'time_updated' => self.time_updated,
|
563
|
+
'text' => self.text,
|
564
|
+
}
|
565
|
+
end
|
566
|
+
|
567
|
+
def v_internet?
|
568
|
+
self.visibility == 'internet'
|
569
|
+
end
|
570
|
+
def v_forest?
|
571
|
+
self.visibility == 'forest' || self.visibility == 'internet'
|
572
|
+
end
|
573
|
+
def distribute?
|
574
|
+
self.visibility != 'tree'
|
575
|
+
end
|
576
|
+
|
577
|
+
def self.as_nested_json(id)
|
578
|
+
post = self[id]
|
579
|
+
JSON[ post.to_json( :include => {
|
580
|
+
:member => {},
|
581
|
+
:likes => {
|
582
|
+
:include => {
|
583
|
+
:member => {}
|
584
|
+
}},
|
585
|
+
:comments => {
|
586
|
+
:include => {
|
587
|
+
:member => {},
|
588
|
+
:likes => {
|
589
|
+
:include => {
|
590
|
+
:member => {}
|
591
|
+
}
|
592
|
+
}
|
593
|
+
}
|
594
|
+
}
|
595
|
+
}) ]
|
596
|
+
end
|
597
|
+
|
598
|
+
# Expand and embed all associated records.
|
599
|
+
def self.get_full(id, viewing_account = nil)
|
600
|
+
post = self[id]
|
601
|
+
return unless post
|
602
|
+
|
603
|
+
# cache member records
|
604
|
+
members = Hash.new
|
605
|
+
members.default_proc = proc do |hash, key|
|
606
|
+
member = Member[ key ]
|
607
|
+
name = member.name_display
|
608
|
+
member.define_singleton_method(:name_display) { name }
|
609
|
+
hash[key] = member
|
610
|
+
end
|
611
|
+
|
612
|
+
post_likes = post.likes
|
613
|
+
|
614
|
+
like_proc = proc do |like|
|
615
|
+
like.define_singleton_method(:member) { members[like.member_id] }
|
616
|
+
like
|
617
|
+
end
|
618
|
+
|
619
|
+
get_comments = lambda do
|
620
|
+
comments = Comment.on_post(post, viewing_account: viewing_account)
|
621
|
+
comment_likes = CommentLike.where('comment_id IN ?', comments.map(&:id)).reduce({}) do |hash, like|
|
622
|
+
if hash[like.comment_id]
|
623
|
+
hash[like.comment_id] << like
|
624
|
+
else
|
625
|
+
hash[like.comment_id] = [like]
|
626
|
+
end
|
627
|
+
hash
|
628
|
+
end
|
629
|
+
|
630
|
+
comments.map do |comment|
|
631
|
+
likes = if comment_likes[comment.id]
|
632
|
+
comment_likes[comment.id].map{|l| like_proc.call(l)}
|
633
|
+
else
|
634
|
+
[]
|
635
|
+
end
|
636
|
+
|
637
|
+
comment.define_singleton_method(:member) { members[comment.member_id] }
|
638
|
+
comment.define_singleton_method(:likes) { likes }
|
639
|
+
comment.define_singleton_method(:post) { post }
|
640
|
+
|
641
|
+
comment
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
comments = get_comments.call
|
646
|
+
|
647
|
+
# enhance post object with expanded associations
|
648
|
+
post.define_singleton_method(:member) { members[post.member_id] }
|
649
|
+
post.define_singleton_method(:likes) { post_likes.map{|l| like_proc.call(l)} }
|
650
|
+
post.define_singleton_method(:comments) {|opts={}|
|
651
|
+
if opts[:refresh_cache]
|
652
|
+
comments = get_comments.call
|
653
|
+
end
|
654
|
+
|
655
|
+
res = comments
|
656
|
+
if opts
|
657
|
+
res = res.find_all {|c| c.id >= opts[:from_id].to_i} if opts[:from_id]
|
658
|
+
res = res.find_all {|c| c.id < opts[:to_id].to_i} if opts[:to_id]
|
659
|
+
res = res.last(opts[:limit].to_i) if opts[:limit]
|
660
|
+
end
|
661
|
+
res
|
662
|
+
}
|
663
|
+
|
664
|
+
post
|
665
|
+
end
|
666
|
+
|
667
|
+
# @return [Post] the earliest Post which contains a URL that the
|
668
|
+
# prospective post text contains
|
669
|
+
# This method only searches recent posts, not necessarily every post in the DB.
|
670
|
+
def self.urls_already_posted?(prospective_post_text)
|
671
|
+
posts_found = []
|
672
|
+
|
673
|
+
prospective_post_text.scan(
|
674
|
+
%r{(?<=\]\()(https?://\S+?)(?=\))|(?:^|\b)(https?://\S+)(?=\s|$)}
|
675
|
+
) do |match1, match2|
|
676
|
+
url = match1 || match2
|
677
|
+
posts = self.s(
|
678
|
+
%{
|
679
|
+
SELECT *
|
680
|
+
FROM (
|
681
|
+
SELECT *
|
682
|
+
FROM posts
|
683
|
+
ORDER BY id DESC
|
684
|
+
LIMIT 256
|
685
|
+
) AS subquery
|
686
|
+
WHERE text ~ ( '(^|\\A|\\s)' || ? || '(\\)|\\Z|\\s|$)' )
|
687
|
+
OR text ~ ( '\\]\\(' || ? || '\\)' )
|
688
|
+
ORDER by time_created
|
689
|
+
},
|
690
|
+
Regexp.escape(url),
|
691
|
+
Regexp.escape(url)
|
692
|
+
)
|
693
|
+
if posts.count > 0 && posts.count < 4
|
694
|
+
posts_found << posts[0]
|
695
|
+
end
|
696
|
+
end
|
697
|
+
|
698
|
+
posts_found.sort_by(&:time_created)[0]
|
699
|
+
end
|
700
|
+
|
701
|
+
def pools_by_member(member_id)
|
702
|
+
Libertree::Model::Pool.qualify.
|
703
|
+
join(:pools_posts, :pools_posts__pool_id => :pools__id).
|
704
|
+
where(pools_posts__post_id: self.id, pools__member_id: member_id)
|
705
|
+
end
|
706
|
+
|
707
|
+
def update_collection_status_for_member(member_id, pool_ids)
|
708
|
+
# - ignore ids in pool_ids that are also in ids
|
709
|
+
# - remove: ids that are not in pool_ids but are in ids
|
710
|
+
# - add: ids that are in pool_ids but not in ids
|
711
|
+
ids = pools_by_member(member_id).map(&:id)
|
712
|
+
|
713
|
+
add_ids = pool_ids - ids
|
714
|
+
add_pools = Libertree::Model::Pool.where(member_id: member_id, id: add_ids)
|
715
|
+
add_pools.each {|pool| pool << self }
|
716
|
+
|
717
|
+
remove_ids = ids - pool_ids
|
718
|
+
remove_pools = Libertree::Model::Pool.where(member_id: member_id, id: remove_ids)
|
719
|
+
remove_pools.each {|pool| pool.remove_post self }
|
720
|
+
|
721
|
+
add_pools
|
722
|
+
end
|
723
|
+
|
724
|
+
def guid
|
725
|
+
server = self.member.server
|
726
|
+
origin = server ? server.domain : Server.own_domain
|
727
|
+
"xmpp:#{origin}?;node=/posts;item=#{self.public_id}"
|
728
|
+
end
|
729
|
+
|
730
|
+
def group
|
731
|
+
Libertree::Model::Group[self.group_id]
|
732
|
+
end
|
733
|
+
end
|
734
|
+
end
|
735
|
+
end
|