summaryrefslogtreecommitdiffstats
path: root/lib/redmine/acts/mentionable.rb
blob: c7b0c065545ea0aa59d75ced5355029b6d4b426b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
# frozen_string_literal: true

# Redmine - project management software
# Copyright (C) 2006-2023 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

module Redmine
  module Acts
    module Mentionable
      def self.included(base)
        base.extend ClassMethods
      end

      module ClassMethods
        def acts_as_mentionable(options = {})
          class_attribute :mentionable_attributes
          self.mentionable_attributes = options[:attributes]

          attr_accessor :mentioned_users

          send :include, Redmine::Acts::Mentionable::InstanceMethods

          after_save :parse_mentions
        end
      end

      module InstanceMethods
        def self.included(base)
          base.extend ClassMethods
        end

        def notified_mentions
          notified = mentioned_users.to_a
          notified.reject! {|user| user.mail.blank? || user.mail_notification == 'none'}
          if respond_to?(:visible?)
            notified.select! {|user| visible?(user)}
          end
          notified
        end

        private

        def parse_mentions
          mentionable_attrs = self.mentionable_attributes
          saved_mentionable_attrs = self.saved_changes.select{|a| mentionable_attrs.include?(a)}

          saved_mentionable_attrs.each do |key, attr|
            old_value, new_value =  attr
            get_mentioned_users(old_value, new_value)
          end
        end

        def get_mentioned_users(old_content, new_content)
          self.mentioned_users = []

          previous_matches =  scan_for_mentioned_users(old_content)
          current_matches = scan_for_mentioned_users(new_content)
          new_matches = (current_matches - previous_matches).flatten

          if new_matches.any?
            self.mentioned_users = User.visible.active.where(login: new_matches)
          end
        end

        def scan_for_mentioned_users(content)
          return [] if content.nil?

          # remove quoted text
          content = content.gsub(%r{\r\n(?:\>\s)+(.*?)\r\n}m, '')

          text_formatting = Setting.text_formatting
          # Remove text wrapped in pre tags based on text formatting
          case text_formatting
          when 'textile'
            content = content.gsub(%r{<pre>(.*?)</pre>}m, '')
          when 'markdown', 'common_mark'
            content = content.gsub(%r{(~~~|```)(.*?)(~~~|```)}m, '')
          end

          content.scan(MENTION_PATTERN).flatten
        end

        MENTION_PATTERN = /
          (?:^|\W)
          @([A-Za-z0-9_\-@\.]*?)
          (?=
            (?=[[:punct:]][^A-Za-z0-9_\/])|
            ,|
            \.+$|
            \s|
            \]|
            <|
            $)
        /ix
      end
    end
  end
end