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,487 @@
|
|
1
|
+
require 'bcrypt'
|
2
|
+
require 'net/ldap'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'gpgme'
|
5
|
+
require 'tmpdir'
|
6
|
+
|
7
|
+
module Libertree
|
8
|
+
module Model
|
9
|
+
class Account < Sequel::Model(:accounts)
|
10
|
+
class KeyError < StandardError; end
|
11
|
+
|
12
|
+
def self.set_auth_settings(type, settings)
|
13
|
+
@@auth_type = type
|
14
|
+
@@auth_settings = settings
|
15
|
+
end
|
16
|
+
|
17
|
+
# These two password methods provide a seamless interface to the BCrypted
|
18
|
+
# password. The pseudo-field "password" can be treated like a normal
|
19
|
+
# String field for reading and writing.
|
20
|
+
def password
|
21
|
+
@password ||= BCrypt::Password.new(password_encrypted)
|
22
|
+
end
|
23
|
+
|
24
|
+
def password=( new_password )
|
25
|
+
@password = BCrypt::Password.create(new_password)
|
26
|
+
self.password_encrypted = @password
|
27
|
+
end
|
28
|
+
|
29
|
+
# Used by Ramaze::Helper::UserHelper.
|
30
|
+
# @return [Account] authenticated account, or nil on failure to authenticate
|
31
|
+
def self.authenticate(creds)
|
32
|
+
if @@auth_type && @@auth_type == :ldap
|
33
|
+
return if creds['username'].nil? || creds['password'].nil?
|
34
|
+
self.authenticate_ldap(creds['username'].to_s,
|
35
|
+
creds['password'].to_s,
|
36
|
+
@@auth_settings)
|
37
|
+
else
|
38
|
+
return if creds['password_reset_code'].nil? && (creds['username'].nil? || creds['password'].nil?)
|
39
|
+
self.authenticate_db(creds)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.authenticate_db(creds)
|
44
|
+
if creds['password_reset_code'].to_s
|
45
|
+
account = Account.where(%{password_reset_code = ? AND NOW() <= password_reset_expiry},
|
46
|
+
creds['password_reset_code'].to_s).first
|
47
|
+
if account
|
48
|
+
return account
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
account = Account[ username: creds['username'].to_s ]
|
53
|
+
if account && account.password == creds['password'].to_s
|
54
|
+
account
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Authenticates against LDAP. On success returns the matching Libertree
|
59
|
+
# account or creates a new one.
|
60
|
+
# TODO: override email= and password= methods when LDAP is used
|
61
|
+
def self.authenticate_ldap(username, password, settings)
|
62
|
+
ldap_connection_settings = {
|
63
|
+
:host => settings['connection']['host'],
|
64
|
+
:port => settings['connection']['port'],
|
65
|
+
:base => settings['connection']['base'],
|
66
|
+
:auth => {
|
67
|
+
:method => :simple,
|
68
|
+
:username => settings['connection']['bind_dn'],
|
69
|
+
:password => settings['connection']['password']
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
@ldap ||= Net::LDAP.new(ldap_connection_settings)
|
74
|
+
|
75
|
+
mapping = settings['mapping'] || {
|
76
|
+
'username' => 'uid',
|
77
|
+
'email' => 'mail',
|
78
|
+
'display_name' => 'displayName'
|
79
|
+
}
|
80
|
+
username.downcase!
|
81
|
+
result = @ldap.bind_as(:filter => "(#{mapping['username']}=#{username})",
|
82
|
+
:password => password)
|
83
|
+
|
84
|
+
if result
|
85
|
+
account = self[ username: username ]
|
86
|
+
|
87
|
+
if account
|
88
|
+
# update email address and password
|
89
|
+
account.email = result.first[mapping['email']].first
|
90
|
+
account.password_encrypted = BCrypt::Password.create( password )
|
91
|
+
account.save
|
92
|
+
else
|
93
|
+
# No Libertree account exists for this authenticated LDAP
|
94
|
+
# user; create a new one. We will set up the password even
|
95
|
+
# though it won't be used while LDAP authentication is
|
96
|
+
# enabled to make sure that the account will be protected if
|
97
|
+
# LDAP authentication is ever disabled.
|
98
|
+
account = self.create( username: username,
|
99
|
+
password_encrypted: BCrypt::Password.create( password ),
|
100
|
+
email: result.first[mapping['email']].first )
|
101
|
+
end
|
102
|
+
|
103
|
+
account.member.profile.name_display = result.first[mapping['display_name']].first
|
104
|
+
account.member.profile.save
|
105
|
+
account
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def member
|
110
|
+
@member ||= Member[ account_id: self.id ]
|
111
|
+
end
|
112
|
+
|
113
|
+
def settings
|
114
|
+
@settings ||= AccountSettings[ account_id: self.id ]
|
115
|
+
end
|
116
|
+
|
117
|
+
def notify_about(data)
|
118
|
+
Notification.create(
|
119
|
+
account_id: self.id,
|
120
|
+
data: data.to_json
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
def notifications( limit = 128 )
|
125
|
+
@notifications ||= Notification.where(account_id: self.id).reverse_order(:id).limit(limit.to_i).all
|
126
|
+
end
|
127
|
+
|
128
|
+
def notifications_unseen
|
129
|
+
@notifications_unseen ||= Notification.where(account_id: self.id, seen: false).order(:id)
|
130
|
+
end
|
131
|
+
|
132
|
+
# TODO: Is this no longer used?
|
133
|
+
def notifications_unseen_grouped(max_groups=5, limit=200)
|
134
|
+
grouped = {}
|
135
|
+
keys = [] # so we have a display order
|
136
|
+
|
137
|
+
notifs = self.notifications_unseen.reverse_order(:id).limit(limit)
|
138
|
+
notifs.each do |n|
|
139
|
+
next if n.subject.nil?
|
140
|
+
|
141
|
+
target = case n.subject
|
142
|
+
when Libertree::Model::Comment, Libertree::Model::PostLike
|
143
|
+
n.subject.post
|
144
|
+
when Libertree::Model::CommentLike
|
145
|
+
n.subject.comment
|
146
|
+
when Libertree::Model::Post
|
147
|
+
# mention or group post
|
148
|
+
post = n.subject
|
149
|
+
post.group || post
|
150
|
+
else
|
151
|
+
n.subject
|
152
|
+
end
|
153
|
+
|
154
|
+
# collect by target and type; we don't want to put comment
|
155
|
+
# and post like notifs in the same bin
|
156
|
+
key = [target, n.subject.class]
|
157
|
+
if grouped[key]
|
158
|
+
grouped[key] << n
|
159
|
+
else
|
160
|
+
grouped[key] = [n]
|
161
|
+
keys << key
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# get the groups in order of keys
|
166
|
+
keys.take(max_groups).map {|k| grouped[k] }
|
167
|
+
end
|
168
|
+
|
169
|
+
def num_notifications_unseen
|
170
|
+
@num_notifications_unseen ||= Notification.where(account_id: self.id, seen: false).count
|
171
|
+
end
|
172
|
+
|
173
|
+
def num_chat_unseen
|
174
|
+
ChatMessage.where(
|
175
|
+
to_member_id: self.member.id,
|
176
|
+
seen: false
|
177
|
+
).exclude(
|
178
|
+
from_member_id: self.ignored_members.map(&:id)
|
179
|
+
).count
|
180
|
+
end
|
181
|
+
|
182
|
+
def num_chat_unseen_from_partner(member)
|
183
|
+
ChatMessage.where(
|
184
|
+
from_member_id: member.id,
|
185
|
+
to_member_id: self.member.id,
|
186
|
+
seen: false
|
187
|
+
).exclude(
|
188
|
+
from_member_id: self.ignored_members.map(&:id)
|
189
|
+
).count
|
190
|
+
end
|
191
|
+
|
192
|
+
def chat_messages_unseen
|
193
|
+
ChatMessage.where(
|
194
|
+
to_member_id: self.member.id,
|
195
|
+
seen: false
|
196
|
+
).exclude(
|
197
|
+
from_member_id: self.ignored_members.map(&:id)
|
198
|
+
).all
|
199
|
+
end
|
200
|
+
|
201
|
+
def chat_partners_current
|
202
|
+
Member.s_wrap(
|
203
|
+
%{
|
204
|
+
(
|
205
|
+
SELECT
|
206
|
+
DISTINCT m.*
|
207
|
+
, EXISTS(
|
208
|
+
SELECT 1
|
209
|
+
FROM chat_messages cm2
|
210
|
+
WHERE
|
211
|
+
cm2.from_member_id = cm.from_member_id
|
212
|
+
AND cm2.to_member_id = cm.to_member_id
|
213
|
+
AND cm2.seen = FALSE
|
214
|
+
) AS has_unseen_from_other
|
215
|
+
FROM
|
216
|
+
chat_messages cm
|
217
|
+
, members m
|
218
|
+
WHERE
|
219
|
+
cm.to_member_id = ?
|
220
|
+
AND (
|
221
|
+
cm.seen = FALSE
|
222
|
+
OR cm.time_created > NOW() - '1 hour'::INTERVAL
|
223
|
+
)
|
224
|
+
AND m.id = cm.from_member_id
|
225
|
+
AND NOT EXISTS(
|
226
|
+
SELECT 1
|
227
|
+
FROM ignored_members im
|
228
|
+
WHERE im.member_id = cm.from_member_id
|
229
|
+
)
|
230
|
+
) UNION (
|
231
|
+
SELECT
|
232
|
+
DISTINCT m.*
|
233
|
+
, EXISTS(
|
234
|
+
SELECT 1
|
235
|
+
FROM chat_messages cm2
|
236
|
+
WHERE
|
237
|
+
cm2.from_member_id = cm.to_member_id
|
238
|
+
AND cm2.to_member_id = cm.from_member_id
|
239
|
+
AND cm2.seen = FALSE
|
240
|
+
) AS has_unseen_from_other
|
241
|
+
FROM
|
242
|
+
chat_messages cm
|
243
|
+
, members m
|
244
|
+
WHERE
|
245
|
+
cm.from_member_id = ?
|
246
|
+
AND cm.time_created > NOW() - '1 hour'::INTERVAL
|
247
|
+
AND m.id = cm.to_member_id
|
248
|
+
AND NOT EXISTS(
|
249
|
+
SELECT 1
|
250
|
+
FROM ignored_members im
|
251
|
+
WHERE im.member_id = cm.to_member_id
|
252
|
+
)
|
253
|
+
)
|
254
|
+
},
|
255
|
+
self.member.id,
|
256
|
+
self.member.id
|
257
|
+
)
|
258
|
+
end
|
259
|
+
|
260
|
+
def rivers
|
261
|
+
River.s("SELECT * FROM rivers WHERE account_id = ? ORDER BY position ASC, id DESC", self.id)
|
262
|
+
end
|
263
|
+
|
264
|
+
def rivers_not_appended
|
265
|
+
rivers.reject(&:appended_to_all)
|
266
|
+
end
|
267
|
+
|
268
|
+
def rivers_appended
|
269
|
+
@rivers_appended ||= rivers.find_all(&:appended_to_all)
|
270
|
+
end
|
271
|
+
|
272
|
+
def self.create(*args)
|
273
|
+
account = super
|
274
|
+
member = Member.create( account_id: account.id )
|
275
|
+
AccountSettings.create( account_id: account.id )
|
276
|
+
River.create( label: "All posts", query: ":forest", account_id: account.id, home: true )
|
277
|
+
account
|
278
|
+
end
|
279
|
+
|
280
|
+
def home_river
|
281
|
+
River.where(account_id: self.id, home: true).first
|
282
|
+
end
|
283
|
+
|
284
|
+
def home_river=(river)
|
285
|
+
DB.dbh[ "SELECT account_set_home_river(?,?)", self.id, river.id ].get
|
286
|
+
end
|
287
|
+
|
288
|
+
def invitations_not_accepted
|
289
|
+
Invitation.where("inviter_account_id = ? AND account_id IS NULL", self.id).order(:id).all
|
290
|
+
end
|
291
|
+
|
292
|
+
def new_invitation
|
293
|
+
if invitations_not_accepted.count < 5
|
294
|
+
Invitation.create( inviter_account_id: self.id )
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def generate_api_token
|
299
|
+
self.api_token = SecureRandom.hex(16)
|
300
|
+
self.save
|
301
|
+
end
|
302
|
+
|
303
|
+
# @param [Time] time The time to compare to
|
304
|
+
# @return [Boolean] whether or not the API was last used by this account
|
305
|
+
# more recently than the given Time
|
306
|
+
def api_last_used_more_recently_than(time)
|
307
|
+
self.api_time_last && self.api_time_last.to_time > time
|
308
|
+
end
|
309
|
+
|
310
|
+
# Clears some memoized data
|
311
|
+
def dirty
|
312
|
+
@notifications = nil
|
313
|
+
@notifications_unseen = nil
|
314
|
+
@num_notifications_unseen = nil
|
315
|
+
@rivers_appended = nil
|
316
|
+
@remote_storage_connection = nil
|
317
|
+
@settings = nil
|
318
|
+
end
|
319
|
+
|
320
|
+
def admin?
|
321
|
+
self.admin
|
322
|
+
end
|
323
|
+
|
324
|
+
def subscribe_to(post)
|
325
|
+
DB.dbh[ %{SELECT subscribe_account_to_post(?,?)}, self.id, post.id ].get
|
326
|
+
end
|
327
|
+
|
328
|
+
def unsubscribe_from(post)
|
329
|
+
DB.dbh[ "DELETE FROM post_subscriptions WHERE account_id = ? AND post_id = ?", self.id, post.id ].get
|
330
|
+
end
|
331
|
+
|
332
|
+
def subscribed_to?(post)
|
333
|
+
DB.dbh[ "SELECT account_subscribed_to_post( ?, ? )", self.id, post.id ].single_value
|
334
|
+
end
|
335
|
+
|
336
|
+
def messages( opts = {} )
|
337
|
+
limit = opts.fetch(:limit, 30)
|
338
|
+
if opts[:newer]
|
339
|
+
time_comparator = '>'
|
340
|
+
else
|
341
|
+
time_comparator = '<'
|
342
|
+
end
|
343
|
+
time = Time.at( opts.fetch(:time, Time.now.to_f) ).strftime("%Y-%m-%d %H:%M:%S.%6N%z")
|
344
|
+
|
345
|
+
ignored_member_ids = self.ignored_members.map(&:id)
|
346
|
+
Message.s_wrap(
|
347
|
+
%{
|
348
|
+
SELECT *
|
349
|
+
FROM view__messages_sent_and_received
|
350
|
+
WHERE member_id = ?
|
351
|
+
AND time_created #{time_comparator} ?
|
352
|
+
ORDER BY time_created DESC
|
353
|
+
LIMIT #{limit}
|
354
|
+
},
|
355
|
+
self.member.id, time
|
356
|
+
).reject { |message|
|
357
|
+
ignored_member_ids.include?(message.sender_member_id)
|
358
|
+
}
|
359
|
+
|
360
|
+
end
|
361
|
+
|
362
|
+
# @return [Boolean] true iff password reset was successfully set up
|
363
|
+
def self.set_up_password_reset_for(email)
|
364
|
+
# TODO: Don't allow registration of accounts with the same email but different case
|
365
|
+
account = self.where('LOWER(email) = ?', email.downcase).first
|
366
|
+
if account
|
367
|
+
account.password_reset_code = SecureRandom.hex(16)
|
368
|
+
account.password_reset_expiry = Time.now + 60 * 60
|
369
|
+
account.save
|
370
|
+
account
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# NOTE: this method does not save the account record
|
375
|
+
def validate_and_set_pubkey(key)
|
376
|
+
# import pubkey into temporary keyring to verify it
|
377
|
+
GPGME::Engine.home_dir = Dir.tmpdir
|
378
|
+
result = GPGME::Key.import key.to_s
|
379
|
+
|
380
|
+
if result.considered == 1 && result.secret_read == 1
|
381
|
+
# Delete the key immediately from the keyring and
|
382
|
+
# alert the user in case a secret key was uploaded
|
383
|
+
keys = GPGME::Key.find(:secret, result.imports.first.fpr)
|
384
|
+
keys.first.delete!(true) # force deletion of secret key
|
385
|
+
keys = nil; result = nil
|
386
|
+
raise KeyError, 'secret key imported'
|
387
|
+
elsif result.considered == 1 && (result.imported == 1 || result.unchanged == 1)
|
388
|
+
# We do not check whether the key matches the given email address.
|
389
|
+
# This is not necessary, because we don't search the keyring to get
|
390
|
+
# the encryption key when sending emails. Instead, we just take
|
391
|
+
# whatever key the user provided.
|
392
|
+
self.pubkey = key.to_s
|
393
|
+
else
|
394
|
+
raise KeyError, 'invalid key'
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
def data_hash
|
399
|
+
{
|
400
|
+
'account' => {
|
401
|
+
'username' => self.username,
|
402
|
+
'time_created' => self.time_created,
|
403
|
+
'email' => self.email,
|
404
|
+
'custom_css' => self.settings.custom_css,
|
405
|
+
'custom_js' => self.settings.custom_js,
|
406
|
+
'custom_link' => self.settings.custom_link,
|
407
|
+
'font_css' => self.font_css,
|
408
|
+
'excerpt_max_height' => self.settings.excerpt_max_height,
|
409
|
+
'profile' => {
|
410
|
+
'name_display' => self.member.profile.name_display,
|
411
|
+
'description' => self.member.profile.description,
|
412
|
+
},
|
413
|
+
|
414
|
+
'rivers' => self.rivers.map(&:to_hash),
|
415
|
+
'posts' => self.member.posts(limit: 9999999).map(&:to_hash),
|
416
|
+
'comments' => self.member.comments(9999999).map(&:to_hash),
|
417
|
+
'messages' => self.messages(limit: 9999999).map(&:to_hash),
|
418
|
+
}
|
419
|
+
}
|
420
|
+
end
|
421
|
+
|
422
|
+
def online?
|
423
|
+
Time.now - time_heartbeat.to_time < 5.01 * 60
|
424
|
+
end
|
425
|
+
|
426
|
+
def contact_lists
|
427
|
+
ContactList.where(account_id: self.id).all
|
428
|
+
end
|
429
|
+
|
430
|
+
# All contacts, from all contact lists
|
431
|
+
# TODO: Can we collect this in SQL instead of mapping, etc. in Ruby?
|
432
|
+
def contacts
|
433
|
+
contact_lists.map { |list| list.members }.flatten.uniq
|
434
|
+
end
|
435
|
+
|
436
|
+
def contacts_mutual
|
437
|
+
self.contacts.find_all { |c|
|
438
|
+
c.account && c.account.contacts.include?(self.member)
|
439
|
+
}
|
440
|
+
end
|
441
|
+
|
442
|
+
def has_contact_list_by_name_containing_member?(contact_list_name, member)
|
443
|
+
DB.dbh[ "SELECT account_has_contact_list_by_name_containing_member( ?, ?, ? )",
|
444
|
+
self.id, contact_list_name, member.id ].single_value
|
445
|
+
end
|
446
|
+
|
447
|
+
def delete_cascade
|
448
|
+
handle = self.username
|
449
|
+
DB.dbh[ "SELECT delete_cascade_account(?)", self.id ].get
|
450
|
+
|
451
|
+
# distribute deletion of member record
|
452
|
+
Libertree::Model::Job.create_for_forests(
|
453
|
+
{
|
454
|
+
task: 'request:MEMBER-DELETE',
|
455
|
+
params: { 'username' => handle, }
|
456
|
+
}
|
457
|
+
)
|
458
|
+
end
|
459
|
+
|
460
|
+
def remote_storage_connection
|
461
|
+
@remote_storage_connection ||= RemoteStorageConnection[ account_id: self.id ]
|
462
|
+
end
|
463
|
+
|
464
|
+
def files
|
465
|
+
Libertree::Model::File.s("SELECT * FROM files WHERE account_id = ? ORDER BY id DESC", self.id)
|
466
|
+
end
|
467
|
+
|
468
|
+
def ignored_members
|
469
|
+
Libertree::Model::IgnoredMember.where(account_id: self.id).map(&:member)
|
470
|
+
end
|
471
|
+
|
472
|
+
def ignoring?(member)
|
473
|
+
self.ignored_members.include? member
|
474
|
+
end
|
475
|
+
|
476
|
+
def ignore_member(member)
|
477
|
+
return if member.nil?
|
478
|
+
Libertree::Model::IgnoredMember.create(account_id: self.id, member_id: member.id)
|
479
|
+
end
|
480
|
+
|
481
|
+
def unignore_member(member)
|
482
|
+
return if member.nil?
|
483
|
+
Libertree::Model::IgnoredMember.where(account_id: self.id, member_id: member.id).delete
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Libertree
|
2
|
+
module Model
|
3
|
+
class ChatMessage < Sequel::Model(:chat_messages)
|
4
|
+
def after_create
|
5
|
+
super
|
6
|
+
self.distribute
|
7
|
+
end
|
8
|
+
|
9
|
+
def distribute
|
10
|
+
return if ! self.recipient.tree
|
11
|
+
Libertree::Model::Job.create(
|
12
|
+
{
|
13
|
+
task: 'request:CHAT',
|
14
|
+
params: {
|
15
|
+
'chat_message_id' => self.id,
|
16
|
+
'server_id' => self.recipient.tree.id,
|
17
|
+
}.to_json,
|
18
|
+
}
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def sender
|
23
|
+
@sender ||= Member[self.from_member_id]
|
24
|
+
end
|
25
|
+
|
26
|
+
def recipient
|
27
|
+
@recipient ||= Member[self.to_member_id]
|
28
|
+
end
|
29
|
+
|
30
|
+
def partner_for(account)
|
31
|
+
if account.member == self.sender
|
32
|
+
self.recipient
|
33
|
+
elsif account.member == self.recipient
|
34
|
+
self.sender
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.between(account, member, limit = 32)
|
39
|
+
return [] if account.nil? || member.nil?
|
40
|
+
|
41
|
+
s(
|
42
|
+
%{
|
43
|
+
SELECT * FROM (
|
44
|
+
SELECT
|
45
|
+
*
|
46
|
+
FROM
|
47
|
+
chat_messages cm
|
48
|
+
WHERE
|
49
|
+
from_member_id = ?
|
50
|
+
AND to_member_id = ?
|
51
|
+
OR
|
52
|
+
from_member_id = ?
|
53
|
+
AND to_member_id = ?
|
54
|
+
ORDER BY
|
55
|
+
time_created DESC
|
56
|
+
LIMIT #{ [limit.to_i, 0].max }
|
57
|
+
) AS x
|
58
|
+
ORDER BY time_created
|
59
|
+
},
|
60
|
+
account.member.id,
|
61
|
+
member.id,
|
62
|
+
member.id,
|
63
|
+
account.member.id
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.mark_seen_between(account, member_id)
|
68
|
+
return if account.nil?
|
69
|
+
|
70
|
+
self.where("from_member_id = ? AND to_member_id = ?", member_id.to_i, account.member.id).
|
71
|
+
update(seen: true)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Libertree
|
2
|
+
module Model
|
3
|
+
class CommentLike < Sequel::Model(:comment_likes)
|
4
|
+
def after_create
|
5
|
+
super
|
6
|
+
if self.local? && self.comment.post.distribute?
|
7
|
+
Libertree::Model::Job.create_for_forests(
|
8
|
+
{
|
9
|
+
task: 'request:COMMENT-LIKE',
|
10
|
+
params: { 'comment_like_id' => self.id, }
|
11
|
+
},
|
12
|
+
*self.forests
|
13
|
+
)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def local?
|
18
|
+
self.remote_id.nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
def member
|
22
|
+
@member ||= Member[self.member_id]
|
23
|
+
end
|
24
|
+
|
25
|
+
def comment
|
26
|
+
@comment ||= Comment[self.comment_id]
|
27
|
+
end
|
28
|
+
|
29
|
+
def before_destroy
|
30
|
+
if self.local? && self.comment.post.distribute?
|
31
|
+
Libertree::Model::Job.create_for_forests(
|
32
|
+
{
|
33
|
+
task: 'request:COMMENT-LIKE-DELETE',
|
34
|
+
params: { 'comment_like_id' => self.id, }
|
35
|
+
},
|
36
|
+
*self.forests
|
37
|
+
)
|
38
|
+
end
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
# TODO: the correct method to call is "destroy"
|
43
|
+
def delete
|
44
|
+
self.before_destroy
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete_cascade
|
49
|
+
self.before_destroy
|
50
|
+
DB.dbh[ "SELECT delete_cascade_comment_like(?)", self.id ].get
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.create(*args)
|
54
|
+
like = super
|
55
|
+
like.comment.notify_about_like like
|
56
|
+
like
|
57
|
+
end
|
58
|
+
|
59
|
+
def forests
|
60
|
+
if self.comment.post.remote?
|
61
|
+
self.comment.post.server.forests
|
62
|
+
else
|
63
|
+
Libertree::Model::Forest.all_local_is_member
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|