Merge remote-tracking branch 'origin/master' into networks
[shapado:cantonics-shapado.git] / app / models / user.rb
1 require 'digest/sha1'
2
3 class User
4   include Mongoid::Document
5   include Mongoid::Timestamps
6   include MultiauthSupport
7   include MongoidExt::Storage
8   include Shapado::Models::GeoCommon
9   include Shapado::Models::Networks
10
11   devise :database_authenticatable, :recoverable, :registerable, :rememberable,
12          :lockable, :token_authenticatable, :encryptable, :trackable, :omniauthable, :encryptor => :restful_authentication_sha1
13
14   ROLES = %w[user moderator admin]
15   LANGUAGE_FILTERS = %w[any user] + AVAILABLE_LANGUAGES
16   LOGGED_OUT_LANGUAGE_FILTERS = %w[any] + AVAILABLE_LANGUAGES
17
18   identity :type => String
19   field :login,                     :type => String, :limit => 40, :index => true
20   field :name,                      :type => String, :limit => 100, :default => '', :null => true
21
22   field :bio,                       :type => String, :limit => 200
23   field :website,                   :type => String, :limit => 200
24   field :location,                  :type => String, :limit => 200
25   field :birthday,                  :type => Time
26
27   field :identity_url,              :type => String
28   index :identity_url
29
30   field :role,                      :type => String, :default => "user"
31   field :last_logged_at,            :type => Time
32
33   field :preferred_languages,       :type => Array, :default => []
34
35   field :language,                  :type => String, :default => "en"
36   index :language
37   field :timezone,                  :type => String
38   field :language_filter,           :type => String, :default => "user", :in => LANGUAGE_FILTERS
39
40   field :ip,                        :type => String
41   field :country_code,              :type => String
42   field :country_name,              :type => String, :default => "unknown"
43   field :hide_country,              :type => Boolean, :default => false
44
45   field :default_subtab,            :type => Hash, :default => {}
46
47   field :followers_count,           :type => Integer, :default => 0
48   field :following_count,           :type => Integer, :default => 0
49
50   field :group_ids,                 :type => Array, :default => []
51
52   field :feed_token,                :type => String, :default => lambda { BSON::ObjectId.new.to_s }
53   field :socket_key,                :type => String, :default => lambda { BSON::ObjectId.new.to_s }
54
55   field :anonymous,                 :type => Boolean, :default => false
56   index :anonymous
57
58   field :networks, :type => Hash, :default => {}
59
60   field :friend_list_id, :type => String
61   embeds_one :notification_opts, :class_name => "NotificationConfig"
62
63   file_key :avatar, :max_length => 1.megabytes
64   field :use_gravatar, :type => Boolean, :default => true
65   file_list :thumbnails
66
67   referenced_in :friend_list
68
69   references_many :memberships, :class_name => "Membership"
70   references_many :owned_groups, :inverse_of => :user, :class_name => "Group"
71   references_many :questions, :dependent => :destroy
72   references_many :answers, :dependent => :destroy
73   references_many :badges, :dependent => :destroy
74   references_many :searches, :dependent => :destroy
75   references_many :activities, :dependent => :destroy
76   references_many :invitations, :dependent => :destroy
77   references_one :external_friends_list, :dependent => :destroy
78
79   before_create :initialize_fields
80   after_create :create_friends_lists
81
82   before_create :generate_uuid
83   after_create :update_anonymous_user
84
85   validates_inclusion_of :language, :in => AVAILABLE_LOCALES
86   validates_inclusion_of :role,  :in => ROLES
87
88   with_options :if => lambda { |e| !e.anonymous } do |v|
89     v.validates_presence_of     :login
90     v.validates_length_of       :login,    :in => 3..40
91     v.validates_uniqueness_of   :login
92     v.validates_format_of       :login,    :with => /\w+/
93   end
94
95   validates_length_of       :name,     :maximum => 100
96
97   validates_presence_of     :email,    :if => lambda { |e| !e.openid_login? && !e.twitter_login? }
98   validates_uniqueness_of   :email,    :if => lambda { |e| e.anonymous || (!e.openid_login? && !e.twitter_login?) }
99   validates_length_of       :email,    :in => 6..100, :allow_nil => true, :if => lambda { |e| !e.email.blank? }
100
101   with_options :if => :password_required? do |v|
102     v.validates_presence_of     :password
103     v.validates_confirmation_of :password
104     v.validates_length_of       :password, :in => 6..20, :allow_blank => true
105   end
106
107   before_save :update_languages
108
109   def display_name
110     name.blank? ? login : name
111   end
112
113   def self.find_for_authentication(conditions={})
114     where(conditions).first || where(:login => conditions[:email]).first
115   end
116
117   def inactive_membership_list
118     self.memberships.where(:state => 'inactive')
119   end
120
121   def login=(value)
122     write_attribute :login, (value ? value.downcase : nil)
123   end
124
125   def email=(value)
126     write_attribute :email, (value ? value.downcase : nil)
127   end
128
129   def networks=(params)
130     self[:networks] = self.find_networks(params)
131   end
132
133   def self.find_by_login_or_id(login, conds = {})
134     where(conds.merge(:login => login)).first || where(conds.merge(:_id => login)).first
135   end
136
137   def self.find_experts(tags, langs = AVAILABLE_LANGUAGES, options = {})
138     opts = {}
139
140     if except = options[:except]
141       except = [except] unless except.is_a?(Array)
142       opts[:user_id] = {:$nin => except}
143     end
144
145     user_ids = UserStat.only(:user_id).where(opts.merge({:answer_tags => {:$in => tags}})).all.map(&:user_id)
146
147     conditions = {:"notification_opts.give_advice" => {:$in => ["1", true]},
148                   :preferred_languages.in => langs,
149                   :_id.in => user_ids}
150
151     if group_id = options[:group_id]
152       conditions[:"group_ids"] = {:$in => group_id}
153     end
154
155     User.only([:email, :login, :name, :language]).where(conditions)
156   end
157
158   def to_param
159     if self.login.blank? || !self.login.match(/^\w[\w\s]*$/)
160       self.id
161     else
162       self.login
163     end
164   end
165
166   def add_preferred_tags(t, group)
167     if t.kind_of?(String)
168       t = t.split(",").map{|e| e.strip}
169     end
170
171     Membership.push_uniq({:group_id => group.id, :user_id => self.id}, {:preferred_tags => {:$each => t.uniq}})
172   end
173
174   def remove_preferred_tags(t, group)
175     if t.kind_of?(String)
176       t = t.split(",").join(" ").split(" ")
177     end
178     Membership.pull_all({:group_id => group.id, :user_id => self.id}, {:preferred_tags => t})
179   end
180
181   def preferred_tags_on(group)
182     @group_preferred_tags ||= (config_for(group, false).preferred_tags || []).to_a
183   end
184
185   def language_filter=(filter)
186     if LANGUAGE_FILTERS.include? filter
187       self[:language_filter] = filter
188       true
189     else
190       false
191     end
192   end
193
194   def languages_to_filter(group)
195     @languages_to_filter ||= begin
196       languages = nil
197       case self.language_filter
198       when "any"
199         languages = group.languages
200       when "user"
201         languages = (self.preferred_languages.empty?) ? group.languages : self.preferred_languages
202       else
203         languages = [self.language_filter]
204       end
205       languages
206     end
207   end
208
209   def is_preferred_tag?(group, *tags)
210     if config = config_for(group, false)
211       ptags = config.preferred_tags
212       tags.detect { |t| ptags.include?(t) } if ptags
213     else
214       false
215     end
216   end
217
218   def admin?
219     self.role == "admin"
220   end
221
222   def age
223     return if self.birthday.blank?
224
225     Time.zone.now.year - self.birthday.year - (self.birthday.to_time.change(:year => Time.zone.now.year) >
226 Time.zone.now ? 1 : 0)
227   end
228
229   def can_modify?(model)
230     return false unless model.respond_to?(:user)
231     self.admin? || self == model.user
232   end
233
234   def can_create_reward?(question)
235     (Time.now - question.created_at) >= 2.days &&
236     config_for(question.group_id).reputation >= 75 &&
237     (question.reward.nil? || !question.reward.active)
238   end
239
240   def groups(options = {})
241     Group.where(options.merge(:_id.in => self.group_ids)).order_by([:activity_rate, :desc])
242   end
243
244   def member_of?(group)
245     if group.kind_of?(Group)
246       group = group.id
247     end
248
249     self.group_ids.include?(group)
250   end
251
252   def role_on(group)
253     if config = config_for(group, false)
254       config.role
255     end
256   end
257
258   def owner_of?(group)
259     admin? || group.owner_id == self.id || role_on(group) == "owner"
260   end
261
262   def admin_of?(group)
263     role_on(group) == "admin" || owner_of?(group)
264   end
265
266   def mod_of?(group)
267     owner_of?(group) || role_on(group) == "moderator" || self.reputation_on(group).to_i >= group.reputation_constrains["moderate"].to_i
268   end
269
270   def editor_of?(group)
271     if c = config_for(group, false)
272       c.is_editor
273     else
274       false
275     end
276   end
277
278   def user_of?(group)
279     mod_of?(group) || member_of?(group)
280   end
281
282   def main_language
283     @main_language ||= self.language.split("-").first
284   end
285
286   def openid_login?
287     !self.auth_keys.blank? || (AppConfig.enable_facebook_auth && !facebook_id.blank?)
288   end
289
290   def linked_in_login?
291     user_info && !user_info["linked_in"].blank? && linked_in_id
292   end
293
294   def identica_login?
295     user_info && !user_info["identica"].blank? && identica_id
296   end
297
298   def twitter_login?
299     user_info && !user_info["twitter"].blank? && twitter_id
300   end
301
302   def facebook_login?
303     user_info && !user_info["facebook"].blank? && facebook_id
304   end
305
306   def social_connections
307     connections = []
308     connections << "linked_in" if linked_in_login?
309     connections << "identica" if identica_login?
310     connections << "twitter" if twitter_login?
311     connections << "facebook" if facebook_login?
312     return connections
313   end
314
315   def is_socially_connected?
316     linked_in_login? || identica_login? || twitter_login? ||
317       facebook_login?
318   end
319
320   def has_voted?(voteable)
321     !vote_on(voteable).nil?
322   end
323
324   def vote_on(voteable)
325     voteable.votes[self.id] if voteable.votes
326   end
327
328   def favorites(opts = {})
329     Answer.where(opts.merge(:favoriter_ids.in => id))
330   end
331
332   def logged!(group = nil)
333     now = Time.zone.now
334
335     if group
336       unless member_of?(group)
337         join!(group)
338       end
339
340       if member_of?(group)
341         on_activity(:login, group)
342       end
343     end
344   end
345
346   def on_activity(activity, group)
347     self.update_reputation(activity, group) if activity != :login
348     activity_on(group, Time.zone.now)
349   end
350
351   def activity_on(group, date)
352     Membership.override({:group_id => group.id, :user_id => self.id}, {:last_activity_at => date.utc})
353
354     day = date.utc.at_beginning_of_day
355     last_day = nil
356     if last_activity_at = config_for(group, false).last_activity_at
357       last_day = last_activity_at.at_beginning_of_day
358     end
359
360     if last_day != day
361       if last_day
362         if last_day.utc.between?(day.yesterday - 12.hours, day.tomorrow)
363           Membership.increment({:group_id => group.id, :user_id => self.id}, {:activity_days => 1})
364
365           Jobs::Activities.async.on_activity(group.id, self.id).commit!
366         elsif !last_day.utc.today? && (last_day.utc != Time.now.utc.yesterday)
367           Rails.logger.info ">> Resetting act days!! last known day: #{last_day}"
368           reset_activity_days!(group)
369         end
370       end
371     end
372   end
373
374   def reset_activity_days!(group)
375     Membership.override({:group_id => group.id, :user_id => self.id}, {:activity_days => 0})
376   end
377
378   def upvote!(group, v = 1.0)
379     Membership.override({:group_id => group.id, :user_id => self.id}, {:votes_up => v.to_f})
380   end
381
382   def downvote!(group, v = 1.0)
383     Membership.override({:group_id => group.id, :user_id => self.id}, {:votes_down => v.to_f})
384   end
385
386   def update_reputation(key, group, v = nil)
387     unless member_of?(group)
388       join!(group)
389     end
390
391     if v.nil?
392       value = group.reputation_rewards[key.to_s].to_i
393       value = key if key.kind_of?(Integer)
394     else
395       value = v
396     end
397
398     Rails.logger.info "#{self.login} received #{value} points of karma by #{key} on #{group.name}"
399     current_reputation = config_for(group, false).reputation
400
401     if value
402       Membership.override({:group_id => group.id, :user_id => self.id}, {:reputation => value})
403     end
404
405     stats = self.reputation_stats(group)
406     stats.save if stats.new?
407
408     event = ReputationEvent.new(:time => Time.now, :event => key,
409                                 :reputation => current_reputation,
410                                 :delta => value )
411     ReputationStat.collection.update({:_id => stats.id}, {:$addToSet => {:events => event.attributes}})
412   end
413
414   def reputation_on(group)
415     if config = config_for(group, false)
416       config.reputation.to_i
417     else
418       0
419     end
420   end
421
422   def views_on(group)
423     if config = config_for(group, false)
424       config.views_count.to_i
425     else
426       0
427     end
428   end
429
430   def stats(*extra_fields)
431     fields = [:_id]
432
433     UserStat.only(fields+extra_fields).where(:user_id => self.id).first || UserStat.create(:user_id => self.id)
434   end
435
436   def badges_count_on(group)
437     config = config_for(group, false)
438     if config
439       [config.bronze_badges_count, config.silver_badges_count, config.gold_badges_count]
440     else
441       [0,0,0]
442     end
443   end
444
445   def badges_on(group, opts = {})
446     self.badges.where(opts.merge(:group_id => group.id)).order_by(:created_at.desc)
447   end
448
449   def find_badge_on(group, token, opts = {})
450     self.badges.where(opts.merge(:token => token, :group_id => group.id)).first
451   end
452
453   # self follows user
454   def add_friend(user)
455     return false if user == self
456     FriendList.collection.update({ "_id" => self.friend_list_id}, { "$addToSet" => { :following_ids => user.id } })
457     FriendList.collection.update({ "_id" => user.friend_list_id}, { "$addToSet" => { :follower_ids => self.id } })
458
459     self.inc(:following_count, 1)
460     user.inc(:followers_count, 1)
461     true
462   end
463
464   def remove_friend(user)
465     return false if user == self
466     FriendList.collection.update({ "_id" => self.friend_list_id}, { "$pull" => { :following_ids => user.id } })
467     FriendList.collection.update({ "_id" => user.friend_list_id}, { "$pull" => { :follower_ids => self.id } })
468
469     self.inc(:following_count, -1)
470     user.inc(:followers_count, -1)
471     true
472   end
473
474   def followers(scope = {})
475     conditions = {}
476     conditions[:preferred_languages] = {:$in => scope[:languages]}  if scope[:languages]
477     conditions[:"group_ids"] = {:$in => scope[:group_id]} if scope[:group_id]
478     User.where(conditions.merge(:_id.in => self.friend_list.follower_ids)) # FIXME mongoid
479   end
480
481   def following
482     User.where(:_id.in => self.friend_list.following_ids)
483   end
484
485   def following?(user)
486     FriendList.only(:following_ids).where(:_id => self.friend_list_id).first.following_ids.include?(user.id)
487   end
488
489   def viewed_on!(group)
490     if member_of?(group)
491       Membership.override({:group_id => group.id, :user_id => self.id}, {:views_count => 1.0})
492     end
493   end
494
495   def method_missing(method, *args, &block)
496     if !args.empty? && method.to_s =~ /can_(\w*)\_on?/
497       key = $1
498       group = args.first
499       if group.reputation_constrains.include?(key.to_s)
500         if group.has_reputation_constrains
501           if self.member_of? group
502             return self.owner_of?(group) || self.mod_of?(group) || (self.reputation_on(group) >= group.reputation_constrains[key].to_i)
503           else
504             return false
505           end
506         else
507           return true
508         end
509       end
510     end
511     super(method, *args, &block)
512   end
513
514   def config_for(group, init = false)
515     membership_selector_for(group).first
516   end
517
518   def membership_selector_for(group)
519     if group.kind_of?(Group)
520       group = group.id
521     end
522
523     Membership.where(:user_id => self.id, :group_id => group)
524   end
525
526   def leave(group)
527     if group.kind_of?(Group)
528       group = group.id
529     end
530
531     membership = config_for(group)
532     if membership
533       membership.state = 'inactive'
534       membership.save
535     end
536   end
537
538   def join(group, &block)
539     if group.kind_of?(Group)
540       group = group.id
541     end
542
543     membership = Membership.create({
544      :user_id => self.id,
545      :group_id => group,
546      :last_activity_at => Time.now,
547      :joined_at => Time.now
548     })
549
550     block.call(membership) if block
551
552     membership
553   end
554
555   def join!(group, &block)
556     if join(group, &block)
557       save!
558     end
559   end
560
561   def reputation_stats(group, options = {})
562     if group.kind_of?(Group)
563       group = group.id
564     end
565     default_options = { :user_id => self.id,
566                         :group_id => group}
567     stats = ReputationStat.where(default_options.merge(options)).first ||
568             ReputationStat.new(default_options)
569   end
570
571   def has_flagged?(flaggeable)
572     flaggeable.flags.detect do |flag|
573       flag.user_id == self.id
574     end
575   end
576
577   def has_requested_to_close?(question)
578     question.close_requests.detect do |close_request|
579       close_request.user_id == self.id
580     end
581   end
582
583   def has_requested_to_open?(question)
584     question.open_requests.detect do |open_request|
585       open_request.user_id == self.id
586     end
587   end
588
589   def generate_uuid
590     self.feed_token = UUIDTools::UUID.random_create.hexdigest
591   end
592
593   def self.find_file_from_params(params, request)
594     if request.path =~ %r{/(avatar|big|medium|small)/([^/\.\?]+)}
595       @user = User.find($2)
596       avatar = @user.has_avatar? ? @user.avatar : Shapado::FileWrapper.new("#{Rails.root}/public/images/avatar-25.png", "image/png")
597       case $1
598       when "avatar"
599         @user.avatar
600       when "big"
601         @user.thumbnails["big"] ? @user.thumbnails.get("big") : avatar
602       when "medium"
603         @user.thumbnails["medium"] ? @user.thumbnails.get("medium") : avatar
604       when "small"
605         @user.thumbnails["small"] ? @user.thumbnails.get("small") : avatar
606       end
607     end
608   end
609
610   def facebook_friends
611     self.external_friends_list.friends["facebook"]
612   end
613
614   def social_friends_ids(provider)
615     self.send(provider+'_friends').map do |friend| friend["id"].to_s end
616   end
617
618   def twitter_friends
619     self.external_friends_list.friends["twitter"]
620   end
621
622   def identica_friends
623     self.external_friends_list.friends["identica"]
624   end
625
626   def linked_in_friends
627     self.external_friends_list.friends["linked_in"]
628   end
629
630   ## TODO: add google contacts
631   def suggestions(group, limit = 5)
632     sample = (suggested_social_friends(group, limit) | suggested_tags_by_suggested_friends(group, limit) ).sample(limit)
633
634     # if we find less suggestions than requested, complete with
635     # most popular users and tags
636     (sample.size < limit) ? sample |
637       (group.top_tags_strings(limit+15)-self.preferred_tags_on(group) + group.top_users(limit+5)-[self]).
638       sample(limit-sample.size) : sample
639   end
640
641   # returns tags followed by my friends but not by self
642   # TODO: optimize
643   def suggested_tags(group, limit = 5)
644     friends = Membership.where(:group_id => group.id,
645                                :user_id.in => self.friend_list.following_ids,
646                                :preferred_tags => {"$ne" => [], "$ne" => nil}).
647                          only(:preferred_tags, :login, :name)
648
649     friends_tags = { }
650     friends.each do |friend|
651       (friend.preferred_tags-self.preferred_tags_on(group)).each do |tag|
652         friends_tags["#{tag}"] ||= { }
653         friends_tags["#{tag}"]["followed_by"] ||= []
654         friends_tags["#{tag}"]["followed_by"] << friend
655       end
656     end
657      friends_tags.to_a.sample(limit)
658   end
659
660   #returns tags followed by self suggested friends that I may not follow
661   def suggested_tags_by_suggested_friends(group, limit = 5)
662     friends = suggested_social_friends(group, limit).only(:_id).map{|u| u.id}
663     unless friends.blank?
664       memberships = Membership.where(:group_id => group.id,
665                                      :user_id.in => friends,
666                                      :preferred_tags => {"$ne" => [], "$ne" => nil},
667                                      :_id => {:$not => {:$in => self.friend_list.following_ids}}).
668                          only(:preferred_tags, :login, :name)
669
670       friends_tags = { }
671       memberships.each do |membership|
672         friend_preferred_tags = membership.preferred_tags
673
674         if friend_preferred_tags
675           (friend_preferred_tags-self.preferred_tags_on(group)).each do |tag|
676             friends_tags["#{tag}"] ||= { }
677             friends_tags["#{tag}"]["followed_by"] ||= []
678             friends_tags["#{tag}"]["followed_by"] << friend
679           end
680         end
681       end
682       friends_tags.to_a.sample(limit)
683     end
684     []
685   end
686
687   # returns user's providers friends that have an account
688   # on shapado but that user is not following
689   def suggested_social_friends(group, limit = 5)
690     array_hash = []
691     social_connections.to_a.each do |provider|
692       unless external_friends_list.friends[provider].blank?
693         array_hash << { "#{provider}_id".to_sym => {:$in => self.social_friends_ids(provider)}}
694       end
695     end
696     (array_hash.blank?)? [] : User.any_of(array_hash).
697       where({:group_ids => group.id,
698              :_id => {:$not => {:$in => self.friend_list.following_ids}}}).
699       limit(limit)
700   end
701
702   # returns user's friends on other social networks that already have an account on shapado
703   def social_external_friends
704     array_hash = []
705     provider_ids = []
706     social_connections.to_a.each do |provider|
707       array_hash << { "#{provider}_id".to_sym => {:$in => self.social_friends_ids(provider)}}
708       provider_ids << "#{provider}_id"
709     end
710     User.any_of(array_hash).
711       only(provider_ids)
712   end
713
714   # returns a follower that is someone self follows
715   # if @user follows bob and bob follows bill
716   # @user.common_follower(bill) will return bob
717   def common_follower(user)
718     User.where(:_id => (self.friend_list.following_ids & user.friend_list.follower_ids).sample).first
719   end
720
721   def invite(email, user_role, group)
722     if self.can_invite_on?(group)
723       Invitation.create(:user_id => self.id,
724                         :email => email,
725                         :group_id => group.id,
726                         :user_role => user_role)
727     end
728   end
729
730   def revoke_invite(invitation)
731     invitation.destroy if self.can_modify?(invitation)
732   end
733
734   def can_invite_on?(group)
735     return true if self.admin_of?(group) || self.role == 'admin' ||
736       group.invitations_perms == 'user' ||
737       (group.invitations_perms == 'moderator' &&
738        self.mod_of?(group))
739     return false
740   end
741
742   def accept_invitation(invitation_id)
743     invitation = Invitation.find(invitation_id)
744     group = invitation.group
745     invitation.update_attributes(:accepted => true,
746                                  :accepted_by => self.id,
747                                  :accepted_at => Time.now) &&
748       group.add_member(self, invitation.user_role)
749   end
750
751   def pending_invitations(group)
752       Invitation.where(:state => 'pending',
753                        :group_id => group.id,
754                        :user_id => self.id)
755   end
756
757   protected
758   def update_languages
759     self.preferred_languages = self.preferred_languages.map { |e| e.split("-").first }
760   end
761
762   def password_required?
763     return false if openid_login? || twitter_login? || self.anonymous
764
765     (encrypted_password.blank? || !password.blank?)
766   end
767
768   def initialize_fields
769     self.friend_list = FriendList.create if self.friend_list.nil?
770     self.notification_opts = NotificationConfig.new if self.notification_opts.nil?
771   end
772
773   def update_anonymous_user
774     return if self.anonymous
775
776     user = User.where({:email => self.email, :anonymous => true}).first
777     if user.present?
778       Rails.logger.info "Merging #{self.email}(#{self.id}) into #{user.email}(#{user.id})"
779       merge_user(user)
780
781       user.destroy
782     end
783   end
784
785   def create_friends_lists
786     external_friends_list = ExternalFriendsList.create
787     self.external_friends_list = external_friends_list
788   end
789 end