about summary refs log tree commit diff
diff options
context:
space:
mode:
authormultiple creatures <dev@multiple-creature.party>2019-04-20 01:00:45 -0500
committermultiple creatures <dev@multiple-creature.party>2019-05-21 03:16:22 -0500
commit87f4b4d230454d4baa7116e55d9aee42199eeb9b (patch)
tree67ba4a84653cc027a414a60732227abeced2fdc0
parent19b78604e9dd1acb6566edd49f5c59536d5fc209 (diff)
Implement share keys and related bangtags, add `sharekey`, `network`, and `curated` to the API, remove app info from the UI, and move timestamps to the right.
-rw-r--r--app/controllers/statuses_controller.rb24
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js38
-rw-r--r--app/javascript/flavours/glitch/styles/components/status.scss6
-rw-r--r--app/lib/bangtags.rb41
-rw-r--r--app/policies/status_policy.rb2
-rw-r--r--app/serializers/rest/status_serializer.rb9
-rw-r--r--app/views/stream_entries/_detailed_status.html.haml29
-rw-r--r--app/views/stream_entries/_simple_status.html.haml4
8 files changed, 132 insertions, 21 deletions
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 28eebda28..299fe0cda 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -12,6 +12,8 @@ class StatusesController < ApplicationController
 
   before_action :set_account
   before_action :set_status
+  before_action :handle_sharekey_change, only: [:show], if: :user_signed_in?
+  before_action :handle_webapp_redirect, only: [:show], if: :user_signed_in?
   before_action :set_instance_presenter
   before_action :set_link_headers
   before_action :check_account_suspension
@@ -190,12 +192,32 @@ class StatusesController < ApplicationController
     @stream_entry = @status.stream_entry
     @type         = @stream_entry.activity_type.downcase
 
-    authorize @status, :show?
+    if @status.sharekey.present? && params[:key] == @status.sharekey
+      skip_authorization
+    else
+      authorize @status, :show?
+    end
   rescue Mastodon::NotPermittedError
     # Reraise in order to get a 404
     raise ActiveRecord::RecordNotFound
   end
 
+  def handle_sharekey_change
+    raise Mastodon::NotPermittedError unless current_account.id == @status.account_id
+    case params[:rekey]
+    when '1'
+      @status.sharekey = SecureRandom.urlsafe_base64(32)
+      @status.save
+    when '0'
+      @status.sharekey = nil
+      @status.save
+    end
+  end
+
+  def handle_webapp_redirect
+    redirect_to "/web/statuses/#{@status.id}" if params[:toweb] == '1'
+  end
+
   def set_instance_presenter
     @instance_presenter = InstancePresenter.new
   end
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index 1633d26ee..e9bbcaa90 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -15,6 +15,7 @@ import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
 import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
 import classNames from 'classnames';
 import PollContainer from 'flavours/glitch/containers/poll_container';
+import { me } from 'flavours/glitch/util/initial_state';
 
 export default class DetailedStatus extends ImmutablePureComponent {
 
@@ -114,10 +115,10 @@ export default class DetailedStatus extends ImmutablePureComponent {
 
     let media           = null;
     let mediaIcon       = null;
-    let applicationLink = '';
     let reblogLink = '';
     let reblogIcon = 'repeat';
     let favouriteLink = '';
+    let sharekeyLinks = '';
 
     if (this.props.measureHeight) {
       outerStyle.height = `${this.state.height}px`;
@@ -168,10 +169,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
       mediaIcon = 'link';
     }
 
-    if (status.get('application')) {
-      applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
-    }
-
     if (status.get('visibility') === 'direct') {
       reblogIcon = 'envelope';
     } else if (status.get('visibility') === 'private') {
@@ -194,6 +191,34 @@ export default class DetailedStatus extends ImmutablePureComponent {
       );
     }
 
+    if (status.get('sharekey')) {
+      sharekeyLinks = (
+        <span>
+          <a href={`${status.get('url')}?key=${status.get('sharekey')}`} target='_blank' className='detailed-status__link'>
+            <i className='fa fa-key' title='Right-click or long press to copy share link with key' />
+          </a>
+          &nbsp;·&nbsp;
+          <a href={`${status.get('url')}?rekey=1&toweb=1`} className='detailed-status__link'>
+            <i className='fa fa-user-plus' title='Generate a new share key' />
+          </a>
+          &nbsp;·&nbsp;
+          <a href={`${status.get('url')}?rekey=0&toweb=1`} className='detailed-status__link'>
+            <i className='fa fa-user-times' title='Revoke share key' />
+          </a>
+          &nbsp;·
+        </span>
+      );
+    } else if (status.getIn(['account', 'id']) == me) {
+      sharekeyLinks = (
+        <span>
+          <a href={`${status.get('url')}?rekey=1&toweb=1`} className='detailed-status__link'>
+            <i className='fa fa-user-plus' title='Generate a new share key' />
+          </a>
+          &nbsp;·
+        </span>
+      );
+    }
+
     if (this.context.router) {
       favouriteLink = (
         <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
@@ -229,9 +254,10 @@ export default class DetailedStatus extends ImmutablePureComponent {
           />
 
           <div className='detailed-status__meta'>
+            {sharekeyLinks} {reblogLink} · {favouriteLink} · <VisibilityIcon visibility={status.get('visibility')} />
             <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
               <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
-            </a>{applicationLink} · {reblogLink} · {favouriteLink} · <VisibilityIcon visibility={status.get('visibility')} />
+            </a>
           </div>
         </div>
       </div>
diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss
index 62f0a815f..4d9f08c1f 100644
--- a/app/javascript/flavours/glitch/styles/components/status.scss
+++ b/app/javascript/flavours/glitch/styles/components/status.scss
@@ -684,6 +684,12 @@
   }
 }
 
+div > span.detailed-status__datetime,
+div > a.detailed-status__datetime {
+  position: relative;
+  float: right;
+}
+
 .status-card {
   display: flex;
   font-size: 14px;
diff --git a/app/lib/bangtags.rb b/app/lib/bangtags.rb
index d1a179a76..0567caf6d 100644
--- a/app/lib/bangtags.rb
+++ b/app/lib/bangtags.rb
@@ -235,6 +235,40 @@ class Bangtags
               mentions = Account.where(id: mention_ids).map { |a| "@#{a.username}" }
               chunk = mentions.join(' ')
             end
+          when 'sharekey'
+            case cmd[2]
+            when 'revoke'
+              if status.conversation_id.present?
+                roars = Status.where(conversation_id: status.conversation_id, account_id: @account.id)
+                roars.each do |roar|
+                  if roar.sharekey.present?
+                    roar.sharekey = nil
+                    roar.save
+                  end
+                end
+              end
+            when 'sync', 'new'
+              if status.conversation_id.present?
+                roars = Status.where(conversation_id: status.conversation_id, account_id: @account.id)
+                earliest_roar = roars.last # The results are in reverse-chronological order.
+                if cmd[2] == 'new' || earlist_roar.sharekey.blank?
+                  sharekey = SecureRandom.urlsafe_base64(32)
+                  earliest_roar.sharekey = sharekey
+                  earliest_roar.save
+                else
+                  sharekey = earliest_roar.sharekey
+                end
+                roars.each do |roar|
+                  if roar.sharekey != sharekey
+                    roar.sharekey = sharekey
+                    roar.save
+                  end
+                end
+              else
+                status.sharekey = SecureRandom.urlsafe_base64(32)
+                status.save
+              end
+            end
           end
         when 'parent'
           chunk = nil
@@ -326,6 +360,13 @@ class Bangtags
             end
             @vars['_they:are'] = name.strip
           end
+        when 'sharekey'
+          case cmd[1]
+          when 'new'
+            chunk = nil
+            status.sharekey = SecureRandom.urlsafe_base64(32)
+            status.save
+          end
         end
       end
 
diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb
index fcf19db62..0961ec3e2 100644
--- a/app/policies/status_policy.rb
+++ b/app/policies/status_policy.rb
@@ -86,7 +86,7 @@ class StatusPolicy < ApplicationPolicy
   def author
     record.account
   end
-  
+
   def local_only?
     record.local_only?
   end
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index ab3788d75..86e887463 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
   attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
              :sensitive, :spoiler_text, :visibility, :language,
              :uri, :url, :replies_count, :reblogs_count,
-             :favourites_count
+             :favourites_count, :network, :curated
 
   attribute :favourited, if: :current_user?
   attribute :reblogged, if: :current_user?
@@ -12,6 +12,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
   attribute :bookmarked, if: :current_user?
   attribute :pinned, if: :pinnable?
   attribute :local_only if :local?
+  attribute :sharekey, if: :owner?
 
   attribute :content, unless: :source_requested?
   attribute :text, if: :source_requested?
@@ -45,8 +46,12 @@ class REST::StatusSerializer < ActiveModel::Serializer
     !current_user.nil?
   end
 
+  def owner?
+    current_user? && current_user.account_id == object.account_id
+  end
+
   def show_application?
-    object.account.user_shows_application? || (current_user? && current_user.account_id == object.account_id)
+    object.account.user_shows_application? || owner?
   end
 
   def visibility
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index 042e5d950..372d29b7c 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -37,17 +37,19 @@
     = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
 
   .detailed-status__meta
-    %data.dt-published{ value: status.created_at.to_time.iso8601 }
 
-    = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener' do
-      %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
-    ·
-    - if status.application && @account.user&.setting_show_application
-      - if status.application.website.blank?
-        %strong.detailed-status__application= status.application.name
-      - else
-        = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener'
+    - if user_signed_in? && @account.id == status.account_id
+      - if status.sharekey.present?
+        = link_to "#{TagManager.instance.url_for(status)}?key=#{status.sharekey}", class: 'detailed-status__link', title: 'Right-click or long-press to copy share link with key', target: stream_link_target, rel: 'noopener' do
+          = fa_icon('key')
+        ·
+      = link_to "#{TagManager.instance.url_for(status)}?rekey=1", class: 'detailed-status__link', title: 'Generate a new share key', target: stream_link_target, rel: 'noopener' do
+        = fa_icon('user-plus')
       ·
+      - if status.sharekey.present?
+        = link_to "#{TagManager.instance.url_for(status)}?rekey=0", class: 'detailed-status__link', title: 'Revoke share key', target: stream_link_target, rel: 'noopener' do
+          = fa_icon('user-times')
+        ·
     = link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do
       - if status.in_reply_to_id.nil?
         = fa_icon('reply')
@@ -75,6 +77,11 @@
         = fa_icon('star')
         = " "
 
-    - if user_signed_in?
+    %span.detailed-status__datetime
+      %data.dt-published{ value: status.created_at.to_time.iso8601 }
+      = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener' do
+        %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
       ·
-      = link_to t('statuses.open_in_web'), web_url("statuses/#{status.id}"), class: 'detailed-status__application', target: '_blank'
+      - if user_signed_in?
+        = link_to t('statuses.open_in_web'), web_url("statuses/#{status.id}"), class: 'detailed-status__application', target: '_blank'
+
diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml
index d4b919b1a..f18844d02 100644
--- a/app/views/stream_entries/_simple_status.html.haml
+++ b/app/views/stream_entries/_simple_status.html.haml
@@ -42,6 +42,10 @@
     = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
 
   .status__action-bar
+    - if status.sharekey.present? && user_signed_in? && @account.id == status.account_id
+      = link_to "#{TagManager.instance.url_for(status)}?key=#{status.sharekey}", class: 'status__action-bar-button icon-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px', title: 'Right click or long-press to copy share link with key', target: stream_link_target, rel: 'noopener' do
+        = fa_icon('key')
+
     .status__action-bar__counter
       = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
         - if status.in_reply_to_id.nil?