From da389a664b87bb131435f2ccb904c0754d5d1655 Mon Sep 17 00:00:00 2001 From: multiple creatures Date: Tue, 6 Aug 2019 13:55:54 -0500 Subject: [PATCH] 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` --- app/controllers/auth/sessions_controller.rb | 14 +++ .../flavours/glitch/actions/streaming.js | 9 ++ app/lib/bangtags.rb | 99 +++++++++++++++++-- app/models/linked_user.rb | 17 ++++ app/models/status.rb | 8 ++ app/models/user.rb | 3 + .../20190805203816_create_linked_users.rb | 12 +++ db/schema.rb | 14 ++- spec/fabricators/linked_user_fabricator.rb | 4 + spec/models/linked_user_spec.rb | 5 + 10 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 app/models/linked_user.rb create mode 100644 db/migrate/20190805203816_create_linked_users.rb create mode 100644 spec/fabricators/linked_user_fabricator.rb create mode 100644 spec/models/linked_user_spec.rb 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# #!account:#{c.downcase}:\n
\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 @\u200c#{target}." + 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 @\u200c#{target} 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 << "#{@vars['_account:link:token']}" + output << '' + output << "On the local account you want to link, paste:" + output << "#!account:link:add:#{@account.username}:#{@vars['_account:link:token']}" + output << '' + output << 'The token can only be used once.' + output << '' + output << "\xe2\x9a\xa0\xef\xb8\x8f This grants full access to your account! Be careful!" + when 'list' + @user.linked_users.find_each do |linked_user| + if linked_user&.account.nil? + link.destroy + else + output << "\u2705 @\u200c#{linked_user.account.username}" + end + end + end + end + output = ['No action.'] 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