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.

601 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 '#hidden?' do
  71. context 'if private_visibility?' do
  72. it 'returns true' do
  73. subject.visibility = :private
  74. expect(subject.hidden?).to be true
  75. end
  76. end
  77. context 'if direct_visibility?' do
  78. it 'returns true' do
  79. subject.visibility = :direct
  80. expect(subject.hidden?).to be true
  81. end
  82. end
  83. context 'if public_visibility?' do
  84. it 'returns false' do
  85. subject.visibility = :public
  86. expect(subject.hidden?).to be false
  87. end
  88. end
  89. context 'if unlisted_visibility?' do
  90. it 'returns false' do
  91. subject.visibility = :unlisted
  92. expect(subject.hidden?).to be false
  93. end
  94. end
  95. end
  96. describe '#content' do
  97. it 'returns the text of the status if it is not a reblog' do
  98. expect(subject.content).to eql subject.text
  99. end
  100. it 'returns the text of the reblogged status' do
  101. subject.reblog = other
  102. expect(subject.content).to eql other.text
  103. end
  104. end
  105. describe '#target' do
  106. it 'returns nil if the status is self-contained' do
  107. expect(subject.target).to be_nil
  108. end
  109. it 'returns nil if the status is a reply' do
  110. subject.thread = other
  111. expect(subject.target).to be_nil
  112. end
  113. it 'returns the reblogged status' do
  114. subject.reblog = other
  115. expect(subject.target).to eq other
  116. end
  117. end
  118. describe '#reblogs_count' do
  119. it 'is the number of reblogs' do
  120. Fabricate(:status, account: bob, reblog: subject)
  121. Fabricate(:status, account: alice, reblog: subject)
  122. expect(subject.reblogs_count).to eq 2
  123. end
  124. it 'is decremented when reblog is removed' do
  125. reblog = Fabricate(:status, account: bob, reblog: subject)
  126. expect(subject.reblogs_count).to eq 1
  127. reblog.destroy
  128. expect(subject.reblogs_count).to eq 0
  129. end
  130. it 'does not fail when original is deleted before reblog' do
  131. reblog = Fabricate(:status, account: bob, reblog: subject)
  132. expect(subject.reblogs_count).to eq 1
  133. expect { subject.destroy }.to_not raise_error
  134. expect(Status.find_by(id: reblog.id)).to be_nil
  135. end
  136. end
  137. describe '#replies_count' do
  138. it 'is the number of replies' do
  139. reply = Fabricate(:status, account: bob, thread: subject)
  140. expect(subject.replies_count).to eq 1
  141. end
  142. it 'is decremented when reply is removed' do
  143. reply = Fabricate(:status, account: bob, thread: subject)
  144. expect(subject.replies_count).to eq 1
  145. reply.destroy
  146. expect(subject.replies_count).to eq 0
  147. end
  148. end
  149. describe '#favourites_count' do
  150. it 'is the number of favorites' do
  151. Fabricate(:favourite, account: bob, status: subject)
  152. Fabricate(:favourite, account: alice, status: subject)
  153. expect(subject.favourites_count).to eq 2
  154. end
  155. it 'is decremented when favourite is removed' do
  156. favourite = Fabricate(:favourite, account: bob, status: subject)
  157. expect(subject.favourites_count).to eq 1
  158. favourite.destroy
  159. expect(subject.favourites_count).to eq 0
  160. end
  161. end
  162. describe '#proper' do
  163. it 'is itself for original statuses' do
  164. expect(subject.proper).to eq subject
  165. end
  166. it 'is the source status for reblogs' do
  167. subject.reblog = other
  168. expect(subject.proper).to eq other
  169. end
  170. end
  171. describe '.mutes_map' do
  172. let(:status) { Fabricate(:status) }
  173. let(:account) { Fabricate(:account) }
  174. subject { Status.mutes_map([status.conversation.id], account) }
  175. it 'returns a hash' do
  176. expect(subject).to be_a Hash
  177. end
  178. it 'contains true value' do
  179. account.mute_conversation!(status.conversation)
  180. expect(subject[status.conversation.id]).to be true
  181. end
  182. end
  183. describe '.favourites_map' do
  184. let(:status) { Fabricate(:status) }
  185. let(:account) { Fabricate(:account) }
  186. subject { Status.favourites_map([status], account) }
  187. it 'returns a hash' do
  188. expect(subject).to be_a Hash
  189. end
  190. it 'contains true value' do
  191. Fabricate(:favourite, status: status, account: account)
  192. expect(subject[status.id]).to be true
  193. end
  194. end
  195. describe '.reblogs_map' do
  196. let(:status) { Fabricate(:status) }
  197. let(:account) { Fabricate(:account) }
  198. subject { Status.reblogs_map([status], account) }
  199. it 'returns a hash' do
  200. expect(subject).to be_a Hash
  201. end
  202. it 'contains true value' do
  203. Fabricate(:status, account: account, reblog: status)
  204. expect(subject[status.id]).to be true
  205. end
  206. end
  207. describe '.in_chosen_languages' do
  208. context 'for accounts with language filters' do
  209. let(:user) { Fabricate(:user, chosen_languages: ['en']) }
  210. it 'does not include statuses in not in chosen languages' do
  211. status = Fabricate(:status, language: 'de')
  212. expect(Status.in_chosen_languages(user.account)).not_to include status
  213. end
  214. it 'includes status with unknown language' do
  215. status = Fabricate(:status, language: nil)
  216. expect(Status.in_chosen_languages(user.account)).to include status
  217. end
  218. end
  219. end
  220. describe '.as_public_timeline' do
  221. it 'only includes statuses with public visibility' do
  222. public_status = Fabricate(:status, visibility: :public)
  223. private_status = Fabricate(:status, visibility: :private)
  224. results = Status.as_public_timeline
  225. expect(results).to include(public_status)
  226. expect(results).not_to include(private_status)
  227. end
  228. it 'does not include replies' do
  229. status = Fabricate(:status)
  230. reply = Fabricate(:status, in_reply_to_id: status.id)
  231. results = Status.as_public_timeline
  232. expect(results).to include(status)
  233. expect(results).not_to include(reply)
  234. end
  235. it 'does not include boosts' do
  236. status = Fabricate(:status)
  237. boost = Fabricate(:status, reblog_of_id: status.id)
  238. results = Status.as_public_timeline
  239. expect(results).to include(status)
  240. expect(results).not_to include(boost)
  241. end
  242. it 'filters out silenced accounts' do
  243. account = Fabricate(:account)
  244. silenced_account = Fabricate(:account, silenced: true)
  245. status = Fabricate(:status, account: account)
  246. silenced_status = Fabricate(:status, account: silenced_account)
  247. results = Status.as_public_timeline
  248. expect(results).to include(status)
  249. expect(results).not_to include(silenced_status)
  250. end
  251. context 'without local_only option' do
  252. let(:viewer) { nil }
  253. let!(:local_account) { Fabricate(:account, domain: nil) }
  254. let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
  255. let!(:local_status) { Fabricate(:status, account: local_account) }
  256. let!(:remote_status) { Fabricate(:status, account: remote_account) }
  257. subject { Status.as_public_timeline(viewer, false) }
  258. context 'without a viewer' do
  259. let(:viewer) { nil }
  260. it 'includes remote instances statuses' do
  261. expect(subject).to include(remote_status)
  262. end
  263. it 'includes local statuses' do
  264. expect(subject).to include(local_status)
  265. end
  266. end
  267. context 'with a viewer' do
  268. let(:viewer) { Fabricate(:account, username: 'viewer') }
  269. it 'includes remote instances statuses' do
  270. expect(subject).to include(remote_status)
  271. end
  272. it 'includes local statuses' do
  273. expect(subject).to include(local_status)
  274. end
  275. end
  276. end
  277. context 'with a local_only option set' do
  278. let!(:local_account) { Fabricate(:account, domain: nil) }
  279. let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
  280. let!(:local_status) { Fabricate(:status, account: local_account) }
  281. let!(:remote_status) { Fabricate(:status, account: remote_account) }
  282. subject { Status.as_public_timeline(viewer, true) }
  283. context 'without a viewer' do
  284. let(:viewer) { nil }
  285. it 'does not include remote instances statuses' do
  286. expect(subject).to include(local_status)
  287. expect(subject).not_to include(remote_status)
  288. end
  289. end
  290. context 'with a viewer' do
  291. let(:viewer) { Fabricate(:account, username: 'viewer') }
  292. it 'does not include remote instances statuses' do
  293. expect(subject).to include(local_status)
  294. expect(subject).not_to include(remote_status)
  295. end
  296. it 'is not affected by personal domain blocks' do
  297. viewer.block_domain!('test.com')
  298. expect(subject).to include(local_status)
  299. expect(subject).not_to include(remote_status)
  300. end
  301. end
  302. end
  303. context 'with a remote_only option set' do
  304. let!(:local_account) { Fabricate(:account, domain: nil) }
  305. let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
  306. let!(:local_status) { Fabricate(:status, account: local_account) }
  307. let!(:remote_status) { Fabricate(:status, account: remote_account) }
  308. subject { Status.as_public_timeline(viewer, :remote) }
  309. context 'without a viewer' do
  310. let(:viewer) { nil }
  311. it 'does not include local instances statuses' do
  312. expect(subject).not_to include(local_status)
  313. expect(subject).to include(remote_status)
  314. end
  315. end
  316. context 'with a viewer' do
  317. let(:viewer) { Fabricate(:account, username: 'viewer') }
  318. it 'does not include local instances statuses' do
  319. expect(subject).not_to include(local_status)
  320. expect(subject).to include(remote_status)
  321. end
  322. end
  323. end
  324. describe 'with an account passed in' do
  325. before do
  326. @account = Fabricate(:account)
  327. end
  328. it 'excludes statuses from accounts blocked by the account' do
  329. blocked = Fabricate(:account)
  330. Fabricate(:block, account: @account, target_account: blocked)
  331. blocked_status = Fabricate(:status, account: blocked)
  332. results = Status.as_public_timeline(@account)
  333. expect(results).not_to include(blocked_status)
  334. end
  335. it 'excludes statuses from accounts who have blocked the account' do
  336. blocked = Fabricate(:account)
  337. Fabricate(:block, account: blocked, target_account: @account)
  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 muted by the account' do
  343. muted = Fabricate(:account)
  344. Fabricate(:mute, account: @account, target_account: muted)
  345. muted_status = Fabricate(:status, account: muted)
  346. results = Status.as_public_timeline(@account)
  347. expect(results).not_to include(muted_status)
  348. end
  349. it 'excludes statuses from accounts from personally blocked domains' do
  350. blocked = Fabricate(:account, domain: 'example.com')
  351. @account.block_domain!(blocked.domain)
  352. blocked_status = Fabricate(:status, account: blocked)
  353. results = Status.as_public_timeline(@account)
  354. expect(results).not_to include(blocked_status)
  355. end
  356. context 'with language preferences' do
  357. it 'excludes statuses in languages not allowed by the account user' do
  358. user = Fabricate(:user, chosen_languages: [:en, :es])
  359. @account.update(user: user)
  360. en_status = Fabricate(:status, language: 'en')
  361. es_status = Fabricate(:status, language: 'es')
  362. fr_status = Fabricate(:status, language: 'fr')
  363. results = Status.as_public_timeline(@account)
  364. expect(results).to include(en_status)
  365. expect(results).to include(es_status)
  366. expect(results).not_to include(fr_status)
  367. end
  368. it 'includes all languages when user does not have a setting' do
  369. user = Fabricate(:user, chosen_languages: nil)
  370. @account.update(user: user)
  371. en_status = Fabricate(:status, language: 'en')
  372. es_status = Fabricate(:status, language: 'es')
  373. results = Status.as_public_timeline(@account)
  374. expect(results).to include(en_status)
  375. expect(results).to include(es_status)
  376. end
  377. it 'includes all languages when account does not have a user' do
  378. expect(@account.user).to be_nil
  379. en_status = Fabricate(:status, language: 'en')
  380. es_status = Fabricate(:status, language: 'es')
  381. results = Status.as_public_timeline(@account)
  382. expect(results).to include(en_status)
  383. expect(results).to include(es_status)
  384. end
  385. end
  386. end
  387. end
  388. describe '.as_tag_timeline' do
  389. it 'includes statuses with a tag' do
  390. tag = Fabricate(:tag)
  391. status = Fabricate(:status, tags: [tag])
  392. other = Fabricate(:status)
  393. results = Status.as_tag_timeline(tag)
  394. expect(results).to include(status)
  395. expect(results).not_to include(other)
  396. end
  397. it 'allows replies to be included' do
  398. original = Fabricate(:status)
  399. tag = Fabricate(:tag)
  400. status = Fabricate(:status, tags: [tag], in_reply_to_id: original.id)
  401. results = Status.as_tag_timeline(tag)
  402. expect(results).to include(status)
  403. end
  404. end
  405. describe '.permitted_for' do
  406. subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
  407. let(:target_account) { alice }
  408. let(:account) { bob }
  409. let!(:public_status) { Fabricate(:status, account: target_account, visibility: 'public') }
  410. let!(:unlisted_status) { Fabricate(:status, account: target_account, visibility: 'unlisted') }
  411. let!(:private_status) { Fabricate(:status, account: target_account, visibility: 'private') }
  412. let!(:direct_status) do
  413. Fabricate(:status, account: target_account, visibility: 'direct').tap do |status|
  414. Fabricate(:mention, status: status, account: account)
  415. end
  416. end
  417. let!(:other_direct_status) do
  418. Fabricate(:status, account: target_account, visibility: 'direct').tap do |status|
  419. Fabricate(:mention, status: status)
  420. end
  421. end
  422. context 'given nil' do
  423. let(:account) { nil }
  424. let(:direct_status) { nil }
  425. it { is_expected.to eq(%w(unlisted public)) }
  426. end
  427. context 'given blocked account' do
  428. before do
  429. target_account.block!(account)
  430. end
  431. it { is_expected.to be_empty }
  432. end
  433. context 'given same account' do
  434. let(:account) { target_account }
  435. it { is_expected.to eq(%w(direct direct private unlisted public)) }
  436. end
  437. context 'given followed account' do
  438. before do
  439. account.follow!(target_account)
  440. end
  441. it { is_expected.to eq(%w(direct private unlisted public)) }
  442. end
  443. context 'given unfollowed account' do
  444. it { is_expected.to eq(%w(direct unlisted public)) }
  445. end
  446. end
  447. describe 'before_validation' do
  448. it 'sets account being replied to correctly over intermediary nodes' do
  449. first_status = Fabricate(:status, account: bob)
  450. intermediary = Fabricate(:status, thread: first_status, account: alice)
  451. final = Fabricate(:status, thread: intermediary, account: alice)
  452. expect(final.in_reply_to_account_id).to eq bob.id
  453. end
  454. it 'creates new conversation for stand-alone status' do
  455. expect(Status.create(account: alice, text: 'First').conversation_id).to_not be_nil
  456. end
  457. it 'keeps conversation of parent node' do
  458. parent = Fabricate(:status, text: 'First')
  459. expect(Status.create(account: alice, thread: parent, text: 'Response').conversation_id).to eq parent.conversation_id
  460. end
  461. it 'sets `local` to true for status by local account' do
  462. expect(Status.create(account: alice, text: 'foo').local).to be true
  463. end
  464. it 'sets `local` to false for status by remote account' do
  465. alice.update(domain: 'example.com')
  466. expect(Status.create(account: alice, text: 'foo').local).to be false
  467. end
  468. end
  469. describe 'validation' do
  470. it 'disallow empty uri for remote status' do
  471. alice.update(domain: 'example.com')
  472. status = Fabricate.build(:status, uri: '', account: alice)
  473. expect(status).to model_have_error_on_field(:uri)
  474. end
  475. end
  476. describe 'after_create' do
  477. it 'saves ActivityPub uri as uri for local status' do
  478. status = Status.create(account: alice, text: 'foo')
  479. status.reload
  480. expect(status.uri).to start_with('https://')
  481. end
  482. end
  483. end