You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

607 lines
18 KiB

8 years ago
Account domain blocks (#2381) * Add <ostatus:conversation /> tag to Atom input/output Only uses ref attribute (not href) because href would be the alternate link that's always included also. Creates new conversation for every non-reply status. Carries over conversation for every reply. Keeps remote URIs verbatim, generates local URIs on the fly like the rest of them. * Conversation muting - prevents notifications that reference a conversation (including replies, favourites, reblogs) from being created. API endpoints /api/v1/statuses/:id/mute and /api/v1/statuses/:id/unmute Currently no way to tell when a status/conversation is muted, so the web UI only has a "disable notifications" button, doesn't work as a toggle * Display "Dismiss notifications" on all statuses in notifications column, not just own * Add "muted" as a boolean attribute on statuses JSON For now always false on contained reblogs, since it's only relevant for statuses returned from the notifications endpoint, which are not nested Remove "Disable notifications" from detailed status view, since it's only relevant in the notifications column * Up max class length * Remove pending test for conversation mute * Add tests, clean up * Rename to "mute conversation" and "unmute conversation" * Raise validation error when trying to mute/unmute status without conversation * Adding account domain blocks that filter notifications and public timelines * Add tests for domain blocks in notifications, public timelines Filter reblogs of blocked domains from home * Add API for listing and creating account domain blocks * API for creating/deleting domain blocks, tests for Status#ancestors and Status#descendants, filter domain blocks from them * Filter domains in streaming API * Update account_domain_block_spec.rb
7 years ago
8 years ago
  1. require 'rails_helper'
  2. RSpec.describe Status, type: :model do
  3. let(:alice) { Fabricate(:account, username: 'alice') }
  4. let(:bob) { Fabricate(:account, username: 'bob') }
  5. let(:other) { Fabricate(:status, account: bob, text: 'Skulls for the skull god! The enemy\'s gates are sideways!') }
  6. subject { Fabricate(:status, account: alice) }
  7. describe '#local?' do
  8. it 'returns true when no remote URI is set' do
  9. expect(subject.local?).to be true
  10. end
  11. it 'returns false if a remote URI is set' do
  12. alice.update(domain: 'example.com')
  13. subject.save
  14. expect(subject.local?).to be false
  15. end
  16. it 'returns true if a URI is set and `local` is true' do
  17. subject.update(uri: 'example.com', local: true)
  18. expect(subject.local?).to be true
  19. end
  20. end
  21. describe '#reblog?' do
  22. it 'returns true when the status reblogs another status' do
  23. subject.reblog = other
  24. expect(subject.reblog?).to be true
  25. end
  26. it 'returns false if the status is self-contained' do
  27. expect(subject.reblog?).to be false
  28. end
  29. end
  30. describe '#reply?' do
  31. it 'returns true if the status references another' do
  32. subject.thread = other
  33. expect(subject.reply?).to be true
  34. end
  35. it 'returns false if the status is self-contained' do
  36. expect(subject.reply?).to be false
  37. end
  38. end
  39. describe '#verb' do
  40. context 'if destroyed?' do
  41. it 'returns :delete' do
  42. subject.destroy!
  43. expect(subject.verb).to be :delete
  44. end
  45. end
  46. context 'unless destroyed?' do
  47. context 'if reblog?' do
  48. it 'returns :share' do
  49. subject.reblog = other
  50. expect(subject.verb).to be :share
  51. end
  52. end
  53. context 'unless reblog?' do
  54. it 'returns :post' do
  55. subject.reblog = nil
  56. expect(subject.verb).to be :post
  57. end
  58. end
  59. end
  60. end
  61. describe '#object_type' do
  62. it 'is note when the status is self-contained' do
  63. expect(subject.object_type).to be :note
  64. end
  65. it 'is comment when the status replies to another' do
  66. subject.thread = other
  67. expect(subject.object_type).to be :comment
  68. end
  69. end
  70. describe '#title' do
  71. # rubocop:disable Style/InterpolationCheck
  72. let(:account) { subject.account }
  73. context 'if destroyed?' do
  74. it 'returns "#{account.acct} deleted status"' do
  75. subject.destroy!
  76. expect(subject.title).to eq "#{account.acct} deleted status"
  77. end
  78. end
  79. context 'unless destroyed?' do
  80. context 'if reblog?' do
  81. it 'returns "#{account.acct} shared #{reblog.account.acct}\'s: #{preview}"' do
  82. reblog = subject.reblog = other
  83. preview = subject.text.slice(0, 10).split("\n")[0]
  84. expect(subject.title).to(
  85. eq "#{account.acct} shared #{reblog.account.acct}'s: #{preview}"
  86. )
  87. end
  88. end
  89. context 'unless reblog?' do
  90. it 'returns "#{account.acct}: #{preview}"' do
  91. subject.reblog = nil
  92. preview = subject.text.slice(0, 20).split("\n")[0]
  93. expect(subject.title).to eq "#{account.acct}: #{preview}"
  94. end
  95. end
  96. end
  97. end
  98. describe '#hidden?' do
  99. context 'if private_visibility?' do
  100. it 'returns true' do
  101. subject.visibility = :private
  102. expect(subject.hidden?).to be true
  103. end
  104. end
  105. context 'if direct_visibility?' do
  106. it 'returns true' do
  107. subject.visibility = :direct
  108. expect(subject.hidden?).to be true
  109. end
  110. end
  111. context 'if public_visibility?' do
  112. it 'returns false' do
  113. subject.visibility = :public
  114. expect(subject.hidden?).to be false
  115. end
  116. end
  117. context 'if unlisted_visibility?' do
  118. it 'returns false' do
  119. subject.visibility = :unlisted
  120. expect(subject.hidden?).to be false
  121. end
  122. end
  123. end
  124. describe '#content' do
  125. it 'returns the text of the status if it is not a reblog' do
  126. expect(subject.content).to eql subject.text
  127. end
  128. it 'returns the text of the reblogged status' do
  129. subject.reblog = other
  130. expect(subject.content).to eql other.text
  131. end
  132. end
  133. describe '#target' do
  134. it 'returns nil if the status is self-contained' do
  135. expect(subject.target).to be_nil
  136. end
  137. it 'returns nil if the status is a reply' do
  138. subject.thread = other
  139. expect(subject.target).to be_nil
  140. end
  141. it 'returns the reblogged status' do
  142. subject.reblog = other
  143. expect(subject.target).to eq other
  144. end
  145. end
  146. describe '#reblogs_count' do
  147. it 'is the number of reblogs' do
  148. Fabricate(:status, account: bob, reblog: subject)
  149. Fabricate(:status, account: alice, reblog: subject)
  150. expect(subject.reblogs_count).to eq 2
  151. end
  152. it 'is decremented when reblog is removed' do
  153. reblog = Fabricate(:status, account: bob, reblog: subject)
  154. expect(subject.reblogs_count).to eq 1
  155. reblog.destroy
  156. expect(subject.reblogs_count).to eq 0
  157. end
  158. it 'does not fail when original is deleted before reblog' do
  159. reblog = Fabricate(:status, account: bob, reblog: subject)
  160. expect(subject.reblogs_count).to eq 1
  161. expect { subject.destroy }.to_not raise_error
  162. expect(Status.find_by(id: reblog.id)).to be_nil
  163. end
  164. end
  165. describe '#replies_count' do
  166. it 'is the number of replies' do
  167. reply = Fabricate(:status, account: bob, thread: subject)
  168. expect(subject.replies_count).to eq 1
  169. end
  170. it 'is decremented when reply is removed' do
  171. reply = Fabricate(:status, account: bob, thread: subject)
  172. expect(subject.replies_count).to eq 1
  173. reply.destroy
  174. expect(subject.replies_count).to eq 0
  175. end
  176. end
  177. describe '#favourites_count' do
  178. it 'is the number of favorites' do
  179. Fabricate(:favourite, account: bob, status: subject)
  180. Fabricate(:favourite, account: alice, status: subject)
  181. expect(subject.favourites_count).to eq 2
  182. end
  183. it 'is decremented when favourite is removed' do
  184. favourite = Fabricate(:favourite, account: bob, status: subject)
  185. expect(subject.favourites_count).to eq 1
  186. favourite.destroy
  187. expect(subject.favourites_count).to eq 0
  188. end
  189. end
  190. describe '#proper' do
  191. it 'is itself for original statuses' do
  192. expect(subject.proper).to eq subject
  193. end
  194. it 'is the source status for reblogs' do
  195. subject.reblog = other
  196. expect(subject.proper).to eq other
  197. end
  198. end
  199. describe '.mutes_map' do
  200. let(:status) { Fabricate(:status) }
  201. let(:account) { Fabricate(:account) }
  202. subject { Status.mutes_map([status.conversation.id], account) }
  203. it 'returns a hash' do
  204. expect(subject).to be_a Hash
  205. end
  206. it 'contains true value' do
  207. account.mute_conversation!(status.conversation)
  208. expect(subject[status.conversation.id]).to be true
  209. end
  210. end
  211. describe '.favourites_map' do
  212. let(:status) { Fabricate(:status) }
  213. let(:account) { Fabricate(:account) }
  214. subject { Status.favourites_map([status], account) }
  215. it 'returns a hash' do
  216. expect(subject).to be_a Hash
  217. end
  218. it 'contains true value' do
  219. Fabricate(:favourite, status: status, account: account)
  220. expect(subject[status.id]).to be true
  221. end
  222. end
  223. describe '.reblogs_map' do
  224. let(:status) { Fabricate(:status) }
  225. let(:account) { Fabricate(:account) }
  226. subject { Status.reblogs_map([status], account) }
  227. it 'returns a hash' do
  228. expect(subject).to be_a Hash
  229. end
  230. it 'contains true value' do
  231. Fabricate(:status, account: account, reblog: status)
  232. expect(subject[status.id]).to be true
  233. end
  234. end
  235. describe '.in_chosen_languages' do
  236. context 'for accounts with language filters' do
  237. let(:user) { Fabricate(:user, chosen_languages: ['en']) }
  238. it 'does not include statuses in not in chosen languages' do
  239. status = Fabricate(:status, language: 'de')
  240. expect(Status.in_chosen_languages(user.account)).not_to include status
  241. end
  242. it 'includes status with unknown language' do
  243. status = Fabricate(:status, language: nil)
  244. expect(Status.in_chosen_languages(user.account)).to include status
  245. end
  246. end
  247. end
  248. describe '.as_public_timeline' do
  249. it 'only includes statuses with public visibility' do
  250. public_status = Fabricate(:status, visibility: :public)
  251. private_status = Fabricate(:status, visibility: :private)
  252. results = Status.as_public_timeline
  253. expect(results).to include(public_status)
  254. expect(results).not_to include(private_status)
  255. end
  256. it 'does not include replies' do
  257. status = Fabricate(:status)
  258. reply = Fabricate(:status, in_reply_to_id: status.id)
  259. results = Status.as_public_timeline
  260. expect(results).to include(status)
  261. expect(results).not_to include(reply)
  262. end
  263. it 'does not include boosts' do
  264. status = Fabricate(:status)
  265. boost = Fabricate(:status, reblog_of_id: status.id)
  266. results = Status.as_public_timeline
  267. expect(results).to include(status)
  268. expect(results).not_to include(boost)
  269. end
  270. it 'filters out silenced accounts' do
  271. account = Fabricate(:account)
  272. silenced_account = Fabricate(:account, silenced: true)
  273. status = Fabricate(:status, account: account)
  274. silenced_status = Fabricate(:status, account: silenced_account)
  275. results = Status.as_public_timeline
  276. expect(results).to include(status)
  277. expect(results).not_to include(silenced_status)
  278. end
  279. context 'without local_only option' do
  280. let(:viewer) { nil }
  281. let!(:local_account) { Fabricate(:account, domain: nil) }
  282. let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
  283. let!(:local_status) { Fabricate(:status, account: local_account) }
  284. let!(:remote_status) { Fabricate(:status, account: remote_account) }
  285. subject { Status.as_public_timeline(viewer, false) }
  286. context 'without a viewer' do
  287. let(:viewer) { nil }
  288. it 'includes remote instances statuses' do
  289. expect(subject).to include(remote_status)
  290. end
  291. it 'includes local statuses' do
  292. expect(subject).to include(local_status)
  293. end
  294. end
  295. context 'with a viewer' do
  296. let(:viewer) { Fabricate(:account, username: 'viewer') }
  297. it 'includes remote instances statuses' do
  298. expect(subject).to include(remote_status)
  299. end
  300. it 'includes local statuses' do
  301. expect(subject).to include(local_status)
  302. end
  303. end
  304. end
  305. context 'with a local_only option set' do
  306. let!(:local_account) { Fabricate(:account, domain: nil) }
  307. let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
  308. let!(:local_status) { Fabricate(:status, account: local_account) }
  309. let!(:remote_status) { Fabricate(:status, account: remote_account) }
  310. subject { Status.as_public_timeline(viewer, true) }
  311. context 'without a viewer' do
  312. let(:viewer) { nil }
  313. it 'does not include remote instances statuses' do
  314. expect(subject).to include(local_status)
  315. expect(subject).not_to include(remote_status)
  316. end
  317. end
  318. context 'with a viewer' do
  319. let(:viewer) { Fabricate(:account, username: 'viewer') }
  320. it 'does not include remote instances statuses' do
  321. expect(subject).to include(local_status)
  322. expect(subject).not_to include(remote_status)
  323. end
  324. it 'is not affected by personal domain blocks' do
  325. viewer.block_domain!('test.com')
  326. expect(subject).to include(local_status)
  327. expect(subject).not_to include(remote_status)
  328. end
  329. end
  330. end
  331. describe 'with an account passed in' do
  332. before do
  333. @account = Fabricate(:account)
  334. end
  335. it 'excludes statuses from accounts blocked by the account' do
  336. blocked = Fabricate(:account)
  337. Fabricate(:block, account: @account, target_account: blocked)
  338. blocked_status = Fabricate(:status, account: blocked)
  339. results = Status.as_public_timeline(@account)
  340. expect(results).not_to include(blocked_status)
  341. end
  342. it 'excludes statuses from accounts who have blocked the account' do
  343. blocked = Fabricate(:account)
  344. Fabricate(:block, account: blocked, target_account: @account)
  345. blocked_status = Fabricate(:status, account: blocked)
  346. results = Status.as_public_timeline(@account)
  347. expect(results).not_to include(blocked_status)
  348. end
  349. it 'excludes statuses from accounts muted by the account' do
  350. muted = Fabricate(:account)
  351. Fabricate(:mute, account: @account, target_account: muted)
  352. muted_status = Fabricate(:status, account: muted)
  353. results = Status.as_public_timeline(@account)
  354. expect(results).not_to include(muted_status)
  355. end
  356. it 'excludes statuses from accounts from personally blocked domains' do
  357. blocked = Fabricate(:account, domain: 'example.com')
  358. @account.block_domain!(blocked.domain)
  359. blocked_status = Fabricate(:status, account: blocked)
  360. results = Status.as_public_timeline(@account)
  361. expect(results).not_to include(blocked_status)
  362. end
  363. context 'with language preferences' do
  364. it 'excludes statuses in languages not allowed by the account user' do
  365. user = Fabricate(:user, chosen_languages: [:en, :es])
  366. @account.update(user: user)
  367. en_status = Fabricate(:status, language: 'en')
  368. es_status = Fabricate(:status, language: 'es')
  369. fr_status = Fabricate(:status, language: 'fr')
  370. results = Status.as_public_timeline(@account)
  371. expect(results).to include(en_status)
  372. expect(results).to include(es_status)
  373. expect(results).not_to include(fr_status)
  374. end
  375. it 'includes all languages when user does not have a setting' do
  376. user = Fabricate(:user, chosen_languages: nil)
  377. @account.update(user: user)
  378. en_status = Fabricate(:status, language: 'en')
  379. es_status = Fabricate(:status, language: 'es')
  380. results = Status.as_public_timeline(@account)
  381. expect(results).to include(en_status)
  382. expect(results).to include(es_status)
  383. end
  384. it 'includes all languages when account does not have a user' do
  385. expect(@account.user).to be_nil
  386. en_status = Fabricate(:status, language: 'en')
  387. es_status = Fabricate(:status, language: 'es')
  388. results = Status.as_public_timeline(@account)
  389. expect(results).to include(en_status)
  390. expect(results).to include(es_status)
  391. end
  392. end
  393. end
  394. end
  395. describe '.as_tag_timeline' do
  396. it 'includes statuses with a tag' do
  397. tag = Fabricate(:tag)
  398. status = Fabricate(:status, tags: [tag])
  399. other = Fabricate(:status)
  400. results = Status.as_tag_timeline(tag)
  401. expect(results).to include(status)
  402. expect(results).not_to include(other)
  403. end
  404. it 'allows replies to be included' do
  405. original = Fabricate(:status)
  406. tag = Fabricate(:tag)
  407. status = Fabricate(:status, tags: [tag], in_reply_to_id: original.id)
  408. results = Status.as_tag_timeline(tag)
  409. expect(results).to include(status)
  410. end
  411. end
  412. describe '.permitted_for' do
  413. subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
  414. let(:target_account) { alice }
  415. let(:account) { bob }
  416. let!(:public_status) { Fabricate(:status, account: target_account, visibility: 'public') }
  417. let!(:unlisted_status) { Fabricate(:status, account: target_account, visibility: 'unlisted') }
  418. let!(:private_status) { Fabricate(:status, account: target_account, visibility: 'private') }
  419. let!(:direct_status) do
  420. Fabricate(:status, account: target_account, visibility: 'direct').tap do |status|
  421. Fabricate(:mention, status: status, account: account)
  422. end
  423. end
  424. let!(:other_direct_status) do
  425. Fabricate(:status, account: target_account, visibility: 'direct').tap do |status|
  426. Fabricate(:mention, status: status)
  427. end
  428. end
  429. context 'given nil' do
  430. let(:account) { nil }
  431. let(:direct_status) { nil }
  432. it { is_expected.to eq(%w(unlisted public)) }
  433. end
  434. context 'given blocked account' do
  435. before do
  436. target_account.block!(account)
  437. end
  438. it { is_expected.to be_empty }
  439. end
  440. context 'given same account' do
  441. let(:account) { target_account }
  442. it { is_expected.to eq(%w(direct direct private unlisted public)) }
  443. end
  444. context 'given followed account' do
  445. before do
  446. account.follow!(target_account)
  447. end
  448. it { is_expected.to eq(%w(direct private unlisted public)) }
  449. end
  450. context 'given unfollowed account' do
  451. it { is_expected.to eq(%w(direct unlisted public)) }
  452. end
  453. end
  454. describe 'before_validation' do
  455. it 'sets account being replied to correctly over intermediary nodes' do
  456. first_status = Fabricate(:status, account: bob)
  457. intermediary = Fabricate(:status, thread: first_status, account: alice)
  458. final = Fabricate(:status, thread: intermediary, account: alice)
  459. expect(final.in_reply_to_account_id).to eq bob.id
  460. end
  461. it 'creates new conversation for stand-alone status' do
  462. expect(Status.create(account: alice, text: 'First').conversation_id).to_not be_nil
  463. end
  464. it 'keeps conversation of parent node' do
  465. parent = Fabricate(:status, text: 'First')
  466. expect(Status.create(account: alice, thread: parent, text: 'Response').conversation_id).to eq parent.conversation_id
  467. end
  468. it 'sets `local` to true for status by local account' do
  469. expect(Status.create(account: alice, text: 'foo').local).to be true
  470. end
  471. it 'sets `local` to false for status by remote account' do
  472. alice.update(domain: 'example.com')
  473. expect(Status.create(account: alice, text: 'foo').local).to be false
  474. end
  475. end
  476. describe 'validation' do
  477. it 'disallow empty uri for remote status' do
  478. alice.update(domain: 'example.com')
  479. status = Fabricate.build(:status, uri: '', account: alice)
  480. expect(status).to model_have_error_on_field(:uri)
  481. end
  482. end
  483. describe 'after_create' do
  484. it 'saves ActivityPub uri as uri for local status' do
  485. status = Status.create(account: alice, text: 'foo')
  486. status.reload
  487. expect(status.uri).to start_with('https://')
  488. end
  489. end
  490. end