added ability to link accounts with `account🔗token` + `account🔗add` & switch between them with `i:am`/`we:are` bangtags; remove links with `account🔗del:USERNAME` or `account🔗clear`; list links with `account🔗list`

staging
multiple creatures 2019-08-06 13:55:54 -05:00
parent 647ac0f86a
commit da389a664b
10 changed files with 175 additions and 10 deletions

View File

@ -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

View File

@ -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;
}
},
};

View File

@ -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

17
app/models/linked_user.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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? }

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
Fabricator(:linked_user) do
user nil
target_user nil
end

View File

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe LinkedUser, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end