Class: Libertree::Model::Account

Inherits:
Object
  • Object
show all
Defined in:
lib/libertree/model/account.rb

Defined Under Namespace

Classes: KeyError

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.authenticate(creds) ⇒ Account

Used by Ramaze::Helper::UserHelper.

Returns:

  • (Account)

    authenticated account, or nil on failure to authenticate



31
32
33
34
35
36
37
38
39
40
41
# File 'lib/libertree/model/account.rb', line 31

def self.authenticate(creds)
  if @@auth_type && @@auth_type == :ldap
    return  if creds['username'].nil? || creds['password'].nil?
    self.authenticate_ldap(creds['username'].to_s,
                           creds['password'].to_s,
                           @@auth_settings)
  else
    return  if creds['password_reset_code'].nil? && (creds['username'].nil? || creds['password'].nil?)
    self.authenticate_db(creds)
  end
end

.authenticate_db(creds) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/libertree/model/account.rb', line 43

def self.authenticate_db(creds)
  if creds['password_reset_code'].to_s
     = Account.where(
      Sequel.lit(
        %{password_reset_code = ? AND NOW() <= password_reset_expiry},
        creds['password_reset_code'].to_s
      )
    ).first

    if 
      return 
    end
  end

   = Account[ username: creds['username'].to_s ]
  if  && .password == creds['password'].to_s
    
  end
end

.authenticate_ldap(username, password, settings) ⇒ Object

Authenticates against LDAP. On success returns the matching Libertree account or creates a new one. TODO: override email= and password= methods when LDAP is used



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/libertree/model/account.rb', line 66

def self.authenticate_ldap(username, password, settings)
  ldap_connection_settings = {
    :host => settings['connection']['host'],
    :port => settings['connection']['port'],
    :base => settings['connection']['base'],
    :auth => {
      :method => :simple,
      :username => settings['connection']['bind_dn'],
      :password => settings['connection']['password']
    }
  }

  @ldap ||= Net::LDAP.new(ldap_connection_settings)

  mapping = settings['mapping'] || {
    'username'     => 'uid',
    'email'        => 'mail',
    'display_name' => 'displayName'
  }
  username.downcase!
  result = @ldap.bind_as(:filter => "(#{mapping['username']}=#{username})",
                         :password => password)

  if result
     = self[ username: username ]

    if 
      # update email address and password
      .email = result.first[mapping['email']].first
      .password_encrypted = BCrypt::Password.create( password )
      .save
    else
      # No Libertree account exists for this authenticated LDAP
      # user; create a new one.  We will set up the password even
      # though it won't be used while LDAP authentication is
      # enabled to make sure that the account will be protected if
      # LDAP authentication is ever disabled.
       = self.create( username: username,
                             password_encrypted: BCrypt::Password.create( password ),
                             email: result.first[mapping['email']].first )
    end

    .member.profile.name_display = result.first[mapping['display_name']].first
    .member.profile.save
    
  end
end

.create(*args) ⇒ Object



277
278
279
280
281
282
283
# File 'lib/libertree/model/account.rb', line 277

def self.create(*args)
   = super
  member = Member.create( account_id: .id )
  AccountSettings.create( account_id: .id )
  River.create( label: "All posts", query: ":forest", account_id: .id, home: true )
  
end

.set_auth_settings(type, settings) ⇒ Object



12
13
14
15
# File 'lib/libertree/model/account.rb', line 12

def self.set_auth_settings(type, settings)
  @@auth_type = type
  @@auth_settings = settings
end

.set_up_password_reset_for(email) ⇒ Boolean

Returns true iff password reset was successfully set up.

Returns:

  • (Boolean)

    true iff password reset was successfully set up



370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/libertree/model/account.rb', line 370

def self.set_up_password_reset_for(email)
  # TODO: Don't allow registration of accounts with the same email but different case
   = self.where(
    Sequel.lit('LOWER(email) = ?', email.downcase)
  ).first

  if 
    .password_reset_code = SecureRandom.hex(16)
    .password_reset_expiry = Time.now + 60 * 60
    .save
    
  end
end

Instance Method Details

#admin?Boolean

Returns:

  • (Boolean)


327
328
329
# File 'lib/libertree/model/account.rb', line 327

def admin?
  self.admin
end

#api_last_used_more_recently_than(time) ⇒ Boolean

Returns whether or not the API was last used by this account more recently than the given Time.

Parameters:

  • time (Time)

    The time to compare to

Returns:

  • (Boolean)

    whether or not the API was last used by this account more recently than the given Time



313
314
315
# File 'lib/libertree/model/account.rb', line 313

def api_last_used_more_recently_than(time)
  self.api_time_last && self.api_time_last.to_time > time
end

#chat_messages_unseenObject



197
198
199
200
201
202
203
204
# File 'lib/libertree/model/account.rb', line 197

def chat_messages_unseen
  ChatMessage.where(
    to_member_id: self.member.id,
    seen: false
  ).exclude(
    from_member_id: self.ignored_members.map(&:id)
  ).all
end

#chat_partners_currentObject



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/libertree/model/account.rb', line 206

def chat_partners_current
  Member.s_wrap(
    %{
      (
        SELECT
              DISTINCT m.*
            , EXISTS(
              SELECT 1
              FROM chat_messages cm2
              WHERE
                cm2.from_member_id = cm.from_member_id
                AND cm2.to_member_id = cm.to_member_id
                AND cm2.seen = FALSE
            ) AS has_unseen_from_other
        FROM
            chat_messages cm
          , members m
        WHERE
          cm.to_member_id = ?
          AND (
            cm.seen = FALSE
            OR cm.time_created > NOW() - '1 hour'::INTERVAL
          )
          AND m.id = cm.from_member_id
          AND NOT EXISTS(
            SELECT 1
            FROM ignored_members im
            WHERE im.member_id = cm.from_member_id
          )
      ) UNION (
        SELECT
              DISTINCT m.*
            , EXISTS(
              SELECT 1
              FROM chat_messages cm2
              WHERE
                cm2.from_member_id = cm.to_member_id
                AND cm2.to_member_id = cm.from_member_id
                AND cm2.seen = FALSE
            ) AS has_unseen_from_other
        FROM
            chat_messages cm
          , members m
        WHERE
          cm.from_member_id = ?
          AND cm.time_created > NOW() - '1 hour'::INTERVAL
          AND m.id = cm.to_member_id
          AND NOT EXISTS(
            SELECT 1
            FROM ignored_members im
            WHERE im.member_id = cm.to_member_id
          )
      )
    },
    self.member.id,
    self.member.id
  )
end

#contact_listsObject



436
437
438
# File 'lib/libertree/model/account.rb', line 436

def contact_lists
  ContactList.where(account_id: self.id).all
end

#contactsObject

All contacts, from all contact lists TODO: Can we collect this in SQL instead of mapping, etc. in Ruby?



442
443
444
# File 'lib/libertree/model/account.rb', line 442

def contacts
  contact_lists.map { |list| list.members }.flatten.uniq
end

#contacts_mutualObject



446
447
448
449
450
# File 'lib/libertree/model/account.rb', line 446

def contacts_mutual
  self.contacts.find_all { |c|
    c. && c..contacts.include?(self.member)
  }
end

#data_hashObject



408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/libertree/model/account.rb', line 408

def data_hash
  {
    'account' => {
      'username'           => self.username,
      'time_created'       => self.time_created,
      'email'              => self.email,
      'custom_css'         => self.settings.custom_css,
      'custom_js'          => self.settings.custom_js,
      'custom_link'        => self.settings.custom_link,
      'font_css'           => self.font_css,
      'excerpt_max_height' => self.settings.excerpt_max_height,
      'profile' => {
        'name_display' => self.member.profile.name_display,
        'description'  => self.member.profile.description,
      },

      'rivers'             => self.rivers.map(&:to_hash),
      'posts'              => self.member.posts(limit: 9999999).map(&:to_hash),
      'comments'           => self.member.comments(9999999).map(&:to_hash),
      'messages'           => self.messages(limit: 9999999).map(&:to_hash),
    }
  }
end

#delete_cascadeObject



457
458
459
460
461
462
463
464
465
466
467
468
# File 'lib/libertree/model/account.rb', line 457

def delete_cascade
  handle = self.username
  DB.dbh[ "SELECT delete_cascade_account(?)", self.id ].get

  # distribute deletion of member record
  Libertree::Model::Job.create_for_forests(
    {
      task: 'request:MEMBER-DELETE',
      params: { 'username' => handle, }
    }
  )
end

#dirtyObject

Clears some memoized data



318
319
320
321
322
323
324
325
# File 'lib/libertree/model/account.rb', line 318

def dirty
  @notifications = nil
  @notifications_unseen = nil
  @num_notifications_unseen = nil
  @rivers_appended = nil
  @remote_storage_connection = nil
  @settings = nil
end

#filesObject



474
475
476
# File 'lib/libertree/model/account.rb', line 474

def files
  Libertree::Model::File.s("SELECT * FROM files WHERE account_id = ? ORDER BY id DESC", self.id)
end

#generate_api_tokenObject



305
306
307
308
# File 'lib/libertree/model/account.rb', line 305

def generate_api_token
  self.api_token = SecureRandom.hex(16)
  self.save
end

#has_contact_list_by_name_containing_member?(contact_list_name, member) ⇒ Boolean

Returns:

  • (Boolean)


452
453
454
455
# File 'lib/libertree/model/account.rb', line 452

def has_contact_list_by_name_containing_member?(contact_list_name, member)
  DB.dbh[ "SELECT account_has_contact_list_by_name_containing_member( ?, ?, ? )",
          self.id, contact_list_name, member.id ].single_value
end

#home_riverObject



285
286
287
# File 'lib/libertree/model/account.rb', line 285

def home_river
  River.where(account_id: self.id, home: true).first
end

#home_river=(river) ⇒ Object



289
290
291
# File 'lib/libertree/model/account.rb', line 289

def home_river=(river)
  DB.dbh[ "SELECT account_set_home_river(?,?)", self.id, river.id ].get
end

#ignore_member(member) ⇒ Object



486
487
488
489
# File 'lib/libertree/model/account.rb', line 486

def ignore_member(member)
  return  if member.nil?
  Libertree::Model::IgnoredMember.create(account_id: self.id, member_id: member.id)
end

#ignored_membersObject



478
479
480
# File 'lib/libertree/model/account.rb', line 478

def ignored_members
  Libertree::Model::IgnoredMember.where(account_id: self.id).map(&:member)
end

#ignoring?(member) ⇒ Boolean

Returns:

  • (Boolean)


482
483
484
# File 'lib/libertree/model/account.rb', line 482

def ignoring?(member)
  self.ignored_members.include? member
end

#invitations_not_acceptedObject



293
294
295
296
297
# File 'lib/libertree/model/account.rb', line 293

def invitations_not_accepted
  Invitation.where(
    Sequel.lit("inviter_account_id = ? AND account_id IS NULL", self.id)
  ).order(:id).all
end

#memberObject



114
115
116
# File 'lib/libertree/model/account.rb', line 114

def member
  @member ||= Member[ account_id: self.id ]
end

#messages(opts = {}) ⇒ Object



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/libertree/model/account.rb', line 343

def messages( opts = {} )
  limit = opts.fetch(:limit, 30)
  if opts[:newer]
    time_comparator = '>'
  else
    time_comparator = '<'
  end
  time = Time.at( opts.fetch(:time, Time.now.to_f) ).strftime("%Y-%m-%d %H:%M:%S.%6N%z")

  ignored_member_ids = self.ignored_members.map(&:id)
  Message.s_wrap(
    %{
      SELECT *
      FROM view__messages_sent_and_received
      WHERE member_id = ?
      AND time_created #{time_comparator} ?
      ORDER BY time_created DESC
      LIMIT #{limit}
    },
    self.member.id, time
  ).reject { |message|
    ignored_member_ids.include?(message.sender_member_id)
  }

end

#new_invitationObject



299
300
301
302
303
# File 'lib/libertree/model/account.rb', line 299

def new_invitation
  if invitations_not_accepted.count < 5
    Invitation.create( inviter_account_id: self.id )
  end
end

#notifications(limit = 128) ⇒ Object



129
130
131
# File 'lib/libertree/model/account.rb', line 129

def notifications( limit = 128 )
  @notifications ||= Notification.where(account_id: self.id).reverse_order(:id).limit(limit.to_i).all
end

#notifications_unseenObject



133
134
135
# File 'lib/libertree/model/account.rb', line 133

def notifications_unseen
  @notifications_unseen ||= Notification.where(account_id: self.id, seen: false).order(:id)
end

#notifications_unseen_grouped(max_groups = 5, limit = 200) ⇒ Object

TODO: Is this no longer used?



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/libertree/model/account.rb', line 138

def notifications_unseen_grouped(max_groups=5, limit=200)
  grouped = {}
  keys = [] # so we have a display order

  notifs = self.notifications_unseen.reverse_order(:id).limit(limit)
  notifs.each do |n|
    next  if n.subject.nil?

    target = case n.subject
             when Libertree::Model::Comment, Libertree::Model::PostLike
               n.subject.post
             when Libertree::Model::CommentLike
               n.subject.comment
             when Libertree::Model::Post
                # mention or group post
                post = n.subject
                post.group || post
             else
               n.subject
             end

    # collect by target and type; we don't want to put comment
    # and post like notifs in the same bin
    key = [target, n.subject.class]
    if grouped[key]
      grouped[key] << n
    else
      grouped[key] = [n]
      keys << key
    end
  end

  # get the groups in order of keys
  keys.take(max_groups).map {|k| grouped[k] }
end

#notify_about(data) ⇒ Object



122
123
124
125
126
127
# File 'lib/libertree/model/account.rb', line 122

def notify_about(data)
  Notification.create(
    account_id: self.id,
    data: data.to_json
  )
end

#num_chat_unseenObject



178
179
180
181
182
183
184
185
# File 'lib/libertree/model/account.rb', line 178

def num_chat_unseen
  ChatMessage.where(
    to_member_id: self.member.id,
    seen: false
  ).exclude(
    from_member_id: self.ignored_members.map(&:id)
  ).count
end

#num_chat_unseen_from_partner(member) ⇒ Object



187
188
189
190
191
192
193
194
195
# File 'lib/libertree/model/account.rb', line 187

def num_chat_unseen_from_partner(member)
  ChatMessage.where(
    from_member_id: member.id,
    to_member_id: self.member.id,
    seen: false
  ).exclude(
    from_member_id: self.ignored_members.map(&:id)
  ).count
end

#num_notifications_unseenObject



174
175
176
# File 'lib/libertree/model/account.rb', line 174

def num_notifications_unseen
  @num_notifications_unseen ||= Notification.where(account_id: self.id, seen: false).count
end

#online?Boolean

Returns:

  • (Boolean)


432
433
434
# File 'lib/libertree/model/account.rb', line 432

def online?
  Time.now - time_heartbeat.to_time < 5.01 * 60
end

#passwordObject

These two password methods provide a seamless interface to the BCrypted password. The pseudo-field “password” can be treated like a normal String field for reading and writing.



20
21
22
# File 'lib/libertree/model/account.rb', line 20

def password
  @password ||= BCrypt::Password.new(password_encrypted)
end

#password=(new_password) ⇒ Object



24
25
26
27
# File 'lib/libertree/model/account.rb', line 24

def password=( new_password )
  @password = BCrypt::Password.create(new_password)
  self.password_encrypted = @password
end

#remote_storage_connectionObject



470
471
472
# File 'lib/libertree/model/account.rb', line 470

def remote_storage_connection
  @remote_storage_connection ||= RemoteStorageConnection[ account_id: self.id ]
end

#riversObject



265
266
267
# File 'lib/libertree/model/account.rb', line 265

def rivers
  River.s("SELECT * FROM rivers WHERE account_id = ? ORDER BY position ASC, id DESC", self.id)
end

#rivers_appendedObject



273
274
275
# File 'lib/libertree/model/account.rb', line 273

def rivers_appended
  @rivers_appended ||= rivers.find_all(&:appended_to_all)
end

#rivers_not_appendedObject



269
270
271
# File 'lib/libertree/model/account.rb', line 269

def rivers_not_appended
  rivers.reject(&:appended_to_all)
end

#settingsObject



118
119
120
# File 'lib/libertree/model/account.rb', line 118

def settings
  @settings ||= AccountSettings[ account_id: self.id ]
end

#subscribe_to(post) ⇒ Object



331
332
333
# File 'lib/libertree/model/account.rb', line 331

def subscribe_to(post)
  DB.dbh[ %{SELECT subscribe_account_to_post(?,?)}, self.id, post.id ].get
end

#subscribed_to?(post) ⇒ Boolean

Returns:

  • (Boolean)


339
340
341
# File 'lib/libertree/model/account.rb', line 339

def subscribed_to?(post)
  DB.dbh[ "SELECT account_subscribed_to_post( ?, ? )", self.id, post.id ].single_value
end

#unignore_member(member) ⇒ Object



491
492
493
494
# File 'lib/libertree/model/account.rb', line 491

def unignore_member(member)
  return  if member.nil?
  Libertree::Model::IgnoredMember.where(account_id: self.id, member_id: member.id).delete
end

#unsubscribe_from(post) ⇒ Object



335
336
337
# File 'lib/libertree/model/account.rb', line 335

def unsubscribe_from(post)
  DB.dbh[ "DELETE FROM post_subscriptions WHERE account_id = ? AND post_id = ?", self.id, post.id ].get
end

#validate_and_set_pubkey(key) ⇒ Object

NOTE: this method does not save the account record



385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/libertree/model/account.rb', line 385

def validate_and_set_pubkey(key)
  # import pubkey into temporary keyring to verify it
  GPGME::Engine.home_dir = Dir.tmpdir
  result = GPGME::Key.import key.to_s

  if result.considered == 1 && result.secret_read == 1
    # Delete the key immediately from the keyring and
    # alert the user in case a secret key was uploaded
    keys = GPGME::Key.find(:secret, result.imports.first.fpr)
    keys.first.delete!(true)  # force deletion of secret key
    keys = nil; result = nil
    raise KeyError, 'secret key imported'
  elsif result.considered == 1 && (result.imported == 1 || result.unchanged == 1)
    # We do not check whether the key matches the given email address.
    # This is not necessary, because we don't search the keyring to get
    # the encryption key when sending emails.  Instead, we just take
    # whatever key the user provided.
    self.pubkey = key.to_s
  else
    raise KeyError, 'invalid key'
  end
end