]> git.openstreetmap.org Git - rails.git/blob - app/controllers/api/notes_controller.rb
Merge pull request #5314 from AntonKhorev/note-subscriptions-api
[rails.git] / app / controllers / api / notes_controller.rb
1 module Api
2   class NotesController < ApiController
3     before_action :check_api_writable, :only => [:create, :comment, :close, :reopen, :destroy]
4     before_action :setup_user_auth, :only => [:create, :show]
5     before_action :authorize, :only => [:close, :reopen, :destroy, :comment]
6
7     authorize_resource
8
9     before_action :set_locale
10     before_action :set_request_formats, :except => [:feed]
11
12     ##
13     # Return a list of notes in a given area
14     def index
15       # Figure out the bbox - we prefer a bbox argument but also
16       # support the old, deprecated, method with four arguments
17       if params[:bbox]
18         bbox = BoundingBox.from_bbox_params(params)
19       elsif params[:l] && params[:r] && params[:b] && params[:t]
20         bbox = BoundingBox.from_lrbt_params(params)
21       else
22         raise OSM::APIBadUserInput, "The parameter bbox is required"
23       end
24
25       # Get any conditions that need to be applied
26       notes = closed_condition(Note.all)
27
28       # Check that the boundaries are valid
29       bbox.check_boundaries
30
31       # Check the the bounding box is not too big
32       bbox.check_size(Settings.max_note_request_area)
33       @min_lon = bbox.min_lon
34       @min_lat = bbox.min_lat
35       @max_lon = bbox.max_lon
36       @max_lat = bbox.max_lat
37
38       # Find the notes we want to return
39       @notes = notes.bbox(bbox).order("updated_at DESC").limit(result_limit).preload(:comments)
40
41       # Render the result
42       respond_to do |format|
43         format.rss
44         format.xml
45         format.json
46         format.gpx
47       end
48     end
49
50     ##
51     # Read a note
52     def show
53       # Check the arguments are sane
54       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
55
56       # Find the note and check it is valid
57       @note = Note.find(params[:id])
58       raise OSM::APINotFoundError unless @note
59       raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible? || current_user&.moderator?
60
61       # Render the result
62       respond_to do |format|
63         format.xml
64         format.rss
65         format.json
66         format.gpx
67       end
68     end
69
70     ##
71     # Create a new note
72     def create
73       # Check the ACLs
74       raise OSM::APIAccessDenied if current_user.nil? && Acl.no_note_comment(request.remote_ip)
75
76       # Check the arguments are sane
77       raise OSM::APIBadUserInput, "No lat was given" unless params[:lat]
78       raise OSM::APIBadUserInput, "No lon was given" unless params[:lon]
79       raise OSM::APIBadUserInput, "No text was given" if params[:text].blank?
80
81       # Extract the arguments
82       lon = OSM.parse_float(params[:lon], OSM::APIBadUserInput, "lon was not a number")
83       lat = OSM.parse_float(params[:lat], OSM::APIBadUserInput, "lat was not a number")
84       comment = params[:text]
85
86       # Include in a transaction to ensure that there is always a note_comment for every note
87       Note.transaction do
88         # Create the note
89         @note = Note.create(:lat => lat, :lon => lon)
90         raise OSM::APIBadUserInput, "The note is outside this world" unless @note.in_world?
91
92         # Save the note
93         @note.save!
94
95         # Add a comment to the note
96         add_comment(@note, comment, "opened")
97       end
98
99       # Return a copy of the new note
100       respond_to do |format|
101         format.xml { render :action => :show }
102         format.json { render :action => :show }
103       end
104     end
105
106     ##
107     # Delete (hide) a note
108     def destroy
109       # Check the arguments are sane
110       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
111
112       # Extract the arguments
113       id = params[:id].to_i
114       comment = params[:text]
115
116       # Find the note and check it is valid
117       Note.transaction do
118         @note = Note.lock.find(id)
119         raise OSM::APINotFoundError unless @note
120         raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
121
122         # Mark the note as hidden
123         @note.status = "hidden"
124         @note.save
125
126         add_comment(@note, comment, "hidden", :notify => false)
127       end
128
129       # Return a copy of the updated note
130       respond_to do |format|
131         format.xml { render :action => :show }
132         format.json { render :action => :show }
133       end
134     end
135
136     ##
137     # Add a comment to an existing note
138     def comment
139       # Check the arguments are sane
140       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
141       raise OSM::APIBadUserInput, "No text was given" if params[:text].blank?
142
143       # Extract the arguments
144       id = params[:id].to_i
145       comment = params[:text]
146
147       # Find the note and check it is valid
148       Note.transaction do
149         @note = Note.lock.find(id)
150         raise OSM::APINotFoundError unless @note
151         raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
152         raise OSM::APINoteAlreadyClosedError, @note if @note.closed?
153
154         # Add a comment to the note
155         add_comment(@note, comment, "commented")
156       end
157
158       # Return a copy of the updated note
159       respond_to do |format|
160         format.xml { render :action => :show }
161         format.json { render :action => :show }
162       end
163     end
164
165     ##
166     # Close a note
167     def close
168       # Check the arguments are sane
169       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
170
171       # Extract the arguments
172       id = params[:id].to_i
173       comment = params[:text]
174
175       # Find the note and check it is valid
176       Note.transaction do
177         @note = Note.lock.find_by(:id => id)
178         raise OSM::APINotFoundError unless @note
179         raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
180         raise OSM::APINoteAlreadyClosedError, @note if @note.closed?
181
182         # Close the note and add a comment
183         @note.close
184
185         add_comment(@note, comment, "closed")
186       end
187
188       # Return a copy of the updated note
189       respond_to do |format|
190         format.xml { render :action => :show }
191         format.json { render :action => :show }
192       end
193     end
194
195     ##
196     # Reopen a note
197     def reopen
198       # Check the arguments are sane
199       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
200
201       # Extract the arguments
202       id = params[:id].to_i
203       comment = params[:text]
204
205       # Find the note and check it is valid
206       Note.transaction do
207         @note = Note.lock.find_by(:id => id)
208         raise OSM::APINotFoundError unless @note
209         raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible? || current_user.moderator?
210         raise OSM::APINoteAlreadyOpenError, @note unless @note.closed? || !@note.visible?
211
212         # Reopen the note and add a comment
213         @note.reopen
214
215         add_comment(@note, comment, "reopened")
216       end
217
218       # Return a copy of the updated note
219       respond_to do |format|
220         format.xml { render :action => :show }
221         format.json { render :action => :show }
222       end
223     end
224
225     ##
226     # Get a feed of recent notes and comments
227     def feed
228       # Get any conditions that need to be applied
229       notes = closed_condition(Note.all)
230       notes = bbox_condition(notes)
231
232       # Find the comments we want to return
233       @comments = NoteComment.where(:note => notes)
234                              .order(:created_at => :desc).limit(result_limit)
235                              .preload(:author, :note => { :comments => :author })
236
237       # Render the result
238       respond_to do |format|
239         format.rss
240       end
241     end
242
243     ##
244     # Return a list of notes matching a given string
245     def search
246       # Get the initial set of notes
247       @notes = closed_condition(Note.all)
248       @notes = bbox_condition(@notes)
249
250       # Add any user filter
251       if params[:display_name] || params[:user]
252         if params[:display_name]
253           @user = User.find_by(:display_name => params[:display_name])
254
255           raise OSM::APIBadUserInput, "User #{params[:display_name]} not known" unless @user
256         else
257           @user = User.find_by(:id => params[:user])
258
259           raise OSM::APIBadUserInput, "User #{params[:user]} not known" unless @user
260         end
261
262         @notes = @notes.joins(:comments).where(:note_comments => { :author_id => @user })
263       end
264
265       # Add any text filter
266       @notes = @notes.joins(:comments).where("to_tsvector('english', note_comments.body) @@ plainto_tsquery('english', ?)", params[:q]) if params[:q]
267
268       # Add any date filter
269       if params[:from]
270         begin
271           from = Time.parse(params[:from]).utc
272         rescue ArgumentError
273           raise OSM::APIBadUserInput, "Date #{params[:from]} is in a wrong format"
274         end
275
276         begin
277           to = if params[:to]
278                  Time.parse(params[:to]).utc
279                else
280                  Time.now.utc
281                end
282         rescue ArgumentError
283           raise OSM::APIBadUserInput, "Date #{params[:to]} is in a wrong format"
284         end
285
286         @notes = if params[:sort] == "updated_at"
287                    @notes.where(:updated_at => from..to)
288                  else
289                    @notes.where(:created_at => from..to)
290                  end
291       end
292
293       # Choose the sort order
294       @notes = if params[:sort] == "created_at"
295                  if params[:order] == "oldest"
296                    @notes.order("created_at ASC")
297                  else
298                    @notes.order("created_at DESC")
299                  end
300                else
301                  if params[:order] == "oldest"
302                    @notes.order("updated_at ASC")
303                  else
304                    @notes.order("updated_at DESC")
305                  end
306                end
307
308       # Find the notes we want to return
309       @notes = @notes.distinct.limit(result_limit).preload(:comments)
310
311       # Render the result
312       respond_to do |format|
313         format.rss { render :action => :index }
314         format.xml { render :action => :index }
315         format.json { render :action => :index }
316         format.gpx { render :action => :index }
317       end
318     end
319
320     private
321
322     #------------------------------------------------------------
323     # utility functions below.
324     #------------------------------------------------------------
325
326     ##
327     # Get the maximum number of results to return
328     def result_limit
329       if params[:limit]
330         if params[:limit].to_i.positive? && params[:limit].to_i <= Settings.max_note_query_limit
331           params[:limit].to_i
332         else
333           raise OSM::APIBadUserInput, "Note limit must be between 1 and #{Settings.max_note_query_limit}"
334         end
335       else
336         Settings.default_note_query_limit
337       end
338     end
339
340     ##
341     # Generate a condition to choose which notes we want based
342     # on their status and the user's request parameters
343     def closed_condition(notes)
344       closed_since = if params[:closed]
345                        params[:closed].to_i.days
346                      else
347                        Note::DEFAULT_FRESHLY_CLOSED_LIMIT
348                      end
349
350       if closed_since.negative?
351         notes.where.not(:status => "hidden")
352       elsif closed_since.positive?
353         notes.where(:status => "open")
354              .or(notes.where(:status => "closed")
355                       .where(notes.arel_table[:closed_at].gt(Time.now.utc - closed_since)))
356       else
357         notes.where(:status => "open")
358       end
359     end
360
361     ##
362     # Generate a condition to choose which notes we want based
363     # on the user's bounding box request parameters
364     def bbox_condition(notes)
365       if params[:bbox]
366         bbox = BoundingBox.from_bbox_params(params)
367
368         bbox.check_boundaries
369         bbox.check_size(Settings.max_note_request_area)
370
371         @min_lon = bbox.min_lon
372         @min_lat = bbox.min_lat
373         @max_lon = bbox.max_lon
374         @max_lat = bbox.max_lat
375
376         notes.bbox(bbox)
377       else
378         notes
379       end
380     end
381
382     ##
383     # Add a comment to a note
384     def add_comment(note, text, event, notify: true)
385       attributes = { :visible => true, :event => event, :body => text }
386
387       if doorkeeper_token
388         author = current_user if scope_enabled?(:write_notes)
389       else
390         author = current_user
391       end
392
393       if author
394         attributes[:author_id] = author.id
395       else
396         attributes[:author_ip] = request.remote_ip
397       end
398
399       comment = note.comments.create!(attributes)
400
401       if notify
402         note.subscribers.visible.each do |user|
403           UserMailer.note_comment_notification(comment, user).deliver_later if current_user != user
404         end
405       end
406
407       NoteSubscription.find_or_create_by(:note => note, :user => current_user) if current_user
408     end
409   end
410 end