about summary refs log tree commit diff
diff options
context:
space:
mode:
authormultiple creatures <dev@multiple-creature.party>2019-08-06 13:55:54 -0500
committermultiple creatures <dev@multiple-creature.party>2019-08-06 13:55:54 -0500
commitda389a664b87bb131435f2ccb904c0754d5d1655 (patch)
tree79f6fe6b29f2c361f1c33aa9a811991892b2f0db
parent647ac0f86abb49b97c55229b70e9c06e943adc98 (diff)
added ability to link accounts with `account:link:token` + `account:link:add` & switch between them with `i:am`/`we:are` bangtags; remove links with `account:link:del:USERNAME` or `account:link:clear`; list links with `account:link:list`
-rw-r--r--app/controllers/auth/sessions_controller.rb14
-rw-r--r--app/javascript/flavours/glitch/actions/streaming.js9
-rw-r--r--app/lib/bangtags.rb99
-rw-r--r--app/models/linked_user.rb17
-rw-r--r--app/models/status.rb8
-rw-r--r--app/models/user.rb3
-rw-r--r--db/migrate/20190805203816_create_linked_users.rb12
-rw-r--r--db/schema.rb14
-rw-r--r--spec/fabricators/linked_user_fabricator.rb4
-rw-r--r--spec/models/linked_user_spec.rb5
10 files changed, 175 insertions, 10 deletions
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index 332f4d7a7..413962607 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -8,6 +8,7 @@ class Auth::SessionsController < Devise::SessionsController
   skip_before_action :require_no_authentication, only: [:create]
   skip_before_action :check_user_permissions, only: [:destroy]
   prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
+  prepend_before_action :switch_user
   prepend_before_action :set_pack
   before_action :set_instance_presenter, only: [:new]
   before_action :set_body_classes
@@ -52,6 +53,10 @@ class Auth::SessionsController < Devise::SessionsController
     params.require(:user).permit(:email, :password, :otp_attempt)
   end
 
+  def switch_params
+    params.permit(:switch_to)
+  end
+
   def after_sign_in_path_for(resource)
     last_url = stored_location_for(:user)
 
@@ -107,6 +112,15 @@ class Auth::SessionsController < Devise::SessionsController
     render :two_factor
   end
 
+  def switch_user
+    return unless switch_params[:switch_to].present? && current_user.present?
+    target_user = User.find_by(id: switch_params[:switch_to])
+    return unless target_user.present? && current_user.in?(target_user.linked_users)
+    self.resource = target_user
+    sign_in(target_user)
+    return root_path
+  end
+
   private
 
   def set_pack
diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js
index b5dd70989..649fda8ca 100644
--- a/app/javascript/flavours/glitch/actions/streaming.js
+++ b/app/javascript/flavours/glitch/actions/streaming.js
@@ -9,6 +9,7 @@ import {
 import { updateNotifications, expandNotifications } from './notifications';
 import { fetchFilters } from './filters';
 import { getLocale } from 'mastodon/locales';
+import { resetCompose } from 'flavours/glitch/actions/compose';
 
 const { messages } = getLocale();
 
@@ -40,6 +41,14 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
         case 'filters_changed':
           dispatch(fetchFilters());
           break;
+        case 'switch_accounts':
+          dispatch(resetCompose());
+          window.location.href = `/auth/sign_in?switch_to=${data.payload}`
+          break;
+        case 'refresh':
+          dispatch(resetCompose());
+          window.location.reload();
+          break;
         }
       },
     };
diff --git a/app/lib/bangtags.rb b/app/lib/bangtags.rb
index 421c142ef..7a6a1cbd1 100644
--- a/app/lib/bangtags.rb
+++ b/app/lib/bangtags.rb
@@ -7,6 +7,7 @@ class Bangtags
   def initialize(status)
     @status        = status
     @account       = status.account
+    @user          = @account.user
     @parent_status = Status.find(status.in_reply_to_id) if status.in_reply_to_id
 
     @crunch_newlines = false
@@ -58,7 +59,7 @@ class Bangtags
     # list of post-processing commands
     @post_cmds = []
     # hash of bangtag variables
-    @vars = account.user.vars
+    @vars = @user.vars
     # keep track of what variables we're appending the value of between chunks
     @vore_stack = []
     # keep track of what type of nested components are active so we can !end them in order
@@ -348,7 +349,7 @@ class Bangtags
             chunk = TagManager.instance.url_for(@parent_status)
           when 'tag', 'untag'
             chunk = nil
-            next unless @parent_status.account.id == @account.id || @account.user.admin?
+            next unless @parent_status.account.id == @account.id || @user.admin?
             tags = cmd[2..-1].map {|t| t.gsub(':', '.')}
             if cmd[1].downcase == 'tag'
               add_tags(@parent_status, *tags)
@@ -376,7 +377,7 @@ class Bangtags
             plain.gsub!(/ dot /i, '.')
             chunk = plain.scan(/[\w\-]+\.[\w\-]+(?:\.[\w\-]+)*/).uniq.join(' ')
           when 'noreplies', 'noats', 'close'
-            next unless @parent_status.account.id == @account.id || @account.user.admin?
+            next unless @parent_status.account.id == @account.id || @user.admin?
             @parent_status.reject_replies = true
             @parent_status.save
             Rails.cache.delete("statuses/#{@parent_status.id}")
@@ -490,6 +491,7 @@ class Bangtags
               end
             else
               who = cmd[0]
+              next if switch_account(who.strip)
               name = who.downcase.gsub(/\s+/, '').strip
               description = cmd[1..-1].join(':').strip
               if description.blank?
@@ -677,7 +679,7 @@ class Bangtags
           chunk = chunk.join
         when 'admin'
           chunk = nil
-          next unless @account.user.admin?
+          next unless @user.admin?
           next if cmd[1].nil?
           @status.visibility = :local
           @status.local_only = true
@@ -710,6 +712,78 @@ class Bangtags
             @tf_cmds.push(cmd)
             @component_stack.push(:tf)
           end
+        when 'account'
+          chunk = nil
+          cmd.shift
+          c = cmd.shift
+          next if c.nil?
+          @status.visibility = :direct
+          @status.local_only = true
+          @status.content_type = 'text/markdown'
+          @chunks << "\n# <code>#!</code><code>account:#{c.downcase}</code>:\n<hr />\n"
+          output = []
+          case c.downcase
+          when 'link'
+            c = cmd.shift
+            next if c.nil?
+            case c.downcase
+            when 'add'
+              target = cmd.shift
+              token = cmd.shift
+              if target.blank? || token.blank?
+                output << "\u274c Missing account parameter." if target.blank?
+                output << "\u274c Missing token parameter." if token.blank?
+                break
+              end
+              target_acct = Account.find_local(target)
+              if target_acct&.user.nil? || target_acct.id == @account.id
+                output << "\u274c Invalid account."
+                break
+              end
+              unless token == target_acct.user.vars['_account:link:token']
+                output << "\u274c Invalid token."
+                break
+              end
+              target_acct.user.vars['_account:link:token'] = nil
+              target_acct.user.save
+              LinkedUser.find_or_create_by!(user_id: @user.id, target_user_id: target_acct.user.id)
+              LinkedUser.find_or_create_by!(user_id: target_acct.user.id, target_user_id: @user.id)
+              output << "\u2705 Linked with <strong>@\u200c#{target}</strong>."
+            when 'del', 'delete'
+              cmd.each do |target|
+                target_acct = Account.find_local(target)
+                next if target_acct&.user.nil? || target_acct.id == @account.id
+                LinkedUser.where(user_id: @user.id, target_user_id: target_acct.user.id).destroy_all
+                LinkedUser.where(user_id: target_acct.user.id, target_user_id: @user.id).destroy_all
+                output << "\u2705 <strong>@\u200c#{target}</strong> unlinked."
+              end
+            when 'clear', 'delall', 'deleteall'
+              LinkedUser.where(target_user_id: @user.id).destroy_all
+              LinkedUser.where(user_id: @user.id).destroy_all
+              output << "\u2705 Cleared all links."
+            when 'token'
+              @vars['_account:link:token'] = SecureRandom.urlsafe_base64(32)
+              output << "Account link token is:"
+              output << "<code>#{@vars['_account:link:token']}</code>"
+              output << ''
+              output << "On the local account you want to link, paste:"
+              output << "<code>#!account:link:add:#{@account.username}:#{@vars['_account:link:token']}</code>"
+              output << ''
+              output << 'The token can only be used once.'
+              output << ''
+              output << "\xe2\x9a\xa0\xef\xb8\x8f <strong>This grants full access to your account! Be careful!</strong>"
+            when 'list'
+              @user.linked_users.find_each do |linked_user|
+                if linked_user&.account.nil?
+                  link.destroy
+                else
+                  output << "\u2705 <strong>@\u200c#{linked_user.account.username}</strong>"
+                end
+              end
+            end
+          end
+          output = ['<em>No action.</em>'] if output.blank?
+          chunk = output.join("\n") + "\n"
         end
       end
 
@@ -741,7 +815,7 @@ class Bangtags
             @vars['_tf:head:full'] = c + parts.count
             chunk = parts.join(' ')
           when 'admin'
-            next unless @account.user.admin?
+            next unless @user.admin?
             next if tf_cmd[1].nil? || chunk.start_with?('`admin:')
             output = []
             action = tf_cmd[1].downcase
@@ -817,7 +891,7 @@ class Bangtags
 
     postprocess_before_save
 
-    account.user.save
+    @user.save
 
     text = @chunks.join
     text.gsub!(/\n\n+/, "\n") if @crunch_newlines
@@ -848,7 +922,7 @@ class Bangtags
           @vars.delete("_media:#{media_idx}:desc")
         end
       when 'admin'
-        next unless @account.user.admin?
+        next unless @user.admin?
         next if post_cmd[1].nil?
         case post_cmd[1]
         when 'eval'
@@ -879,9 +953,9 @@ class Bangtags
             next
           end
 
-          name = @account.user.vars['_they:are']
+          name = @user.vars['_they:are']
           if name.present?
-            footer = "#{@account.user.vars["_they:are:#{name}"]} from @#{@account.username}"
+            footer = "#{@user.vars["_they:are:#{name}"]} from @#{@account.username}"
           else
             footer = "@#{@account.username}"
           end
@@ -935,6 +1009,13 @@ class Bangtags
     from_status.save
   end
 
+  def switch_account(target_acct)
+    target_acct = Account.find_local(target_acct)
+    return false unless target_acct&.user.present? && target_acct.user.in?(@user.linked_users)
+    Redis.current.publish("timeline:#{@account.id}", Oj.dump(event: :switch_accounts, payload: target_acct.user.id))
+    true
+  end
+
   def html_entities
     @html_entities ||= HTMLEntities.new
   end
diff --git a/app/models/linked_user.rb b/app/models/linked_user.rb
new file mode 100644
index 000000000..e049c6f77
--- /dev/null
+++ b/app/models/linked_user.rb
@@ -0,0 +1,17 @@
+# == Schema Information
+#
+# Table name: linked_users
+#
+#  id             :bigint(8)        not null, primary key
+#  user_id        :bigint(8)
+#  target_user_id :bigint(8)
+#  created_at     :datetime         not null
+#  updated_at     :datetime         not null
+#
+
+class LinkedUser < ApplicationRecord
+  belongs_to :user, inverse_of: :linked_users
+  belongs_to :target_user, class_name: 'User'
+
+  validates :user_id, uniqueness: { scope: :target_user_id }
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index a6be93789..9f11e6d5d 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -306,6 +306,14 @@ class Status < ApplicationRecord
     update_status_stat!(key => [public_send(key) - 1, 0].max)
   end
 
+  def session=(value)
+    @session = value
+  end
+
+  def session
+    @session || nil
+  end
+
   after_create_commit  :increment_counter_caches
   after_destroy_commit :decrement_counter_caches
 
diff --git a/app/models/user.rb b/app/models/user.rb
index cbe62d189..479392642 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -75,6 +75,9 @@ class User < ApplicationRecord
   has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
   has_many :backups, inverse_of: :user
 
+  has_many :user_links, class_name: 'LinkedUser', foreign_key: :target_user_id, dependent: :destroy, inverse_of: :user
+  has_many :linked_users, through: :user_links, source: :user
+
   has_one :invite_request, class_name: 'UserInviteRequest', inverse_of: :user, dependent: :destroy
   accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? }
 
diff --git a/db/migrate/20190805203816_create_linked_users.rb b/db/migrate/20190805203816_create_linked_users.rb
new file mode 100644
index 000000000..36744fed6
--- /dev/null
+++ b/db/migrate/20190805203816_create_linked_users.rb
@@ -0,0 +1,12 @@
+class CreateLinkedUsers < ActiveRecord::Migration[5.2]
+  def change
+    create_table :linked_users do |t|
+      t.references :user, foreign_key: { on_delete: :cascade }
+      t.references :target_user, foreign_key: { to_table: 'users', on_delete: :cascade }
+
+      t.timestamps
+    end
+
+    add_index :linked_users , [:user_id, :target_user_id], :unique => true
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 407e1038f..3c2664db5 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2019_08_05_064643) do
+ActiveRecord::Schema.define(version: 2019_08_05_203816) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -361,6 +361,16 @@ ActiveRecord::Schema.define(version: 2019_08_05_064643) do
     t.index ["user_id"], name: "index_invites_on_user_id"
   end
 
+  create_table "linked_users", force: :cascade do |t|
+    t.bigint "user_id"
+    t.bigint "target_user_id"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["target_user_id"], name: "index_linked_users_on_target_user_id"
+    t.index ["user_id", "target_user_id"], name: "index_linked_users_on_user_id_and_target_user_id", unique: true
+    t.index ["user_id"], name: "index_linked_users_on_user_id"
+  end
+
   create_table "list_accounts", force: :cascade do |t|
     t.bigint "list_id", null: false
     t.bigint "account_id", null: false
@@ -827,6 +837,8 @@ ActiveRecord::Schema.define(version: 2019_08_05_064643) do
   add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
   add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
   add_foreign_key "invites", "users", on_delete: :cascade
+  add_foreign_key "linked_users", "users", column: "target_user_id", on_delete: :cascade
+  add_foreign_key "linked_users", "users", on_delete: :cascade
   add_foreign_key "list_accounts", "accounts", on_delete: :cascade
   add_foreign_key "list_accounts", "follows", on_delete: :cascade
   add_foreign_key "list_accounts", "lists", on_delete: :cascade
diff --git a/spec/fabricators/linked_user_fabricator.rb b/spec/fabricators/linked_user_fabricator.rb
new file mode 100644
index 000000000..d9a62f60f
--- /dev/null
+++ b/spec/fabricators/linked_user_fabricator.rb
@@ -0,0 +1,4 @@
+Fabricator(:linked_user) do
+  user        nil
+  target_user nil
+end
diff --git a/spec/models/linked_user_spec.rb b/spec/models/linked_user_spec.rb
new file mode 100644
index 000000000..b283a8b31
--- /dev/null
+++ b/spec/models/linked_user_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe LinkedUser, type: :model do
+  pending "add some examples to (or delete) #{__FILE__}"
+end