summaryrefslogtreecommitdiff
path: root/lib/api/discussions.rb
blob: 4c4ec200060a57129d6eb567ac364cf511ac2da9 (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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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
# frozen_string_literal: true

module API
  class Discussions < ::API::Base
    include PaginationParams
    helpers ::API::Helpers::NotesHelpers
    helpers ::RendersNotes

    before { authenticate! }

    Helpers::DiscussionsHelpers.feature_category_per_noteable_type.each do |noteable_type, feature_category|
      parent_type = noteable_type.parent_class.to_s.underscore
      noteables_str = noteable_type.to_s.underscore.pluralize
      noteables_path = noteable_type == Commit ? "repository/#{noteables_str}" : noteables_str

      params do
        requires :id, type: String, desc: "The ID of a #{parent_type}"
      end
      resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
        desc "Get a list of #{noteable_type.to_s.downcase} discussions" do
          success Entities::Discussion
        end
        params do
          requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
          use :pagination
        end

        get ":id/#{noteables_path}/:noteable_id/discussions", feature_category: feature_category do
          noteable = find_noteable(noteable_type, params[:noteable_id])

          discussion_ids = paginate(noteable.discussion_ids_relation)
          notes = readable_discussion_notes(noteable, discussion_ids)

          present Discussion.build_collection(notes, noteable), with: Entities::Discussion
        end

        desc "Get a single #{noteable_type.to_s.downcase} discussion" do
          success Entities::Discussion
        end
        params do
          requires :discussion_id, type: String, desc: 'The ID of a discussion'
          requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
        end
        get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id", feature_category: feature_category do
          noteable = find_noteable(noteable_type, params[:noteable_id])
          notes = readable_discussion_notes(noteable, params[:discussion_id])

          if notes.empty?
            break not_found!("Discussion")
          end

          discussion = Discussion.build(notes, noteable)

          present discussion, with: Entities::Discussion
        end

        desc "Create a new #{noteable_type.to_s.downcase} discussion" do
          success Entities::Discussion
        end
        params do
          requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
          requires :body, type: String, desc: 'The content of a note'
          optional :created_at, type: String, desc: 'The creation date of the note'
          optional :position, type: Hash do
            requires :base_sha, type: String, desc: 'Base commit SHA in the source branch'
            requires :start_sha, type: String, desc: 'SHA referencing commit in target branch'
            requires :head_sha, type: String, desc: 'SHA referencing HEAD of this merge request'
            requires :position_type, type: String, desc: 'Type of the position reference', values: %w(text image)
            optional :new_path, type: String, desc: 'File path after change'
            optional :new_line, type: Integer, desc: 'Line number after change'
            optional :old_path, type: String, desc: 'File path before change'
            optional :old_line, type: Integer, desc: 'Line number before change'
            optional :width, type: Integer, desc: 'Width of the image'
            optional :height, type: Integer, desc: 'Height of the image'
            optional :x, type: Integer, desc: 'X coordinate in the image'
            optional :y, type: Integer, desc: 'Y coordinate in the image'

            optional :line_range, type: Hash, desc: 'Multi-line start and end' do
              optional :start, type: Hash do
                optional :line_code, type: String, desc: 'Start line code for multi-line note'
                optional :type, type: String, desc: 'Start line type for multi-line note'
                optional :old_line, type: String, desc: 'Start old_line line number'
                optional :new_line, type: String, desc: 'Start new_line line number'
              end
              optional :end, type: Hash do
                optional :line_code, type: String, desc: 'End line code for multi-line note'
                optional :type, type: String, desc: 'End line type for multi-line note'
                optional :old_line, type: String, desc: 'End old_line line number'
                optional :new_line, type: String, desc: 'End new_line line number'
              end
            end
          end
        end
        post ":id/#{noteables_path}/:noteable_id/discussions", feature_category: feature_category do
          noteable = find_noteable(noteable_type, params[:noteable_id])
          type = params[:position] ? 'DiffNote' : 'DiscussionNote'
          id_key = noteable.is_a?(Commit) ? :commit_id : :noteable_id

          opts = {
            note: params[:body],
            created_at: params[:created_at],
            type: type,
            noteable_type: noteables_str.classify,
            position: params[:position],
            id_key => noteable.id
          }

          note = create_note(noteable, opts)

          if note.valid?
            present note.discussion, with: Entities::Discussion
          else
            bad_request!("Note #{note.errors.messages}")
          end
        end

        desc "Get comments in a single #{noteable_type.to_s.downcase} discussion" do
          success Entities::Discussion
        end
        params do
          requires :discussion_id, type: String, desc: 'The ID of a discussion'
          requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
        end
        get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes", feature_category: feature_category do
          noteable = find_noteable(noteable_type, params[:noteable_id])
          notes = readable_discussion_notes(noteable, params[:discussion_id])

          if notes.empty?
            break not_found!("Notes")
          end

          present notes, with: Entities::Note
        end

        desc "Add a comment to a #{noteable_type.to_s.downcase} discussion" do
          success Entities::Note
        end
        params do
          requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
          requires :discussion_id, type: String, desc: 'The ID of a discussion'
          requires :body, type: String, desc: 'The content of a note'
          optional :created_at, type: String, desc: 'The creation date of the note'
        end
        post ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes", feature_category: feature_category do
          noteable = find_noteable(noteable_type, params[:noteable_id])
          notes = readable_discussion_notes(noteable, params[:discussion_id])
          first_note = notes.first

          break not_found!("Discussion") if notes.empty?

          unless first_note.part_of_discussion? || first_note.to_discussion.can_convert_to_discussion?
            break bad_request!("Discussion can not be replied to.")
          end

          opts = {
            note: params[:body],
            type: 'DiscussionNote',
            in_reply_to_discussion_id: params[:discussion_id],
            created_at: params[:created_at]
          }
          note = create_note(noteable, opts)

          if note.valid?
            present note, with: Entities::Note
          else
            bad_request!("Note #{note.errors.messages}")
          end
        end

        desc "Get a comment in a #{noteable_type.to_s.downcase} discussion" do
          success Entities::Note
        end
        params do
          requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
          requires :discussion_id, type: String, desc: 'The ID of a discussion'
          requires :note_id, type: Integer, desc: 'The ID of a note'
        end
        get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id", feature_category: feature_category do
          noteable = find_noteable(noteable_type, params[:noteable_id])

          get_note(noteable, params[:note_id])
        end

        desc "Edit a comment in a #{noteable_type.to_s.downcase} discussion" do
          success Entities::Note
        end
        params do
          requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
          requires :discussion_id, type: String, desc: 'The ID of a discussion'
          requires :note_id, type: Integer, desc: 'The ID of a note'
          optional :body, type: String, desc: 'The content of a note'
          optional :resolved, type: Boolean, desc: 'Mark note resolved/unresolved'
          exactly_one_of :body, :resolved
        end
        put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id", feature_category: feature_category do
          noteable = find_noteable(noteable_type, params[:noteable_id])

          if params[:resolved].nil?
            update_note(noteable, params[:note_id])
          else
            resolve_note(noteable, params[:note_id], params[:resolved])
          end
        end

        desc "Delete a comment in a #{noteable_type.to_s.downcase} discussion" do
          success Entities::Note
        end
        params do
          requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
          requires :discussion_id, type: String, desc: 'The ID of a discussion'
          requires :note_id, type: Integer, desc: 'The ID of a note'
        end
        delete ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id", feature_category: feature_category do
          noteable = find_noteable(noteable_type, params[:noteable_id])

          delete_note(noteable, params[:note_id])
        end

        if Noteable.resolvable_types.include?(noteable_type.to_s)
          desc "Resolve/unresolve an existing #{noteable_type.to_s.downcase} discussion" do
            success Entities::Discussion
          end
          params do
            requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
            requires :discussion_id, type: String, desc: 'The ID of a discussion'
            requires :resolved, type: Boolean, desc: 'Mark discussion resolved/unresolved'
          end
          put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id", feature_category: feature_category do
            noteable = find_noteable(noteable_type, params[:noteable_id])

            resolve_discussion(noteable, params[:discussion_id], params[:resolved])
          end
        end
      end
    end

    helpers do
      # rubocop: disable CodeReuse/ActiveRecord
      def readable_discussion_notes(noteable, discussion_ids)
        notes = noteable.notes
          .where(discussion_id: discussion_ids)
          .inc_relations_for_view
          .includes(:noteable)
          .fresh

        # Without RendersActions#prepare_notes_for_rendering,
        # Note#system_note_with_references_visible_for? will attempt to render
        # Markdown references mentioned in the note to see whether they
        # should be redacted. For notes that reference a commit, this
        # would also incur a Gitaly call to verify the commit exists.
        #
        # With prepare_notes_for_rendering, we can avoid Gitaly calls
        # because notes are redacted if they point to projects that
        # cannot be accessed by the user.
        notes = prepare_notes_for_rendering(notes)
        notes.select { |n| n.readable_by?(current_user) }
      end
      # rubocop: enable CodeReuse/ActiveRecord
    end
  end
end