]> git.openstreetmap.org Git - rails.git/commitdiff
Merge branch 'pull/5030'
authorAnton Khorev <tony29@yandex.ru>
Wed, 31 Jul 2024 13:56:28 +0000 (16:56 +0300)
committerAnton Khorev <tony29@yandex.ru>
Wed, 31 Jul 2024 13:56:28 +0000 (16:56 +0300)
34 files changed:
.rubocop_todo.yml
Gemfile.lock
app/abilities/api_capability.rb
app/controllers/api/messages_controller.rb [new file with mode: 0644]
app/views/api/messages/_message.json.jbuilder [new file with mode: 0644]
app/views/api/messages/_message.xml.builder [new file with mode: 0644]
app/views/api/messages/inbox.json.jbuilder [new file with mode: 0644]
app/views/api/messages/inbox.xml.builder [new file with mode: 0644]
app/views/api/messages/outbox.json.jbuilder [new file with mode: 0644]
app/views/api/messages/outbox.xml.builder [new file with mode: 0644]
app/views/api/messages/show.json.jbuilder [new file with mode: 0644]
app/views/api/messages/show.xml.builder [new file with mode: 0644]
config/locales/be.yml
config/locales/bg.yml
config/locales/ce.yml
config/locales/da.yml
config/locales/de.yml
config/locales/en.yml
config/locales/eo.yml
config/locales/es.yml
config/locales/he.yml
config/locales/mk.yml
config/locales/nl.yml
config/locales/zh-CN.yml
config/locales/zh-TW.yml
config/routes.rb
config/settings.yml
lib/oauth.rb
test/abilities/api_capability_test.rb
test/controllers/api/changeset_comments_controller_test.rb
test/controllers/api/messages_controller_test.rb [new file with mode: 0644]
test/controllers/api/user_preferences_controller_test.rb
test/factories/oauth_access_token.rb
yarn.lock

index 6fe5b2e57a222f91f042f6ee05ded916b027ee11..3b18f72460c1b652651d8a0f9a3e44972cc33778 100644 (file)
@@ -71,7 +71,7 @@ Metrics/ClassLength:
 # Offense count: 59
 # Configuration parameters: AllowedMethods, AllowedPatterns.
 Metrics/CyclomaticComplexity:
-  Max: 29
+  Max: 31
 
 # Offense count: 753
 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
@@ -86,7 +86,7 @@ Metrics/ParameterLists:
 # Offense count: 56
 # Configuration parameters: AllowedMethods, AllowedPatterns.
 Metrics/PerceivedComplexity:
-  Max: 30
+  Max: 32
 
 # Offense count: 2394
 # This cop supports safe autocorrection (--autocorrect).
@@ -95,7 +95,7 @@ Minitest/EmptyLineBeforeAssertionMethods:
 
 # Offense count: 565
 Minitest/MultipleAssertions:
-  Max: 54
+  Max: 60
 
 # Offense count: 1
 # This cop supports unsafe autocorrection (--autocorrect-all).
index 05ccd47d1f39fdde4c03cf59d7371385251f4c9d..e1337f6a46a3ad74b247617f1070f0c63a98c2a2 100644 (file)
@@ -95,8 +95,8 @@ GEM
     autoprefixer-rails (10.4.16.0)
       execjs (~> 2)
     aws-eventstream (1.3.0)
-    aws-partitions (1.957.0)
-    aws-sdk-core (3.201.2)
+    aws-partitions (1.959.0)
+    aws-sdk-core (3.201.3)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.651.0)
       aws-sigv4 (~> 1.8)
@@ -108,7 +108,7 @@ GEM
       aws-sdk-core (~> 3, >= 3.201.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
-    aws-sigv4 (1.8.0)
+    aws-sigv4 (1.9.1)
       aws-eventstream (~> 1, >= 1.0.2)
     base64 (0.2.0)
     better_errors (2.10.1)
@@ -177,7 +177,7 @@ GEM
     delayed_job_active_record (4.1.8)
       activerecord (>= 3.0, < 8.0)
       delayed_job (>= 3.0, < 5)
-    docile (1.4.0)
+    docile (1.4.1)
     doorkeeper (5.7.1)
       railties (>= 5)
     doorkeeper-i18n (5.2.7)
@@ -237,7 +237,7 @@ GEM
     faraday (2.10.0)
       faraday-net_http (>= 2.0, < 3.2)
       logger
-    faraday-net_http (3.1.0)
+    faraday-net_http (3.1.1)
       net-http
     ffi (1.17.0)
     ffi-compiler (1.3.2)
@@ -253,7 +253,7 @@ GEM
       ffi (>= 1.0.0)
     globalid (1.2.1)
       activesupport (>= 6.1)
-    google-protobuf (3.25.3)
+    google-protobuf (3.25.4)
     hashdiff (1.1.0)
     hashie (5.0.0)
     highline (3.1.0)
@@ -284,7 +284,7 @@ GEM
       image_optim (~> 0.24)
       railties
       sprockets
-    image_processing (1.12.2)
+    image_processing (1.13.0)
       mini_magick (>= 4.9.5, < 5)
       ruby-vips (>= 2.0.17, < 3)
     image_size (3.4.0)
@@ -352,7 +352,7 @@ GEM
     net-smtp (0.5.0)
       net-protocol
     nio4r (2.7.3)
-    nokogiri (1.16.6)
+    nokogiri (1.16.7)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
     oauth (0.4.7)
@@ -407,7 +407,7 @@ GEM
     parser (3.3.4.0)
       ast (~> 2.4.1)
       racc
-    pg (1.5.6)
+    pg (1.5.7)
     popper_js (2.11.8)
     progress (3.6.0)
     psych (5.1.2)
@@ -416,7 +416,7 @@ GEM
     puma (5.6.8)
       nio4r (~> 2.0)
     quad_tile (1.0.1)
-    racc (1.8.0)
+    racc (1.8.1)
     rack (2.2.9)
     rack-cors (2.0.2)
       rack (>= 2.0.0)
@@ -528,7 +528,7 @@ GEM
       ffi (~> 1.12)
       logger
     rubyzip (2.3.2)
-    sanitize (6.1.1)
+    sanitize (6.1.2)
       crass (~> 1.0.2)
       nokogiri (>= 1.12.0)
     sass-embedded (1.64.2)
@@ -597,7 +597,7 @@ GEM
     websocket-extensions (0.1.5)
     xpath (3.2.0)
       nokogiri (~> 1.8)
-    zeitwerk (2.6.16)
+    zeitwerk (2.6.17)
 
 PLATFORMS
   ruby
index f27dd2e63a515d0a0bf51571b6a49d6a5032c954..44e67634552599c861f9320209e8a7810a155edd 100644 (file)
@@ -19,6 +19,8 @@ class ApiCapability
         can [:gpx_files], User if scope?(token, :read_gpx)
         can [:index, :show], UserPreference if scope?(token, :read_prefs)
         can [:update, :update_all, :destroy], UserPreference if scope?(token, :write_prefs)
+        can [:inbox, :outbox, :show, :update, :destroy], Message if scope?(token, :consume_messages)
+        can [:create], Message if scope?(token, :send_messages)
 
         if user.terms_agreed?
           can [:create, :update, :upload, :close, :subscribe, :unsubscribe], Changeset if scope?(token, :write_api)
diff --git a/app/controllers/api/messages_controller.rb b/app/controllers/api/messages_controller.rb
new file mode 100644 (file)
index 0000000..074f873
--- /dev/null
@@ -0,0 +1,149 @@
+# The MessagesController is the RESTful interface to Message objects
+
+module Api
+  class MessagesController < ApiController
+    before_action :authorize
+
+    before_action :check_api_writable, :only => [:create, :update, :destroy]
+    before_action :check_api_readable, :except => [:create, :update, :destroy]
+
+    authorize_resource
+
+    around_action :api_call_handle_error, :api_call_timeout
+
+    before_action :set_request_formats
+
+    def inbox
+      @skip_body = true
+      @messages = Message.includes(:sender, :recipient).where(:to_user_id => current_user.id)
+
+      show_messages
+    end
+
+    def outbox
+      @skip_body = true
+      @messages = Message.includes(:sender, :recipient).where(:from_user_id => current_user.id)
+
+      show_messages
+    end
+
+    # Dump the details on a message given in params[:id]
+    def show
+      @message = Message.includes(:sender, :recipient).find(params[:id])
+
+      raise OSM::APIAccessDenied if current_user.id != @message.from_user_id && current_user.id != @message.to_user_id
+
+      # Render the result
+      respond_to do |format|
+        format.xml
+        format.json
+      end
+    end
+
+    # Create a new message from current user
+    def create
+      # Check the arguments are sane
+      raise OSM::APIBadUserInput, "No title was given" if params[:title].blank?
+      raise OSM::APIBadUserInput, "No body was given" if params[:body].blank?
+
+      # Extract the arguments
+      if params[:recipient_id]
+        recipient_id = params[:recipient_id].to_i
+        recipient = User.find(recipient_id)
+      elsif params[:recipient]
+        recipient_display_name = params[:recipient]
+        recipient = User.find_by(:display_name => recipient_display_name)
+      else
+        raise OSM::APIBadUserInput, "No recipient was given"
+      end
+
+      raise OSM::APIRateLimitExceeded if current_user.sent_messages.where(:sent_on => Time.now.utc - 1.hour..).count >= current_user.max_messages_per_hour
+
+      @message = Message.new(:sender => current_user,
+                             :recipient => recipient,
+                             :sent_on => Time.now.utc,
+                             :title => params[:title],
+                             :body => params[:body],
+                             :body_format => "markdown")
+      @message.save!
+
+      UserMailer.message_notification(@message).deliver_later if @message.notify_recipient?
+
+      # Return a copy of the new message
+      respond_to do |format|
+        format.xml { render :action => :show }
+        format.json { render :action => :show }
+      end
+    end
+
+    # Update read status of a message
+    def update
+      @message = Message.find(params[:id])
+      read_status_idx = %w[true false].index params[:read_status]
+
+      raise OSM::APIBadUserInput, "Invalid value of `read_status` was given" if read_status_idx.nil?
+      raise OSM::APIAccessDenied unless current_user.id == @message.to_user_id
+
+      @message.message_read = read_status_idx.zero?
+      @message.save!
+
+      # Return a copy of the message
+      respond_to do |format|
+        format.xml { render :action => :show }
+        format.json { render :action => :show }
+      end
+    end
+
+    # Delete message by marking it as not visible for the current user
+    def destroy
+      @message = Message.find(params[:id])
+      if current_user.id == @message.from_user_id
+        @message.from_user_visible = false
+      elsif current_user.id == @message.to_user_id
+        @message.to_user_visible = false
+      else
+        raise OSM::APIAccessDenied
+      end
+
+      @message.save!
+
+      # Return a copy of the message
+      respond_to do |format|
+        format.xml { render :action => :show }
+        format.json { render :action => :show }
+      end
+    end
+
+    private
+
+    def show_messages
+      @messages = @messages.where(:muted => false)
+      if params[:order].nil? || params[:order] == "newest"
+        @messages = @messages.where(:id => ..params[:from_id]) unless params[:from_id].nil?
+        @messages = @messages.order(:id => :desc)
+      elsif params[:order] == "oldest"
+        @messages = @messages.where(:id => params[:from_id]..) unless params[:from_id].nil?
+        @messages = @messages.order(:id => :asc)
+      else
+        raise OSM::APIBadUserInput, "Invalid order specified"
+      end
+
+      limit = params[:limit]
+      if !limit
+        limit = Settings.default_message_query_limit
+      elsif !limit.to_i.positive? || limit.to_i > Settings.max_message_query_limit
+        raise OSM::APIBadUserInput, "Messages limit must be between 1 and #{Settings.max_message_query_limit}"
+      else
+        limit = limit.to_i
+      end
+
+      @messages = @messages.limit(limit)
+
+      # Render the result
+      respond_to do |format|
+        format.xml
+        format.json
+      end
+    end
+  end
+end
diff --git a/app/views/api/messages/_message.json.jbuilder b/app/views/api/messages/_message.json.jbuilder
new file mode 100644 (file)
index 0000000..a04295d
--- /dev/null
@@ -0,0 +1,17 @@
+json.id message.id
+json.from_user_id message.from_user_id
+json.from_display_name message.sender.display_name
+json.to_user_id message.to_user_id
+json.to_display_name message.recipient.display_name
+json.title message.title
+json.sent_on message.sent_on.xmlschema
+
+if current_user.id == message.from_user_id
+  json.deleted !message.from_user_visible
+elsif current_user.id == message.to_user_id
+  json.message_read message.message_read
+  json.deleted !message.to_user_visible
+end
+
+json.body_format message.body_format
+json.body message.body unless @skip_body
diff --git a/app/views/api/messages/_message.xml.builder b/app/views/api/messages/_message.xml.builder
new file mode 100644 (file)
index 0000000..64ac9e3
--- /dev/null
@@ -0,0 +1,21 @@
+attrs = {
+  "id" => message.id,
+  "from_user_id" => message.from_user_id,
+  "from_display_name" => message.sender.display_name,
+  "to_user_id" => message.to_user_id,
+  "to_display_name" => message.recipient.display_name,
+  "sent_on" => message.sent_on.xmlschema,
+  "body_format" => message.body_format
+}
+
+if current_user.id == message.from_user_id
+  attrs["deleted"] = !message.from_user_visible
+elsif current_user.id == message.to_user_id
+  attrs["message_read"] = message.message_read
+  attrs["deleted"] = !message.to_user_visible
+end
+
+xml.message(attrs) do |nd|
+  nd.title(message.title)
+  nd.body(message.body) unless @skip_body
+end
diff --git a/app/views/api/messages/inbox.json.jbuilder b/app/views/api/messages/inbox.json.jbuilder
new file mode 100644 (file)
index 0000000..524006d
--- /dev/null
@@ -0,0 +1,5 @@
+json.partial! "api/root_attributes"
+
+json.messages(@messages) do |message|
+  json.partial! message
+end
diff --git a/app/views/api/messages/inbox.xml.builder b/app/views/api/messages/inbox.xml.builder
new file mode 100644 (file)
index 0000000..0ef9003
--- /dev/null
@@ -0,0 +1,7 @@
+xml.instruct!
+
+xml.osm(OSM::API.new.xml_root_attributes) do |osm|
+  xml.tag! "messages" do
+    osm << (render(@messages) || "")
+  end
+end
diff --git a/app/views/api/messages/outbox.json.jbuilder b/app/views/api/messages/outbox.json.jbuilder
new file mode 100644 (file)
index 0000000..524006d
--- /dev/null
@@ -0,0 +1,5 @@
+json.partial! "api/root_attributes"
+
+json.messages(@messages) do |message|
+  json.partial! message
+end
diff --git a/app/views/api/messages/outbox.xml.builder b/app/views/api/messages/outbox.xml.builder
new file mode 100644 (file)
index 0000000..440e342
--- /dev/null
@@ -0,0 +1,5 @@
+xml.instruct!
+
+xml.osm(OSM::API.new.xml_root_attributes) do |osm|
+  osm << (render(@messages) || "")
+end
diff --git a/app/views/api/messages/show.json.jbuilder b/app/views/api/messages/show.json.jbuilder
new file mode 100644 (file)
index 0000000..d8f24e9
--- /dev/null
@@ -0,0 +1,5 @@
+json.partial! "api/root_attributes"
+
+json.message do
+  json.partial! @message
+end
diff --git a/app/views/api/messages/show.xml.builder b/app/views/api/messages/show.xml.builder
new file mode 100644 (file)
index 0000000..008d592
--- /dev/null
@@ -0,0 +1,5 @@
+xml.instruct! :xml, :version => "1.0"
+
+xml.osm(OSM::API.new.xml_root_attributes) do |osm|
+  osm << render(@message)
+end
index 1318b77adff7ce014ee717b981fbb4d4dc2ccfa9..a6eeca216220b3dff11575770ae036d448b2243a 100644 (file)
@@ -699,6 +699,8 @@ be:
       comment: Каментар
       newer_comments: Навейшыя каментары
       older_comments: Старэйшыя каментары
+    new:
+      heading: Дадаць каментар да абмеркавання наступнага запісу ў дзённіку?
   doorkeeper:
     errors:
       messages:
@@ -2116,6 +2118,16 @@ be:
         contributors_intro_html: 'Нашымі ўдзельнікамі з''яўляюцца тысячы людзей. Мы
           таксама ўключаем дадзеныя ад нацыянальных картаграфічных агенцтваў, якія
           распаўсюджваюцца на ўмовах адкрытых ліцэнзій, сярод іх:'
+        contributors_at_austria: |2-
+           Пераключыць змест
+
+          Аўстрыя
+        contributors_au_australia: |2-
+           Пераключыць змест
+
+          Аўстралія
+        contributors_ca_canada: Канада
+        contributors_cz_czechia: Чэхія
         contributors_footer_2_html: |-
           Уключэнне дадзеных у OpenStreetMap не азначае, што пастаўшчыкі пачатковых дадзеных
           якім-небудзь чынам падтрымліваюць OpenStreetMap, прадстаўляюць гарантыі, ці
@@ -2527,6 +2539,7 @@ be:
   users:
     new:
       title: Зарэгістравацца
+      tab_title: Зарэгістравацца
       no_auto_account_create: На жаль, мы не можам стварыць для вас уліковы запіс
         аўтаматычна.
       about:
index ed9bab4477bbf680b87f061af5087a812757d1d4..6150a6c4fc766427252c4b1918732780c33d177c 100644 (file)
@@ -1566,8 +1566,23 @@ bg:
         mapping_link: картографирате
       legal_babble:
         credit_title_html: Как да кредитирате OpenStreetMap
+        credit_1_html: 'Когато използвате данни от OpenStreetMap, от вас се изисква
+          да направите следните две неща:'
+        credit_2_1: Предоставете кредит към OpenStreetMap, като покажете нашето уведомление
+          за авторски права.
+        credit_2_2: Ясно да посочите, че данните са достъпни съгласно Лиценза за отворени
+          бази данни (Open Database License).
+        credit_3_html: По отношение на известието за авторските права имаме различни
+          изисквания за начина, по който то трябва да се показва, в зависимост от
+          това как използвате нашите данни. В зависимост от това дали сте създали
+          карта с възможност за преглед, печатна карта или статично изображение, се
+          прилагат различни правила за начина на показване на съобщението за авторски
+          права. Пълна информация за изискванията може да бъде намерена в %{attribution_guidelines_link}.
         more_title_html: Открийте повече
         contributors_title_html: Нашите сътрудници
+        contributors_intro_html: 'Нашите сътрудници са хиляди хора. Ние включваме
+          и данни, които са отворено лицензирани от национални картографски агенции
+          и други източници, сред които:'
         infringement_title_html: Нарушаване на авторските права
         infringement_1_html: Напомняме на сътрудниците на OSM никога да не добавят
           данни от източници, защитени с авторски права (например Google Maps или
@@ -1788,6 +1803,7 @@ bg:
           степен като местните клонове. Всъщност много групи съществуват много успешно
           като неформално събиране на хора или като общностна група. Всеки може да
           ги създаде или да се присъедини към тях. Прочетете повече на %{communities_wiki_link}.
+        communities_wiki: уики страница Общности
   traces:
     new:
       upload_trace: Качване на следи от GPS
@@ -1848,8 +1864,10 @@ bg:
     index:
       public_traces: Публични следи от GPS
       public_traces_from: Публични следи от GPS от потребител %{user}
+      description: Преглед на скорошни качвания на GPS следи
       tagged_with: с етикет %{tags}
       upload_trace: Качване на следи от GPS
+      all_traces: Всички следи
       my_traces: Моите следи
     georss:
       title: OpenStreetMap GPS трасета
index bec7ea2c7ed0a96b7aa5d323253a814f31de14c8..aa84d23a985b2dbf59d608cc4e6843d227743f25 100644 (file)
@@ -2263,7 +2263,7 @@ ce:
         description: Йукъаралло латтош йу болалуш болчарна хьехам.
       community:
         title: ГӀо а, йукъараллин форум а
-        description: OpenStreetMap-ах лаьцна гӀо лаха а, къамелаш дан а юкъара меттиг.
+        description: OpenStreetMap-ах лаьцна гӀо лаха а, къамелаш дан а йукъара меттиг.
       mailing_lists:
         title: Почтан тептарш
         description: Хаттар ло йа дийцаре де оьшуш долу хаттарш,  шуьйрачу актуальни
@@ -2980,6 +2980,8 @@ ce:
       flash: Декъашхочунна %{name} тӀехь блок кхоьллина .
     update:
       only_creator_can_edit: И блок кхоьллинчу модераторан бен хийца йиш яц.
+      only_creator_or_revoker_can_edit: ХӀара блок кхоьллинчу йа йухайаьккхинчу модераторийн
+        бен хийца йиш йац.
       success: Блок карлайаккхина.
     index:
       title: Декъашхочун блоктохар
index defc1b43d737238bb8ebf5e51099a58b6b108b05..90c17a15584f0f3f6f371e583dd9fddfb8f941ae 100644 (file)
@@ -3022,6 +3022,8 @@ da:
     update:
       only_creator_can_edit: Kun moderatoren som oprettede denne blokering kan ændre
         den.
+      only_creator_or_revoker_can_edit: Kun de moderatorer, der har oprettet eller
+        ophævet denne blokering, kan redigere den.
       success: Blokering opdateret.
     index:
       title: Brugerblokeringer
index 163dc10316110d785ebab7c73df46c99e6b57f7a..06a217f6b6ce828d92bef52e2f1f40adb5fece00 100644 (file)
@@ -3179,6 +3179,8 @@ de:
     update:
       only_creator_can_edit: Nur der Moderator, der die Sperre eingerichtet hat, kann
         sie ändern.
+      only_creator_or_revoker_can_edit: Nur die Moderatoren, die die Sperre eingerichtet
+        haben, können sie ändern.
       success: Sperre aktualisiert.
     index:
       title: Benutzersperren
index dc7f1a1c0e76206f329668cac9f6363533210023..3f2e8a93b0d7bf78c346f7f0f255d51e1a09e2fd 100644 (file)
@@ -2641,6 +2641,8 @@ en:
       write_notes: Modify notes
       write_redactions: Redact map data
       read_email: Read user email address
+      consume_messages: Read, update status and delete user messages
+      send_messages: Send private messages to other users
       skip_authorization: Auto approve application
     for_roles:
       moderator: This permission is for actions available only to moderators
index 550a21ab4c7f3564debdfabff943d47395e48298..dc4748e155fbe47c710e2450b955084273d996ae 100644 (file)
@@ -2979,6 +2979,8 @@ eo:
     update:
       only_creator_can_edit: Nur la kontrolanto kiu kreis ĉi tiun blokadon, povas
         redakti ĝin.
+      only_creator_or_revoker_can_edit: Nur la kontrolantoj, kiuj kreis aŭ nuligis
+        tiun ĉi blokadon povas redakti ĝin.
       success: Blokado ĝisdatigita.
     index:
       title: Blokadoj de uzanto
@@ -3157,14 +3159,15 @@ eo:
       custom_dimensions: Agordi proprajn dimensiojn
       format: 'Dosiertipo:'
       scale: 'Skalo:'
-      image_dimensions: Bildo montros la norman tavolon je distingivo %{width}×%{height}
+      image_dimensions: Bildo montros la tavolon “%{layer}” je distingivo %{width}×%{height}
       download: Elŝuti
       short_url: Mallonga retadreso
       include_marker: Inkludi markon
       center_marker: Centrigi mapon al marko
       paste_html: Engluu HTML-kodon al via retpaĝo
       view_larger_map: Vidi pli grandan mapon
-      only_standard_layer: Nur la norma tavolo de mapo elporteblas kiel bildon
+      only_standard_layer: Nur la tavoloj Norma, Biciklada kaj Transporta estas elporteblaj
+        kiel bildoj
     embed:
       report_problem: Raporti problemon
     key:
index 33078e4cd8ba708b7e93331cc9997dfdb4eb6936..1d517adf567fd1aa36eb3f6d9ece580e77c10811 100644 (file)
@@ -28,6 +28,7 @@
 # Author: Dgstranz
 # Author: Egofer
 # Author: Ejegg
+# Author: EmicraftNoob
 # Author: Eulalio
 # Author: Fitoschido
 # Author: Fortega
 es:
   time:
     formats:
-      friendly: '%e de %B de %Y a las %H:%M'
+      friendly: '%e %B %Y a las %H:%M'
   helpers:
     file:
       prompt: Seleccionar archivo
@@ -3140,6 +3141,8 @@ es:
       flash: Has creado un bloqueo en el usuario %{name}.
     update:
       only_creator_can_edit: Sólo el moderador que ha creado este bloqueo puede editarlo.
+      only_creator_or_revoker_can_edit: Solo los moderadores que han creado o revocado
+        este bloqueo pueden editarlo.
       success: Bloqueo actualizado.
     index:
       title: Bloqueos de usuario
@@ -3321,14 +3324,15 @@ es:
       custom_dimensions: Establecer dimensiones personalizadas
       format: 'Formato:'
       scale: 'Escala:'
-      image_dimensions: La imagen mostrará la capa estándar en %{width} x %{height}
+      image_dimensions: La imagen mostrará la capa %{layer} en %{width} x %{height}
       download: Descargar
       short_url: URL corta
       include_marker: Incluir marcador
       center_marker: Centrar mapa en el marcador
       paste_html: Pegar código HTML para incrustar en el sitio web
       view_larger_map: Ver el mapa más grande
-      only_standard_layer: Sólo la capa estándar se puede exportar como una imagen
+      only_standard_layer: Sólo las capas Estándar, Mapa Ciclista y Transporte pueden
+        exportarse como una imagen
     embed:
       report_problem: Reportar un problema
     key:
index d2ae4aa4e00f70e10e1145419434968154c08014..bf5812cd3f7db495a913c6eb350a0782ca344078 100644 (file)
@@ -3008,6 +3008,8 @@ he:
       flash: נוצרה חסימה על חשבון %{name}
     update:
       only_creator_can_edit: רק המפקח שיצר את החסימה הזאת יכול לערוך אותה.
+      only_creator_or_revoker_can_edit: רק המפקחים שיצרו או ביטלו את החסימה הזאת יכולים
+        לערוך אותה.
       success: החסימה עודכנה.
     index:
       title: חסימות משתמש
index bfbb0b5c12ca59f38e9e8734964a56a3ec645f28..44b3e11018c35e5c4b66fddd71165f9c2d5e5f59 100644 (file)
@@ -2998,6 +2998,8 @@ mk:
       flash: Направен е блок на корисникот %{name}.
     update:
       only_creator_can_edit: Само модераторот кој го направил блоков може да го менува.
+      only_creator_or_revoker_can_edit: Овој блок можат да го менуваат само модератори
+        кои го создале или отповикале.
       success: Блокот е изменет.
     index:
       title: Кориснички блокови
index 4ba4701f1c521f4a6836b16e95d45d28d228cdd7..0a90b62e7e28df4ef56f98455be308b3cfdbc1c0 100644 (file)
@@ -462,9 +462,9 @@ nl:
         way: weg
         relation: relatie
     start_rjs:
-      feature_warning: Er worden %{num_features} objecten geladen, waardoor uw browser
-        traag kan worden of niet meer kan reageren. Weet u zeker dat u deze gegevens
-        wilt weergeven?
+      feature_warning: '%{num_features} functies worden geladen, waardoor uw browser
+        traag kan worden of niet reageert. Weet u zeker dat je deze gegevens wilt
+        weergeven?'
       load_data: Gegevens laden
       loading: Bezig met laden…
     tag_details:
@@ -694,6 +694,8 @@ nl:
       comment: Reactie
       newer_comments: Nieuwere reacties
       older_comments: Oudere reacties
+    new:
+      heading: Een reactie toevoegen aan de volgende dagboekaantekening?
   doorkeeper:
     errors:
       messages:
@@ -2138,7 +2140,7 @@ nl:
         credit_title_html: Hoe OpenStreetMap te vermelden
         credit_1_html: 'Wanneer u OpenStreetMap-gegevens gebruikt, bent u verplicht
           de volgende twee dingen te doen:'
-        credit_2_1: Vermeld OpenStreetMap door onze copyrightmelding te tonen.
+        credit_2_1: Vermeld OpenStreetMap door onze copyrightmelding weer te geven.
         credit_2_2: Maak duidelijk dat de data beschikbaar is onder de Open Database-licentie.
         credit_3_html: Voor de auteursrechtelijke vermelding hanteren wij verschillende
           voorschriften ten aanzien van de manier waarop deze moet worden weergegeven,
@@ -3274,14 +3276,15 @@ nl:
       custom_dimensions: Aangepaste afmetingen instellen
       format: 'Formaat:'
       scale: 'Schaal:'
-      image_dimensions: Afbeelding geeft standaardlaag weer op %{width} x %{height}
+      image_dimensions: Afbeelding geeft laag %{layer} weer op %{width} x %{height}
       download: Downloaden
       short_url: Korte URL
       include_marker: Marker opnemen
       center_marker: Kaart centreren op de marker
       paste_html: Kopieer de HTML-code en voeg deze toe aan uw website
       view_larger_map: Grotere kaart weergeven
-      only_standard_layer: Alleen de standaard laag kan worden geëxporteerd als afbeelding
+      only_standard_layer: Alleen de lagen Standaard, Fietskaart en Transportlagen
+        kunnen worden geëxporteerd als afbeelding
     embed:
       report_problem: Een probleem melden
     key:
@@ -3441,7 +3444,7 @@ nl:
       directions_from: Routebeschrijving vanaf hier
       directions_to: Routebeschrijving naar hier
       add_note: Hier een opmerking toevoegen
-      show_address: Adres tonen
+      show_address: Adres weergeven
       query_features: Kaartelementen opvragen
       centre_map: De kaart hier centreren
   redactions:
index c14f7687b2aed79361094dfc791985b5cb6d0820..7ff6dfe88d200af5b1d583043fdb77ee9a7c85ce 100644 (file)
@@ -2812,6 +2812,7 @@ zh-CN:
       flash: 已建立对用户 %{name} 的封禁
     update:
       only_creator_can_edit: 只有执行此封禁的仲裁员才能编辑。
+      only_creator_or_revoker_can_edit: 只有建立或撤销此封禁的仲裁员才能编辑。
       success: 封禁已更新。
     index:
       title: 用户的封禁
@@ -2981,14 +2982,14 @@ zh-CN:
       custom_dimensions: 设定自定义区域
       format: 格式:
       scale: 比例:
-      image_dimensions: 将以%{width} x %{height}的尺寸按标准图层输出图像
+      image_dimensions: 将以%{width} x %{height}的尺寸按%{layer}图层输出图像
       download: 下载
       short_url: 短URL
       include_marker: 包含标记
       center_marker: 以标记作为地图中心
       paste_html: 粘贴HTML以嵌入网站
       view_larger_map: 查看更大的地图
-      only_standard_layer: 只有标准图层可被导出为图片
+      only_standard_layer: 只有标准图层、自行车地图和交通运输图层可被导出为图片
     embed:
       report_problem: 报告问题
     key:
index ddaccf317d67f44b7c8c0ed334f3ec68df5fe13b..2d240c3faa5b4917d10cb467ca7da5f3fbeb88df 100644 (file)
@@ -2781,6 +2781,7 @@ zh-TW:
       flash: 已建立對使用者 %{name} 的封鎖。
     update:
       only_creator_can_edit: 只有建立這項封鎖的仲裁員可作出編輯。
+      only_creator_or_revoker_can_edit: 只有建立或撤銷此封鎖的仲裁員可作出編輯。
       success: 封鎖已更新。
     index:
       title: 使用者封鎖
@@ -2950,14 +2951,14 @@ zh-TW:
       custom_dimensions: 設定自訂的尺寸
       format: 格式:
       scale: 比例:
-      image_dimensions: 圖片會顯示成 %{width} x %{height} 標準圖層
+      image_dimensions: 圖片會顯示成 %{width} x %{height} %{layer}圖層
       download: 下載
       short_url: 簡短 URL
       include_marker: 包括標記
       center_marker: 將標記設為地圖中心點
       paste_html: 貼上 HTML 以嵌入網站
       view_larger_map: 查看更大的地圖
-      only_standard_layer: 只有標準圖層能匯出成圖片
+      only_standard_layer: 只有標準圖層、自行車地圖、交通運輸圖層能匯出成圖片
     embed:
       report_problem: 回報問題
     key:
index c832cbb35866fcbfb0f22073e56f8ef5b05fa7b3..650818d6fc8f45cf0b1839f488ffdbc2cda0f2ba 100644 (file)
@@ -78,6 +78,15 @@ OpenStreetMap::Application.routes.draw do
       end
     end
 
+    resources :messages, :path => "user/messages", :constraints => { :id => /\d+/ }, :only => [:create, :show, :destroy], :controller => "messages", :as => :api_messages do
+      collection do
+        get "inbox"
+        get "outbox"
+      end
+    end
+
+    post "/user/messages/:id" => "messages#update", :as => :api_message_update
+
     post "gpx/create" => "traces#create"
     get "gpx/:id" => "traces#show", :as => :api_trace, :id => /\d+/
     put "gpx/:id" => "traces#update", :id => /\d+/
index fa7207721c9dd28db5ce5c35281f60e7a20f3227..71df9ad3d7ef560eb732bbcf2467351b349209dd 100644 (file)
@@ -59,6 +59,10 @@ user_block_periods: [0, 1, 3, 6, 12, 24, 48, 96, 168, 336, 731, 4383, 8766, 8766
 user_account_deletion_delay: null
 # Rate limit for message sending
 max_messages_per_hour: 60
+# Default limit on the number of messages returned by inbox and outbox message api
+default_message_query_limit: 100
+# Maximum number of messages returned by inbox and outbox message api
+max_message_query_limit: 100
 # Rate limit for friending
 max_friends_per_hour: 60
 # Rate limit for changeset comments
index 88db38eb4bf914b8331655e134cbdf70b243c4ee..a8f49762112a739ccb351ff130dd71bc678ba6e5 100644 (file)
@@ -2,7 +2,7 @@ module Oauth
   SCOPES = %w[read_prefs write_prefs write_diary write_api read_gpx write_gpx write_notes].freeze
   PRIVILEGED_SCOPES = %w[read_email skip_authorization].freeze
   MODERATOR_SCOPES = %w[write_redactions].freeze
-  OAUTH2_SCOPES = %w[write_redactions openid].freeze
+  OAUTH2_SCOPES = %w[write_redactions consume_messages send_messages openid].freeze
 
   class Scope
     attr_reader :name
index 10419c0f814b2e8b240cb80b3263b2f85857a7dd..bcfcaf74e4d809ad0a8aba72af5c0837f8af062d 100644 (file)
@@ -4,7 +4,7 @@ require "test_helper"
 
 class ChangesetCommentApiCapabilityTest < ActiveSupport::TestCase
   test "as a normal user with permissionless token" do
-    token = create(:access_token)
+    token = create(:oauth_access_token)
     capability = ApiCapability.new token
 
     [:create, :destroy, :restore].each do |action|
@@ -12,8 +12,8 @@ class ChangesetCommentApiCapabilityTest < ActiveSupport::TestCase
     end
   end
 
-  test "as a normal user with allow_write_api token" do
-    token = create(:access_token, :allow_write_api => true)
+  test "as a normal user with write_api token" do
+    token = create(:oauth_access_token, :scopes => %w[write_api])
     capability = ApiCapability.new token
 
     [:destroy, :restore].each do |action|
@@ -26,7 +26,7 @@ class ChangesetCommentApiCapabilityTest < ActiveSupport::TestCase
   end
 
   test "as a moderator with permissionless token" do
-    token = create(:access_token, :user => create(:moderator_user))
+    token = create(:oauth_access_token, :resource_owner_id => create(:moderator_user).id)
     capability = ApiCapability.new token
 
     [:create, :destroy, :restore].each do |action|
@@ -34,8 +34,8 @@ class ChangesetCommentApiCapabilityTest < ActiveSupport::TestCase
     end
   end
 
-  test "as a moderator with allow_write_api token" do
-    token = create(:access_token, :user => create(:moderator_user), :allow_write_api => true)
+  test "as a moderator with write_api token" do
+    token = create(:oauth_access_token, :resource_owner_id => create(:moderator_user).id, :scopes => %w[write_api])
     capability = ApiCapability.new token
 
     [:create, :destroy, :restore].each do |action|
@@ -46,7 +46,7 @@ end
 
 class NoteApiCapabilityTest < ActiveSupport::TestCase
   test "as a normal user with permissionless token" do
-    token = create(:access_token)
+    token = create(:oauth_access_token)
     capability = ApiCapability.new token
 
     [:create, :comment, :close, :reopen, :destroy].each do |action|
@@ -54,8 +54,8 @@ class NoteApiCapabilityTest < ActiveSupport::TestCase
     end
   end
 
-  test "as a normal user with allow_write_notes token" do
-    token = create(:access_token, :allow_write_notes => true)
+  test "as a normal user with write_notes token" do
+    token = create(:oauth_access_token, :scopes => %w[write_notes])
     capability = ApiCapability.new token
 
     [:destroy].each do |action|
@@ -68,7 +68,7 @@ class NoteApiCapabilityTest < ActiveSupport::TestCase
   end
 
   test "as a moderator with permissionless token" do
-    token = create(:access_token, :user => create(:moderator_user))
+    token = create(:oauth_access_token, :resource_owner_id => create(:moderator_user).id)
     capability = ApiCapability.new token
 
     [:destroy].each do |action|
@@ -76,8 +76,8 @@ class NoteApiCapabilityTest < ActiveSupport::TestCase
     end
   end
 
-  test "as a moderator with allow_write_notes token" do
-    token = create(:access_token, :user => create(:moderator_user), :allow_write_notes => true)
+  test "as a moderator with write_notes token" do
+    token = create(:oauth_access_token, :resource_owner_id => create(:moderator_user).id, :scopes => %w[write_notes])
     capability = ApiCapability.new token
 
     [:destroy].each do |action|
@@ -95,14 +95,14 @@ class UserApiCapabilityTest < ActiveSupport::TestCase
     end
 
     # A user with empty tokens
-    token = create(:access_token)
+    token = create(:oauth_access_token)
     capability = ApiCapability.new token
 
     [:index, :show, :update_all, :update, :destroy].each do |act|
       assert capability.cannot? act, UserPreference
     end
 
-    token = create(:access_token, :allow_read_prefs => true)
+    token = create(:oauth_access_token, :scopes => %w[read_prefs])
     capability = ApiCapability.new token
 
     [:update_all, :update, :destroy].each do |act|
@@ -113,7 +113,7 @@ class UserApiCapabilityTest < ActiveSupport::TestCase
       assert capability.can? act, UserPreference
     end
 
-    token = create(:access_token, :allow_write_prefs => true)
+    token = create(:oauth_access_token, :scopes => %w[write_prefs])
     capability = ApiCapability.new token
 
     [:index, :show].each do |act|
index d3b171588b71cd8d86a2b753624a7a8dff7c77e0..f479b24b3cb99b3b9f68921fd772086f69bd4103 100644 (file)
@@ -303,11 +303,11 @@ module Api
     # But writing oauth tests is hard, and so it's easier to put in a controller test.)
     def test_api_write_and_terms_agreed_via_token
       user = create(:user, :terms_agreed => nil)
-      token = create(:access_token, :user => user, :allow_write_api => true)
+      token = create(:oauth_access_token, :resource_owner_id => user.id, :scopes => %w[write_api])
       changeset = create(:changeset, :closed)
 
       assert_difference "ChangesetComment.count", 0 do
-        signed_post changeset_comment_path(changeset), :params => { :text => "This is a comment" }, :oauth => { :token => token }
+        post changeset_comment_path(changeset), :params => { :text => "This is a comment" }, :headers => bearer_authorization_header(token.token)
       end
       assert_response :forbidden
 
@@ -316,7 +316,7 @@ module Api
       user.save!
 
       assert_difference "ChangesetComment.count", 1 do
-        signed_post changeset_comment_path(changeset), :params => { :text => "This is a comment" }, :oauth => { :token => token }
+        post changeset_comment_path(changeset), :params => { :text => "This is a comment" }, :headers => bearer_authorization_header(token.token)
       end
       assert_response :success
     end
diff --git a/test/controllers/api/messages_controller_test.rb b/test/controllers/api/messages_controller_test.rb
new file mode 100644 (file)
index 0000000..0b54be4
--- /dev/null
@@ -0,0 +1,611 @@
+require "test_helper"
+
+module Api
+  class MessagesControllerTest < ActionDispatch::IntegrationTest
+    ##
+    # test all routes which lead to this controller
+    def test_routes
+      assert_routing(
+        { :path => "/api/0.6/user/messages/inbox", :method => :get },
+        { :controller => "api/messages", :action => "inbox" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/user/messages/inbox.xml", :method => :get },
+        { :controller => "api/messages", :action => "inbox", :format => "xml" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/user/messages/inbox.json", :method => :get },
+        { :controller => "api/messages", :action => "inbox", :format => "json" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/user/messages/outbox", :method => :get },
+        { :controller => "api/messages", :action => "outbox" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/user/messages/outbox.xml", :method => :get },
+        { :controller => "api/messages", :action => "outbox", :format => "xml" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/user/messages/outbox.json", :method => :get },
+        { :controller => "api/messages", :action => "outbox", :format => "json" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/user/messages/1", :method => :get },
+        { :controller => "api/messages", :action => "show", :id => "1" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/user/messages/1.xml", :method => :get },
+        { :controller => "api/messages", :action => "show", :id => "1", :format => "xml" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/user/messages/1.json", :method => :get },
+        { :controller => "api/messages", :action => "show", :id => "1", :format => "json" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/user/messages", :method => :post },
+        { :controller => "api/messages", :action => "create" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/user/messages/1", :method => :post },
+        { :controller => "api/messages", :action => "update", :id => "1" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/user/messages/1", :method => :delete },
+        { :controller => "api/messages", :action => "destroy", :id => "1" }
+      )
+    end
+
+    def test_create_success
+      recipient = create(:user)
+      sender = create(:user)
+
+      sender_token = create(:oauth_access_token,
+                            :resource_owner_id => sender.id,
+                            :scopes => %w[send_messages consume_messages])
+      sender_auth = bearer_authorization_header(sender_token.token)
+
+      msg = build(:message)
+
+      assert_difference "Message.count", 1 do
+        assert_difference "ActionMailer::Base.deliveries.size", 1 do
+          perform_enqueued_jobs do
+            post api_messages_path,
+                 :params => { :title => msg.title,
+                              :recipient_id => recipient.id,
+                              :body => msg.body,
+                              :format => "json" },
+                 :headers => sender_auth
+            assert_response :success
+          end
+        end
+      end
+
+      assert_equal "application/json", response.media_type
+      js = ActiveSupport::JSON.decode(@response.body)
+      jsm = js["message"]
+      assert_not_nil jsm
+      assert_not_nil jsm["id"]
+      assert_equal sender.id, jsm["from_user_id"]
+      assert_equal sender.display_name, jsm["from_display_name"]
+      assert_equal recipient.id, jsm["to_user_id"]
+      assert_equal recipient.display_name, jsm["to_display_name"]
+      assert_equal msg.title, jsm["title"]
+      assert_not_nil jsm["sent_on"]
+      assert_equal !msg.from_user_visible, jsm["deleted"]
+      assert_not jsm.key?("message_read")
+      assert_equal "markdown", jsm["body_format"]
+      assert_equal msg.body, jsm["body"]
+    end
+
+    def test_create_fail
+      recipient = create(:user)
+
+      sender = create(:user)
+      sender_token = create(:oauth_access_token,
+                            :resource_owner_id => sender.id,
+                            :scopes => %w[send_messages consume_messages])
+      sender_auth = bearer_authorization_header(sender_token.token)
+
+      assert_no_difference "Message.count" do
+        assert_no_difference "ActionMailer::Base.deliveries.size" do
+          perform_enqueued_jobs do
+            post api_messages_path,
+                 :params => { :title => "Title",
+                              :recipient_id => recipient.id,
+                              :body => "body" }
+          end
+        end
+      end
+      assert_response :unauthorized
+
+      assert_no_difference "Message.count" do
+        assert_no_difference "ActionMailer::Base.deliveries.size" do
+          perform_enqueued_jobs do
+            post api_messages_path,
+                 :params => { :recipient_id => recipient.id,
+                              :body => "body" },
+                 :headers => sender_auth
+          end
+        end
+      end
+      assert_response :bad_request
+
+      assert_no_difference "Message.count" do
+        assert_no_difference "ActionMailer::Base.deliveries.size" do
+          perform_enqueued_jobs do
+            post api_messages_path,
+                 :params => { :title => "Title",
+                              :body => "body" },
+                 :headers => sender_auth
+          end
+        end
+      end
+      assert_response :bad_request
+
+      assert_no_difference "Message.count" do
+        assert_no_difference "ActionMailer::Base.deliveries.size" do
+          perform_enqueued_jobs do
+            post api_messages_path,
+                 :params => { :title => "Title",
+                              :recipient_id => recipient.id },
+                 :headers => sender_auth
+          end
+        end
+      end
+      assert_response :bad_request
+    end
+
+    def test_show
+      recipient = create(:user)
+      sender = create(:user)
+      user3 = create(:user)
+
+      sender_token = create(:oauth_access_token,
+                            :resource_owner_id => sender.id,
+                            :scopes => %w[consume_messages])
+      sender_auth = bearer_authorization_header(sender_token.token)
+
+      recipient_token = create(:oauth_access_token,
+                               :resource_owner_id => recipient.id,
+                               :scopes => %w[consume_messages])
+      recipient_auth = bearer_authorization_header(recipient_token.token)
+
+      user3_token = create(:oauth_access_token,
+                           :resource_owner_id => user3.id,
+                           :scopes => %w[send_messages consume_messages])
+      user3_auth = bearer_authorization_header(user3_token.token)
+
+      msg = create(:message, :unread, :sender => sender, :recipient => recipient)
+
+      # fail if not authorized
+      get api_message_path(:id => msg.id)
+      assert_response :unauthorized
+
+      # only recipient and sender can read the message
+      get api_message_path(:id => msg.id), :headers => user3_auth
+      assert_response :forbidden
+
+      # message does not exist
+      get api_message_path(:id => 99999), :headers => user3_auth
+      assert_response :not_found
+
+      # verify xml output
+      get api_message_path(:id => msg.id), :headers => recipient_auth
+      assert_equal "application/xml", response.media_type
+      assert_select "message", :count => 1 do
+        assert_select "[id='#{msg.id}']"
+        assert_select "[from_user_id='#{sender.id}']"
+        assert_select "[from_display_name='#{sender.display_name}']"
+        assert_select "[to_user_id='#{recipient.id}']"
+        assert_select "[to_display_name='#{recipient.display_name}']"
+        assert_select "[sent_on]"
+        assert_select "[deleted='#{!msg.to_user_visible}']"
+        assert_select "[message_read='#{msg.message_read}']"
+        assert_select "[body_format='markdown']"
+        assert_select "title", msg.title
+        assert_select "body", msg.body
+      end
+
+      # verify json output
+      get api_message_path(:id => msg.id, :format => "json"), :headers => recipient_auth
+      assert_equal "application/json", response.media_type
+      js = ActiveSupport::JSON.decode(@response.body)
+      jsm = js["message"]
+      assert_not_nil jsm
+      assert_equal msg.id, jsm["id"]
+      assert_equal sender.id, jsm["from_user_id"]
+      assert_equal sender.display_name, jsm["from_display_name"]
+      assert_equal recipient.id, jsm["to_user_id"]
+      assert_equal recipient.display_name, jsm["to_display_name"]
+      assert_equal msg.title, jsm["title"]
+      assert_not_nil jsm["sent_on"]
+      assert_equal msg.message_read, jsm["message_read"]
+      assert_equal !msg.to_user_visible, jsm["deleted"]
+      assert_equal "markdown", jsm["body_format"]
+      assert_equal msg.body, jsm["body"]
+
+      get api_message_path(:id => msg.id), :headers => sender_auth
+      assert_equal "application/xml", response.media_type
+      assert_select "message", :count => 1 do
+        assert_select "[id='#{msg.id}']"
+        assert_select "[from_user_id='#{sender.id}']"
+        assert_select "[from_display_name='#{sender.display_name}']"
+        assert_select "[to_user_id='#{recipient.id}']"
+        assert_select "[to_display_name='#{recipient.display_name}']"
+        assert_select "[sent_on]"
+        assert_select "[deleted='#{!msg.from_user_visible}']"
+        assert_select "[message_read='#{msg.message_read}']", 0
+        assert_select "[body_format='markdown']"
+        assert_select "title", msg.title
+        assert_select "body", msg.body
+      end
+
+      # verify json output
+      get api_message_path(:id => msg.id, :format => "json"), :headers => sender_auth
+      assert_equal "application/json", response.media_type
+      js = ActiveSupport::JSON.decode(@response.body)
+      jsm = js["message"]
+      assert_not_nil jsm
+      assert_equal msg.id, jsm["id"]
+      assert_equal sender.id, jsm["from_user_id"]
+      assert_equal sender.display_name, jsm["from_display_name"]
+      assert_equal recipient.id, jsm["to_user_id"]
+      assert_equal recipient.display_name, jsm["to_display_name"]
+      assert_equal msg.title, jsm["title"]
+      assert_not_nil jsm["sent_on"]
+      assert_equal !msg.from_user_visible, jsm["deleted"]
+      assert_not jsm.key?("message_read")
+      assert_equal "markdown", jsm["body_format"]
+      assert_equal msg.body, jsm["body"]
+    end
+
+    def test_update_status
+      recipient = create(:user)
+      sender = create(:user)
+      user3 = create(:user)
+
+      recipient_token = create(:oauth_access_token,
+                               :resource_owner_id => recipient.id,
+                               :scopes => %w[consume_messages])
+      recipient_auth = bearer_authorization_header(recipient_token.token)
+
+      user3_token = create(:oauth_access_token,
+                           :resource_owner_id => user3.id,
+                           :scopes => %w[send_messages consume_messages])
+      user3_auth = bearer_authorization_header(user3_token.token)
+
+      msg = create(:message, :unread, :sender => sender, :recipient => recipient)
+
+      # attempt to mark message as read by recipient, not authenticated
+      post api_message_path(:id => msg.id), :params => { :read_status => true }
+      assert_response :unauthorized
+
+      # attempt to mark message as read by recipient, not allowed
+      post api_message_path(:id => msg.id), :params => { :read_status => true }, :headers => user3_auth
+      assert_response :forbidden
+
+      # missing parameter
+      post api_message_path(:id => msg.id), :headers => recipient_auth
+      assert_response :bad_request
+
+      # wrong type of parameter
+      post api_message_path(:id => msg.id),
+           :params => { :read_status => "not a boolean" },
+           :headers => recipient_auth
+      assert_response :bad_request
+
+      # mark message as read by recipient
+      post api_message_path(:id => msg.id, :format => "json"),
+           :params => { :read_status => true },
+           :headers => recipient_auth
+      assert_response :success
+      assert_equal "application/json", response.media_type
+      js = ActiveSupport::JSON.decode(@response.body)
+      jsm = js["message"]
+      assert_not_nil jsm
+      assert_equal msg.id, jsm["id"]
+      assert_equal sender.id, jsm["from_user_id"]
+      assert_equal sender.display_name, jsm["from_display_name"]
+      assert_equal recipient.id, jsm["to_user_id"]
+      assert_equal recipient.display_name, jsm["to_display_name"]
+      assert_equal msg.title, jsm["title"]
+      assert_not_nil jsm["sent_on"]
+      assert jsm["message_read"]
+      assert_equal !msg.to_user_visible, jsm["deleted"]
+      assert_equal "markdown", jsm["body_format"]
+      assert_equal msg.body, jsm["body"]
+
+      # mark message as unread by recipient
+      post api_message_path(:id => msg.id, :format => "json"),
+           :params => { :read_status => false },
+           :headers => recipient_auth
+      assert_response :success
+      assert_equal "application/json", response.media_type
+      js = ActiveSupport::JSON.decode(@response.body)
+      jsm = js["message"]
+      assert_not_nil jsm
+      assert_equal msg.id, jsm["id"]
+      assert_equal sender.id, jsm["from_user_id"]
+      assert_equal sender.display_name, jsm["from_display_name"]
+      assert_equal recipient.id, jsm["to_user_id"]
+      assert_equal recipient.display_name, jsm["to_display_name"]
+      assert_equal msg.title, jsm["title"]
+      assert_not_nil jsm["sent_on"]
+      assert_not jsm["message_read"]
+      assert_equal !msg.to_user_visible, jsm["deleted"]
+      assert_equal "markdown", jsm["body_format"]
+      assert_equal msg.body, jsm["body"]
+    end
+
+    def test_delete
+      recipient = create(:user)
+      recipient_token = create(:oauth_access_token,
+                               :resource_owner_id => recipient.id,
+                               :scopes => %w[consume_messages])
+      recipient_auth = bearer_authorization_header(recipient_token.token)
+
+      sender = create(:user)
+      sender_token = create(:oauth_access_token,
+                            :resource_owner_id => sender.id,
+                            :scopes => %w[send_messages consume_messages])
+      sender_auth = bearer_authorization_header(sender_token.token)
+
+      user3 = create(:user)
+      user3_token = create(:oauth_access_token,
+                           :resource_owner_id => user3.id,
+                           :scopes => %w[send_messages consume_messages])
+      user3_auth = bearer_authorization_header(user3_token.token)
+
+      msg = create(:message, :read, :sender => sender, :recipient => recipient)
+
+      # attempt to delete message, not authenticated
+      delete api_message_path(:id => msg.id)
+      assert_response :unauthorized
+
+      # attempt to delete message, by user3
+      delete api_message_path(:id => msg.id), :headers => user3_auth
+      assert_response :forbidden
+
+      # delete message by recipient
+      delete api_message_path(:id => msg.id, :format => "json"), :headers => recipient_auth
+      assert_response :success
+      assert_equal "application/json", response.media_type
+      js = ActiveSupport::JSON.decode(@response.body)
+      jsm = js["message"]
+      assert_not_nil jsm
+      assert_equal msg.id, jsm["id"]
+      assert_equal sender.id, jsm["from_user_id"]
+      assert_equal sender.display_name, jsm["from_display_name"]
+      assert_equal recipient.id, jsm["to_user_id"]
+      assert_equal recipient.display_name, jsm["to_display_name"]
+      assert_equal msg.title, jsm["title"]
+      assert_not_nil jsm["sent_on"]
+      assert_equal msg.message_read, jsm["message_read"]
+      assert jsm["deleted"]
+      assert_equal "markdown", jsm["body_format"]
+      assert_equal msg.body, jsm["body"]
+
+      # delete message by sender
+      delete api_message_path(:id => msg.id, :format => "json"), :headers => sender_auth
+      assert_response :success
+      assert_equal "application/json", response.media_type
+      js = ActiveSupport::JSON.decode(@response.body)
+      jsm = js["message"]
+      assert_not_nil jsm
+      assert_equal msg.id, jsm["id"]
+      assert_equal sender.id, jsm["from_user_id"]
+      assert_equal sender.display_name, jsm["from_display_name"]
+      assert_equal recipient.id, jsm["to_user_id"]
+      assert_equal recipient.display_name, jsm["to_display_name"]
+      assert_equal msg.title, jsm["title"]
+      assert_not_nil jsm["sent_on"]
+      assert jsm["deleted"]
+      assert_not jsm.key?("message_read")
+      assert_equal "markdown", jsm["body_format"]
+      assert_equal msg.body, jsm["body"]
+    end
+
+    def test_list_messages
+      user1 = create(:user)
+      user1_token = create(:oauth_access_token,
+                           :resource_owner_id => user1.id,
+                           :scopes => %w[send_messages consume_messages])
+      user1_auth = bearer_authorization_header(user1_token.token)
+
+      user2 = create(:user)
+      user2_token = create(:oauth_access_token,
+                           :resource_owner_id => user2.id,
+                           :scopes => %w[send_messages consume_messages])
+      user2_auth = bearer_authorization_header(user2_token.token)
+
+      user3 = create(:user)
+      user3_token = create(:oauth_access_token,
+                           :resource_owner_id => user3.id,
+                           :scopes => %w[send_messages consume_messages])
+      user3_auth = bearer_authorization_header(user3_token.token)
+
+      # create some messages between users
+      # user | inbox | outbox
+      #   1  |   0   |   3
+      #   2  |   2   |   1
+      #   3  |   2   |   0
+      create(:message, :unread, :sender => user1, :recipient => user2)
+      create(:message, :unread, :sender => user1, :recipient => user2)
+      create(:message, :unread, :sender => user1, :recipient => user3)
+      create(:message, :unread, :sender => user2, :recipient => user3)
+
+      # only authorized users
+      get inbox_api_messages_path
+      assert_response :unauthorized
+      get outbox_api_messages_path
+      assert_response :unauthorized
+
+      # no messages in user1.inbox
+      get inbox_api_messages_path, :headers => user1_auth
+      assert_response :success
+      assert_equal "application/xml", response.media_type
+      assert_select "message", :count => 0
+
+      # 3 messages in user1.outbox
+      get outbox_api_messages_path, :headers => user1_auth
+      assert_response :success
+      assert_equal "application/xml", response.media_type
+      assert_select "message", :count => 3 do
+        assert_select "[from_user_id='#{user1.id}']"
+        assert_select "[from_display_name='#{user1.display_name}']"
+        assert_select "[to_user_id]"
+        assert_select "[to_display_name]"
+        assert_select "[sent_on]"
+        assert_select "[message_read]", 0
+        assert_select "[deleted='false']"
+        assert_select "[body_format]"
+        assert_select "body", false
+        assert_select "title"
+      end
+
+      # 2 messages in user2.inbox
+      get inbox_api_messages_path, :headers => user2_auth
+      assert_response :success
+      assert_equal "application/xml", response.media_type
+      assert_select "message", :count => 2 do
+        assert_select "[from_user_id]"
+        assert_select "[from_display_name]"
+        assert_select "[to_user_id='#{user2.id}']"
+        assert_select "[to_display_name='#{user2.display_name}']"
+        assert_select "[sent_on]"
+        assert_select "[message_read='false']"
+        assert_select "[deleted='false']"
+        assert_select "[body_format]"
+        assert_select "body", false
+        assert_select "title"
+      end
+
+      # 1 message in user2.outbox
+      get outbox_api_messages_path, :headers => user2_auth
+      assert_response :success
+      assert_equal "application/xml", response.media_type
+      assert_select "message", :count => 1 do
+        assert_select "[from_user_id='#{user2.id}']"
+        assert_select "[from_display_name='#{user2.display_name}']"
+        assert_select "[to_user_id]"
+        assert_select "[to_display_name]"
+        assert_select "[sent_on]"
+        assert_select "[deleted='false']"
+        assert_select "[message_read]", 0
+        assert_select "[body_format]"
+        assert_select "body", false
+        assert_select "title"
+      end
+
+      # 2 messages in user3.inbox
+      get inbox_api_messages_path, :headers => user3_auth
+      assert_response :success
+      assert_equal "application/xml", response.media_type
+      assert_select "message", :count => 2 do
+        assert_select "[from_user_id]"
+        assert_select "[from_display_name]"
+        assert_select "[to_user_id='#{user3.id}']"
+        assert_select "[to_display_name='#{user3.display_name}']"
+        assert_select "[sent_on]"
+        assert_select "[message_read='false']"
+        assert_select "[deleted='false']"
+        assert_select "[body_format]"
+        assert_select "body", false
+        assert_select "title"
+      end
+
+      # 0 messages in user3.outbox
+      get outbox_api_messages_path, :headers => user3_auth
+      assert_response :success
+      assert_equal "application/xml", response.media_type
+      assert_select "message", :count => 0
+    end
+
+    def test_paged_list_messages_asc
+      recipient = create(:user)
+      recipient_token = create(:oauth_access_token,
+                               :resource_owner_id => recipient.id,
+                               :scopes => %w[consume_messages])
+      recipient_auth = bearer_authorization_header(recipient_token.token)
+
+      sender = create(:user)
+
+      create_list(:message, 100, :unread, :sender => sender, :recipient => recipient)
+
+      msgs_read = {}
+      params = { :order => "oldest", :limit => 20 }
+      10.times do
+        get inbox_api_messages_path(:format => "json"),
+            :params => params,
+            :headers => recipient_auth
+        assert_response :success
+        assert_equal "application/json", response.media_type
+        js = ActiveSupport::JSON.decode(@response.body)
+        jsm = js["messages"]
+        assert_operator jsm.count, :<=, 20
+
+        break if jsm.nil? || jsm.count.zero?
+
+        assert_operator(jsm[0]["id"], :>=, params[:from_id]) unless params[:from_id].nil?
+        # ensure ascending order
+        (0..jsm.count - 1).each do |i|
+          assert_operator(jsm[i]["id"], :<, jsm[i + 1]["id"]) unless i == jsm.count - 1
+          msgs_read[jsm[i]["id"]] = jsm[i]
+        end
+        params[:from_id] = jsm[jsm.count - 1]["id"]
+      end
+      assert_equal 100, msgs_read.count
+    end
+
+    def test_paged_list_messages_desc
+      recipient = create(:user)
+      recipient_token = create(:oauth_access_token,
+                               :resource_owner_id => recipient.id,
+                               :scopes => %w[consume_messages])
+      recipient_auth = bearer_authorization_header(recipient_token.token)
+
+      sender = create(:user)
+
+      create_list(:message, 100, :unread, :sender => sender, :recipient => recipient)
+
+      real_max_id = -1
+      msgs_read = {}
+      params = { :order => "newest", :limit => 20 }
+      10.times do
+        get inbox_api_messages_path(:format => "json"),
+            :params => params,
+            :headers => recipient_auth
+        assert_response :success
+        assert_equal "application/json", response.media_type
+        js = ActiveSupport::JSON.decode(@response.body)
+        jsm = js["messages"]
+        assert_operator jsm.count, :<=, 20
+
+        break if jsm.nil? || jsm.count.zero?
+
+        if params[:from_id].nil?
+          real_max_id = jsm[0]["id"]
+        else
+          assert_operator jsm[0]["id"], :<=, params[:from_id]
+        end
+        # ensure descending order
+        (0..jsm.count - 1).each do |i|
+          assert_operator(jsm[i]["id"], :>, jsm[i + 1]["id"]) unless i == jsm.count - 1
+          msgs_read[jsm[i]["id"]] = jsm[i]
+        end
+        params[:from_id] = jsm[jsm.count - 1]["id"]
+      end
+      assert_equal 100, msgs_read.count
+      assert_not_equal(-1, real_max_id)
+
+      # invoke without min_id/max_id parameters, verify that we get the last batch
+      get inbox_api_messages_path(:format => "json"), :params => { :limit => 20 }, :headers => recipient_auth
+      assert_response :success
+      assert_equal "application/json", response.media_type
+      js = ActiveSupport::JSON.decode(@response.body)
+      jsm = js["messages"]
+      assert_not_nil jsm
+      assert_equal real_max_id, jsm[0]["id"]
+    end
+  end
+end
index 4e96d4ce9146a8d6227c16280980e659d258987f..41406e1b3d22413b50e07850dadf3ac91f29aacc 100644 (file)
@@ -252,10 +252,10 @@ module Api
     # read preferences
     def test_show_using_token
       user = create(:user)
-      token = create(:access_token, :user => user, :allow_read_prefs => true)
+      token = create(:oauth_access_token, :resource_owner_id => user.id, :scopes => %w[read_prefs])
       create(:user_preference, :user => user, :k => "key", :v => "value")
 
-      signed_get user_preference_path(:preference_key => "key"), :oauth => { :token => token }
+      get user_preference_path(:preference_key => "key"), :headers => bearer_authorization_header(token.token)
       assert_response :success
     end
 
@@ -264,10 +264,10 @@ module Api
     # by other methods.
     def test_show_using_token_fail
       user = create(:user)
-      token = create(:access_token, :user => user, :allow_read_prefs => false)
+      token = create(:oauth_access_token, :resource_owner_id => user.id)
       create(:user_preference, :user => user, :k => "key", :v => "value")
 
-      signed_get user_preference_path(:preference_key => "key"), :oauth => { :token => token }
+      get user_preference_path(:preference_key => "key"), :headers => bearer_authorization_header(token.token)
       assert_response :forbidden
     end
   end
index 3f862fbca752e9887cbf63df02491ea493c83f10..6a8b62f6c077217548278d3330c6c98c2823abc7 100644 (file)
@@ -1,5 +1,7 @@
 FactoryBot.define do
   factory :oauth_access_token, :class => "Doorkeeper::AccessToken" do
     application :factory => :oauth_application
+
+    resource_owner_id { create(:user).id }
   end
 end
index df127bc8c9bb8d9acb1f032fb2810301fce4c1ae..c027f1479470789c624d8af2b4fe2dde480c4b13 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae"
   integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==
 
-"@eslint/config-array@^0.17.0":
-  version "0.17.0"
-  resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.17.0.tgz#ff305e1ee618a00e6e5d0485454c8d92d94a860d"
-  integrity sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==
+"@eslint/config-array@^0.17.1":
+  version "0.17.1"
+  resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.17.1.tgz#d9b8b8b6b946f47388f32bedfd3adf29ca8f8910"
+  integrity sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==
   dependencies:
     "@eslint/object-schema" "^2.1.4"
     debug "^4.3.1"
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"
 
-"@eslint/js@9.7.0":
-  version "9.7.0"
-  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.7.0.tgz#b712d802582f02b11cfdf83a85040a296afec3f0"
-  integrity sha512-ChuWDQenef8OSFnvuxv0TCVxEwmu3+hPNKvM9B34qpM0rDRbjL8t5QkQeHHeAfsKQjuH9wS82WeCi1J/owatng==
+"@eslint/js@9.8.0":
+  version "9.8.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.8.0.tgz#ae9bc14bb839713c5056f5018bcefa955556d3a4"
+  integrity sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==
 
 "@eslint/object-schema@^2.1.4":
   version "2.1.4"
@@ -246,15 +246,15 @@ eslint-visitor-keys@^4.0.0:
   integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==
 
 eslint@^9.0.0:
-  version "9.7.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.7.0.tgz#bedb48e1cdc2362a0caaa106a4c6ed943e8b09e4"
-  integrity sha512-FzJ9D/0nGiCGBf8UXO/IGLTgLVzIxze1zpfA8Ton2mjLovXdAPlYDv+MQDcqj3TmrhAGYfOpz9RfR+ent0AgAw==
+  version "9.8.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.8.0.tgz#a4f4a090c8ea2d10864d89a6603e02ce9f649f0f"
+  integrity sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
     "@eslint-community/regexpp" "^4.11.0"
-    "@eslint/config-array" "^0.17.0"
+    "@eslint/config-array" "^0.17.1"
     "@eslint/eslintrc" "^3.1.0"
-    "@eslint/js" "9.7.0"
+    "@eslint/js" "9.8.0"
     "@humanwhocodes/module-importer" "^1.0.1"
     "@humanwhocodes/retry" "^0.3.0"
     "@nodelib/fs.walk" "^1.2.8"