Browse Source

Merge tag 'v2.6.5' into cybrespace

kɜ:ʳ cybredragon 5 months ago
parent
commit
1e31148c57
63 changed files with 372 additions and 244 deletions
  1. 67
    0
      CHANGELOG.md
  2. 1
    0
      Gemfile
  3. 5
    0
      Gemfile.lock
  4. 1
    1
      app/controllers/api/v1/accounts_controller.rb
  5. 1
    1
      app/controllers/application_controller.rb
  6. 14
    4
      app/javascript/mastodon/actions/accounts.js
  7. 8
    1
      app/javascript/mastodon/components/status.js
  8. 1
    4
      app/javascript/mastodon/components/status_action_bar.js
  9. 2
    0
      app/javascript/mastodon/components/status_list.js
  10. 1
    1
      app/javascript/mastodon/features/status/components/action_bar.js
  11. 1
    1
      app/javascript/mastodon/features/status/components/card.js
  12. 7
    1
      app/javascript/mastodon/reducers/conversations.js
  13. 12
    0
      app/javascript/mastodon/reducers/relationships.js
  14. 2
    2
      app/javascript/mastodon/reducers/statuses.js
  15. 1
    1
      app/javascript/styles/mastodon/components.scss
  16. 5
    2
      app/javascript/styles/mastodon/forms.scss
  17. 6
    0
      app/lib/activitypub/activity.rb
  18. 1
    1
      app/lib/activitypub/activity/create.rb
  19. 4
    2
      app/lib/activitypub/activity/delete.rb
  20. 1
    1
      app/lib/entity_cache.rb
  21. 5
    1
      app/lib/feed_manager.rb
  22. 2
    2
      app/lib/formatter.rb
  23. 42
    10
      app/lib/request.rb
  24. 2
    2
      app/lib/settings/scoped_settings.rb
  25. 1
    1
      app/models/account.rb
  26. 2
    2
      app/models/concerns/account_interactions.rb
  27. 1
    0
      app/models/media_attachment.rb
  28. 1
    1
      app/models/notification.rb
  29. 1
    1
      app/models/setting.rb
  30. 5
    5
      app/models/status.rb
  31. 1
    1
      app/models/trending_tags.rb
  32. 1
    1
      app/presenters/instance_presenter.rb
  33. 4
    0
      app/serializers/rest/filter_serializer.rb
  34. 1
    1
      app/services/activitypub/process_account_service.rb
  35. 3
    3
      app/services/batched_remove_status_service.rb
  36. 1
    1
      app/services/concerns/author_extractor.rb
  37. 2
    2
      app/services/fetch_atom_service.rb
  38. 5
    4
      app/services/fetch_link_card_service.rb
  39. 4
    4
      app/services/follow_service.rb
  40. 1
    1
      app/services/process_mentions_service.rb
  41. 21
    11
      app/services/resolve_account_service.rb
  42. 1
    1
      app/services/resolve_url_service.rb
  43. 1
    1
      app/validators/follow_limit_validator.rb
  44. 1
    1
      app/views/accounts/show.html.haml
  45. 1
    1
      app/views/shared/_error_messages.html.haml
  46. 2
    0
      app/workers/activitypub/delivery_worker.rb
  47. 10
    3
      app/workers/local_notification_worker.rb
  48. 3
    0
      config/initializers/json_ld.rb
  49. 3
    1
      config/puma.rb
  50. 50
    0
      lib/json_ld/security.rb
  51. 2
    1
      lib/mastodon/accounts_cli.rb
  52. 9
    4
      lib/mastodon/media_cli.rb
  53. 1
    1
      lib/mastodon/version.rb
  54. 1
    1
      nanobox/nginx-local.conf
  55. 1
    1
      nanobox/nginx-stream.conf.erb
  56. 2
    2
      nanobox/nginx-web.conf.erb
  57. 1
    2
      package.json
  58. 3
    1
      spec/controllers/authorize_interactions_controller_spec.rb
  59. 11
    6
      spec/lib/request_spec.rb
  60. 7
    1
      spec/models/account_spec.rb
  61. 1
    1
      spec/models/notification_spec.rb
  62. 9
    2
      spec/services/fetch_atom_service_spec.rb
  63. 5
    136
      yarn.lock

+ 67
- 0
CHANGELOG.md View File

@@ -3,6 +3,73 @@ Changelog
3 3
 
4 4
 All notable changes to this project will be documented in this file.
5 5
 
6
+## [2.6.5] - 2018-12-01
7
+### Changed
8
+
9
+- Change lists to display replies to others on the list and list owner (#9324)
10
+
11
+### Fixed
12
+
13
+- Fix failures caused by commonly-used JSON-LD contexts being unavailable (#9412)
14
+
15
+## [2.6.4] - 2018-11-30
16
+### Fixed
17
+
18
+- Fix yarn dependencies not installing due to yanked event-stream package (#9401)
19
+
20
+## [2.6.3] - 2018-11-30
21
+### Added
22
+
23
+- Add hyphen to characters allowed in remote usernames (#9345)
24
+
25
+### Changed
26
+
27
+- Change server user count to exclude suspended accounts (#9380)
28
+
29
+### Fixed
30
+
31
+- Fix ffmpeg processing sometimes stalling due to overfilled stdout buffer (#9368)
32
+- Fix missing DNS records raising the wrong kind of exception (#9379)
33
+- Fix already queued deliveries still trying to reach inboxes marked as unavailable (#9358)
34
+
35
+### Security
36
+
37
+- Fix TLS handshake timeout not being enforced (#9381)
38
+
39
+## [2.6.2] - 2018-11-23
40
+### Added
41
+
42
+- Add Page to whitelisted ActivityPub types (#9188)
43
+- Add 20px to column width in web UI (#9227)
44
+- Add amount of freed disk space in `tootctl media remove` (#9229, #9239, #9288)
45
+- Add "Show thread" link to self-replies (#9228)
46
+
47
+### Changed
48
+
49
+- Change order of Atom and RSS links so Atom is first (#9302)
50
+- Change Nginx configuration for Nanobox apps (#9310)
51
+- Change the follow action to appear instant in web UI (#9220)
52
+- Change how the ActiveRecord connection is instantiated in on_worker_boot (#9238)
53
+- Change `tootctl accounts cull` to always touch accounts so they can be skipped (#9293)
54
+- Change mime type comparison to ignore JSON-LD profile (#9179)
55
+
56
+### Fixed
57
+
58
+- Fix web UI crash when conversation has no last status (#9207)
59
+- Fix follow limit validator reporting lower number past threshold (#9230)
60
+- Fix form validation flash message color and input borders (#9235)
61
+- Fix invalid twitter:player cards being displayed (#9254)
62
+- Fix emoji update date being processed incorrectly (#9255)
63
+- Fix playing embed resetting if status is reloaded in web UI (#9270, #9275)
64
+- Fix web UI crash when favouriting a deleted status (#9272)
65
+- Fix intermediary arrays being created for hash maps (#9291)
66
+- Fix filter ID not being a string in REST API (#9303)
67
+
68
+### Security
69
+
70
+- Fix multiple remote account deletions being able to deadlock the database (#9292)
71
+- Fix HTTP connection timeout of 10s not being enforced (#9329)
72
+
6 73
 ## [2.6.1] - 2018-10-30
7 74
 ### Fixed
8 75
 

+ 1
- 0
Gemfile View File

@@ -90,6 +90,7 @@ gem 'webpacker', '~> 3.5'
90 90
 gem 'webpush'
91 91
 
92 92
 gem 'json-ld', '~> 2.2'
93
+gem 'json-ld-preloaded', '~> 2.2'
93 94
 gem 'rdf-normalize', '~> 0.3'
94 95
 
95 96
 group :development, :test do

+ 5
- 0
Gemfile.lock View File

@@ -291,6 +291,10 @@ GEM
291 291
     json-ld (2.2.1)
292 292
       multi_json (~> 1.12)
293 293
       rdf (>= 2.2.8, < 4.0)
294
+    json-ld-preloaded (2.2.3)
295
+      json-ld (>= 2.2, < 4.0)
296
+      multi_json (~> 1.12)
297
+      rdf (>= 2.2, < 4.0)
294 298
     jsonapi-renderer (0.2.0)
295 299
     jwt (2.1.0)
296 300
     kaminari (1.1.1)
@@ -696,6 +700,7 @@ DEPENDENCIES
696 700
   idn-ruby
697 701
   iso-639
698 702
   json-ld (~> 2.2)
703
+  json-ld-preloaded (~> 2.2)
699 704
   kaminari (~> 1.1)
700 705
   letter_opener (~> 1.4)
701 706
   letter_opener_web (~> 1.3)

+ 1
- 1
app/controllers/api/v1/accounts_controller.rb View File

@@ -17,7 +17,7 @@ class Api::V1::AccountsController < Api::BaseController
17 17
   end
18 18
 
19 19
   def follow
20
-    FollowService.new.call(current_user.account, @account.acct, reblogs: truthy_param?(:reblogs))
20
+    FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
21 21
 
22 22
     options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
23 23
 

+ 1
- 1
app/controllers/application_controller.rb View File

@@ -113,7 +113,7 @@ class ApplicationController < ActionController::Base
113 113
     klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
114 114
 
115 115
     unless uncached_ids.empty?
116
-      uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h
116
+      uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item }
117 117
 
118 118
       uncached.each_value do |item|
119 119
         Rails.cache.write(item, item)

+ 14
- 4
app/javascript/mastodon/actions/accounts.js View File

@@ -145,12 +145,14 @@ export function fetchAccountFail(id, error) {
145 145
 export function followAccount(id, reblogs = true) {
146 146
   return (dispatch, getState) => {
147 147
     const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
148
-    dispatch(followAccountRequest(id));
148
+    const locked = getState().getIn(['accounts', id, 'locked'], false);
149
+
150
+    dispatch(followAccountRequest(id, locked));
149 151
 
150 152
     api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
151 153
       dispatch(followAccountSuccess(response.data, alreadyFollowing));
152 154
     }).catch(error => {
153
-      dispatch(followAccountFail(error));
155
+      dispatch(followAccountFail(error, locked));
154 156
     });
155 157
   };
156 158
 };
@@ -167,10 +169,12 @@ export function unfollowAccount(id) {
167 169
   };
168 170
 };
169 171
 
170
-export function followAccountRequest(id) {
172
+export function followAccountRequest(id, locked) {
171 173
   return {
172 174
     type: ACCOUNT_FOLLOW_REQUEST,
173 175
     id,
176
+    locked,
177
+    skipLoading: true,
174 178
   };
175 179
 };
176 180
 
@@ -179,13 +183,16 @@ export function followAccountSuccess(relationship, alreadyFollowing) {
179 183
     type: ACCOUNT_FOLLOW_SUCCESS,
180 184
     relationship,
181 185
     alreadyFollowing,
186
+    skipLoading: true,
182 187
   };
183 188
 };
184 189
 
185
-export function followAccountFail(error) {
190
+export function followAccountFail(error, locked) {
186 191
   return {
187 192
     type: ACCOUNT_FOLLOW_FAIL,
188 193
     error,
194
+    locked,
195
+    skipLoading: true,
189 196
   };
190 197
 };
191 198
 
@@ -193,6 +200,7 @@ export function unfollowAccountRequest(id) {
193 200
   return {
194 201
     type: ACCOUNT_UNFOLLOW_REQUEST,
195 202
     id,
203
+    skipLoading: true,
196 204
   };
197 205
 };
198 206
 
@@ -201,6 +209,7 @@ export function unfollowAccountSuccess(relationship, statuses) {
201 209
     type: ACCOUNT_UNFOLLOW_SUCCESS,
202 210
     relationship,
203 211
     statuses,
212
+    skipLoading: true,
204 213
   };
205 214
 };
206 215
 
@@ -208,6 +217,7 @@ export function unfollowAccountFail(error) {
208 217
   return {
209 218
     type: ACCOUNT_UNFOLLOW_FAIL,
210 219
     error,
220
+    skipLoading: true,
211 221
   };
212 222
 };
213 223
 

+ 8
- 1
app/javascript/mastodon/components/status.js View File

@@ -67,6 +67,7 @@ class Status extends ImmutablePureComponent {
67 67
     unread: PropTypes.bool,
68 68
     onMoveUp: PropTypes.func,
69 69
     onMoveDown: PropTypes.func,
70
+    showThread: PropTypes.bool,
70 71
   };
71 72
 
72 73
   // Avoid checking props that are functions (and whose equality will always
@@ -168,7 +169,7 @@ class Status extends ImmutablePureComponent {
168 169
     let media = null;
169 170
     let statusAvatar, prepend, rebloggedByText;
170 171
 
171
-    const { intl, hidden, featured, otherAccounts, unread } = this.props;
172
+    const { intl, hidden, featured, otherAccounts, unread, showThread } = this.props;
172 173
 
173 174
     let { status, account, ...other } = this.props;
174 175
 
@@ -309,6 +310,12 @@ class Status extends ImmutablePureComponent {
309 310
 
310 311
             {media}
311 312
 
313
+            {showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
314
+              <button className='status__content__read-more-button' onClick={this.handleClick}>
315
+                <FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
316
+              </button>
317
+            )}
318
+
312 319
             <StatusActionBar status={status} account={account} {...other} />
313 320
           </div>
314 321
         </div>

+ 1
- 4
app/javascript/mastodon/components/status_action_bar.js View File

@@ -148,7 +148,6 @@ class StatusActionBar extends ImmutablePureComponent {
148 148
 
149 149
     let menu = [];
150 150
     let reblogIcon = 'retweet';
151
-    let replyIcon;
152 151
     let replyTitle;
153 152
 
154 153
     menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
@@ -191,10 +190,8 @@ class StatusActionBar extends ImmutablePureComponent {
191 190
     }
192 191
 
193 192
     if (status.get('in_reply_to_id', null) === null) {
194
-      replyIcon = 'reply';
195 193
       replyTitle = intl.formatMessage(messages.reply);
196 194
     } else {
197
-      replyIcon = 'reply-all';
198 195
       replyTitle = intl.formatMessage(messages.replyAll);
199 196
     }
200 197
 
@@ -204,7 +201,7 @@ class StatusActionBar extends ImmutablePureComponent {
204 201
 
205 202
     return (
206 203
       <div className='status__action-bar'>
207
-        <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /></div>
204
+        <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon='reply' onClick={this.handleReplyClick} /></div>
208 205
         <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
209 206
         <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='floppy-o' onClick={this.handleFavouriteClick} />
210 207
         {shareButton}

+ 2
- 0
app/javascript/mastodon/components/status_list.js View File

@@ -104,6 +104,7 @@ export default class StatusList extends ImmutablePureComponent {
104 104
           onMoveUp={this.handleMoveUp}
105 105
           onMoveDown={this.handleMoveDown}
106 106
           contextType={timelineId}
107
+          showThread
107 108
         />
108 109
       ))
109 110
     ) : null;
@@ -117,6 +118,7 @@ export default class StatusList extends ImmutablePureComponent {
117 118
           onMoveUp={this.handleMoveUp}
118 119
           onMoveDown={this.handleMoveDown}
119 120
           contextType={timelineId}
121
+          showThread
120 122
         />
121 123
       )).concat(scrollableContent);
122 124
     }

+ 1
- 1
app/javascript/mastodon/features/status/components/action_bar.js View File

@@ -159,7 +159,7 @@ class ActionBar extends React.PureComponent {
159 159
 
160 160
     return (
161 161
       <div className='detailed-status__action-bar'>
162
-        <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
162
+        <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
163 163
         <div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
164 164
         <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='floppy-o' onClick={this.handleFavouriteClick} /></div>
165 165
         {shareButton}

+ 1
- 1
app/javascript/mastodon/features/status/components/card.js View File

@@ -73,7 +73,7 @@ export default class Card extends React.PureComponent {
73 73
   };
74 74
 
75 75
   componentWillReceiveProps (nextProps) {
76
-    if (this.props.card !== nextProps.card) {
76
+    if (!Immutable.is(this.props.card, nextProps.card)) {
77 77
       this.setState({ embedded: false });
78 78
     }
79 79
   }

+ 7
- 1
app/javascript/mastodon/reducers/conversations.js View File

@@ -56,7 +56,13 @@ const expandNormalizedConversations = (state, conversations, next) => {
56 56
 
57 57
         list = list.concat(items);
58 58
 
59
-        return list.sortBy(x => x.get('last_status'), (a, b) => compareId(a, b) * -1);
59
+        return list.sortBy(x => x.get('last_status'), (a, b) => {
60
+          if(a === null || b === null) {
61
+            return -1;
62
+          }
63
+
64
+          return compareId(a, b) * -1;
65
+        });
60 66
       });
61 67
     }
62 68
 

+ 12
- 0
app/javascript/mastodon/reducers/relationships.js View File

@@ -1,6 +1,10 @@
1 1
 import {
2 2
   ACCOUNT_FOLLOW_SUCCESS,
3
+  ACCOUNT_FOLLOW_REQUEST,
4
+  ACCOUNT_FOLLOW_FAIL,
3 5
   ACCOUNT_UNFOLLOW_SUCCESS,
6
+  ACCOUNT_UNFOLLOW_REQUEST,
7
+  ACCOUNT_UNFOLLOW_FAIL,
4 8
   ACCOUNT_BLOCK_SUCCESS,
5 9
   ACCOUNT_UNBLOCK_SUCCESS,
6 10
   ACCOUNT_MUTE_SUCCESS,
@@ -37,6 +41,14 @@ const initialState = ImmutableMap();
37 41
 
38 42
 export default function relationships(state = initialState, action) {
39 43
   switch(action.type) {
44
+  case ACCOUNT_FOLLOW_REQUEST:
45
+    return state.setIn([action.id, action.locked ? 'requested' : 'following'], true);
46
+  case ACCOUNT_FOLLOW_FAIL:
47
+    return state.setIn([action.id, action.locked ? 'requested' : 'following'], false);
48
+  case ACCOUNT_UNFOLLOW_REQUEST:
49
+    return state.setIn([action.id, 'following'], false);
50
+  case ACCOUNT_UNFOLLOW_FAIL:
51
+    return state.setIn([action.id, 'following'], true);
40 52
   case ACCOUNT_FOLLOW_SUCCESS:
41 53
   case ACCOUNT_UNFOLLOW_SUCCESS:
42 54
   case ACCOUNT_BLOCK_SUCCESS:

+ 2
- 2
app/javascript/mastodon/reducers/statuses.js View File

@@ -38,11 +38,11 @@ export default function statuses(state = initialState, action) {
38 38
   case FAVOURITE_REQUEST:
39 39
     return state.setIn([action.status.get('id'), 'favourited'], true);
40 40
   case FAVOURITE_FAIL:
41
-    return state.setIn([action.status.get('id'), 'favourited'], false);
41
+    return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
42 42
   case REBLOG_REQUEST:
43 43
     return state.setIn([action.status.get('id'), 'reblogged'], true);
44 44
   case REBLOG_FAIL:
45
-    return state.setIn([action.status.get('id'), 'reblogged'], false);
45
+    return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], false);
46 46
   case STATUS_MUTE_SUCCESS:
47 47
     return state.setIn([action.id, 'muted'], true);
48 48
   case STATUS_UNMUTE_SUCCESS:

+ 1
- 1
app/javascript/styles/mastodon/components.scss View File

@@ -1846,7 +1846,7 @@ a.account__display-name {
1846 1846
 }
1847 1847
 
1848 1848
 .column {
1849
-  width: 330px;
1849
+  width: 350px;
1850 1850
   position: relative;
1851 1851
   box-sizing: border-box;
1852 1852
   display: flex;

+ 5
- 2
app/javascript/styles/mastodon/forms.scss View File

@@ -330,9 +330,12 @@ code {
330 330
     }
331 331
 
332 332
     input[type=text],
333
+    input[type=number],
333 334
     input[type=email],
334
-    input[type=password] {
335
-      border-bottom-color: $valid-value-color;
335
+    input[type=password],
336
+    textarea,
337
+    select {
338
+      border-color: lighten($error-red, 12%);
336 339
     }
337 340
 
338 341
     .error {

+ 6
- 0
app/lib/activitypub/activity.rb View File

@@ -129,4 +129,10 @@ class ActivityPub::Activity
129 129
       ::FetchRemoteStatusService.new.call(@object['url'])
130 130
     end
131 131
   end
132
+
133
+  def lock_or_return(key, expire_after = 7.days.seconds)
134
+    yield if redis.set(key, true, nx: true, ex: expire_after)
135
+  ensure
136
+    redis.del(key)
137
+  end
132 138
 end

+ 1
- 1
app/lib/activitypub/activity/create.rb View File

@@ -177,7 +177,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
177 177
     updated   = tag['updated']
178 178
     emoji     = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
179 179
 
180
-    return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && emoji.updated_at >= updated)
180
+    return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && updated >= emoji.updated_at)
181 181
 
182 182
     emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
183 183
     emoji.image_remote_url = image_url

+ 4
- 2
app/lib/activitypub/activity/delete.rb View File

@@ -12,8 +12,10 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
12 12
   private
13 13
 
14 14
   def delete_person
15
-    SuspendAccountService.new.call(@account)
16
-    @account.destroy!
15
+    lock_or_return("delete_in_progress:#{@account.id}") do
16
+      SuspendAccountService.new.call(@account)
17
+      @account.destroy!
18
+    end
17 19
   end
18 20
 
19 21
   def delete_note

+ 1
- 1
app/lib/entity_cache.rb View File

@@ -21,7 +21,7 @@ class EntityCache
21 21
     end
22 22
 
23 23
     unless uncached_ids.empty?
24
-      uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain, disabled: false).map { |item| [item.shortcode, item] }.to_h
24
+      uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain, disabled: false).each_with_object({}) { |item, h| h[item.shortcode] = item }
25 25
       uncached.each_value { |item| Rails.cache.write(to_key(:emoji, item.shortcode, domain), item, expires_in: MAX_EXPIRATION) }
26 26
     end
27 27
 

+ 5
- 1
app/lib/feed_manager.rb View File

@@ -40,7 +40,11 @@ class FeedManager
40 40
   end
41 41
 
42 42
   def push_to_list(list, status)
43
-    return false if status.reply? && status.in_reply_to_account_id != status.account_id
43
+    if status.reply? && status.in_reply_to_account_id != status.account_id
44
+      should_filter = status.in_reply_to_account_id != list.account_id
45
+      should_filter &&= !ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?
46
+      return false if should_filter
47
+    end
44 48
     return false unless add_to_feed(:list, list.id, status)
45 49
     trim(:list, list.id)
46 50
     PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")

+ 2
- 2
app/lib/formatter.rb View File

@@ -128,9 +128,9 @@ class Formatter
128 128
     return html if emojis.empty?
129 129
 
130 130
     emoji_map = if animate
131
-                  emojis.map { |e| [e.shortcode, full_asset_url(e.image.url)] }.to_h
131
+                  emojis.each_with_object({}) { |e, h| h[e.shortcode] = full_asset_url(e.image.url) }
132 132
                 else
133
-                  emojis.map { |e| [e.shortcode, full_asset_url(e.image.url(:static))] }.to_h
133
+                  emojis.each_with_object({}) { |e, h| h[e.shortcode] = full_asset_url(e.image.url(:static)) }
134 134
                 end
135 135
 
136 136
     i                     = -1

+ 42
- 10
app/lib/request.rb View File

@@ -2,6 +2,17 @@
2 2
 
3 3
 require 'ipaddr'
4 4
 require 'socket'
5
+require 'resolv'
6
+
7
+# Monkey-patch the HTTP.rb timeout class to avoid using a timeout block
8
+# around the Socket#open method, since we use our own timeout blocks inside
9
+# that method
10
+class HTTP::Timeout::PerOperation
11
+  def connect(socket_class, host, port, nodelay = false)
12
+    @socket = socket_class.open(host, port)
13
+    @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
14
+  end
15
+end
5 16
 
6 17
 class Request
7 18
   REQUEST_TARGET = '(request-target)'
@@ -45,7 +56,7 @@ class Request
45 56
     end
46 57
 
47 58
     begin
48
-      yield response.extend(ClientLimit)
59
+      yield response.extend(ClientLimit) if block_given?
49 60
     ensure
50 61
       http_client.close
51 62
     end
@@ -94,7 +105,11 @@ class Request
94 105
   end
95 106
 
96 107
   def timeout
97
-    { write: 10, connect: 10, read: 10 }
108
+    # We enforce a 1s timeout on DNS resolving, 10s timeout on socket opening
109
+    # and 5s timeout on the TLS handshake, meaning the worst case should take
110
+    # about 16s in total
111
+
112
+    { connect: 5, read: 10, write: 10 }
98 113
   end
99 114
 
100 115
   def http_client
@@ -139,17 +154,34 @@ class Request
139 154
   class Socket < TCPSocket
140 155
     class << self
141 156
       def open(host, *args)
142
-        return super host, *args if thru_hidden_service? host
157
+        return super(host, *args) if thru_hidden_service?(host)
158
+
143 159
         outer_e = nil
144
-        Addrinfo.foreach(host, nil, nil, :SOCK_STREAM) do |address|
145
-          begin
146
-            raise Mastodon::HostValidationError if PrivateAddressCheck.private_address? IPAddr.new(address.ip_address)
147
-            return super address.ip_address, *args
148
-          rescue => e
149
-            outer_e = e
160
+
161
+        Resolv::DNS.open do |dns|
162
+          dns.timeouts = 1
163
+
164
+          addresses = dns.getaddresses(host).take(2)
165
+          time_slot = 10.0 / addresses.size
166
+
167
+          addresses.each do |address|
168
+            begin
169
+              raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s))
170
+
171
+              ::Timeout.timeout(time_slot, HTTP::TimeoutError) do
172
+                return super(address.to_s, *args)
173
+              end
174
+            rescue => e
175
+              outer_e = e
176
+            end
150 177
           end
151 178
         end
152
-        raise outer_e if outer_e
179
+
180
+        if outer_e
181
+          raise outer_e
182
+        else
183
+          raise SocketError, "No address for #{host}"
184
+        end
153 185
       end
154 186
 
155 187
       alias new open

+ 2
- 2
app/lib/settings/scoped_settings.rb View File

@@ -31,7 +31,7 @@ module Settings
31 31
 
32 32
     def all_as_records
33 33
       vars = thing_scoped
34
-      records = vars.map { |r| [r.var, r] }.to_h
34
+      records = vars.each_with_object({}) { |r, h| h[r.var] = r }
35 35
 
36 36
       Setting.default_settings.each do |key, default_value|
37 37
         next if records.key?(key) || default_value.is_a?(Hash)
@@ -65,7 +65,7 @@ module Settings
65 65
 
66 66
     class << self
67 67
       def default_settings
68
-        defaulting = DEFAULTING_TO_UNSCOPED.map { |k| [k, Setting[k]] }.to_h
68
+        defaulting = DEFAULTING_TO_UNSCOPED.each_with_object({}) { |k, h| h[k] = Setting[k] }
69 69
         Setting.default_settings.merge!(defaulting)
70 70
       end
71 71
     end

+ 1
- 1
app/models/account.rb View File

@@ -49,7 +49,7 @@
49 49
 #
50 50
 
51 51
 class Account < ApplicationRecord
52
-  USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.]+[a-z0-9_]+)?/i
52
+  USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
53 53
   MENTION_RE  = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
54 54
 
55 55
   include AccountAvatar

+ 2
- 2
app/models/concerns/account_interactions.rb View File

@@ -45,9 +45,9 @@ module AccountInteractions
45 45
     end
46 46
 
47 47
     def domain_blocking_map(target_account_ids, account_id)
48
-      accounts_map    = Account.where(id: target_account_ids).select('id, domain').map { |a| [a.id, a.domain] }.to_h
48
+      accounts_map    = Account.where(id: target_account_ids).select('id, domain').each_with_object({}) { |a, h| h[a.id] = a.domain }
49 49
       blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id)
50
-      accounts_map.map { |id, domain| [id, blocked_domains[domain]] }.to_h
50
+      accounts_map.reduce({}) { |h, (id, domain)| h.merge(id => blocked_domains[domain]) }
51 51
     end
52 52
 
53 53
     def domain_blocking_map_by_domain(target_domains, account_id)

+ 1
- 0
app/models/media_attachment.rb View File

@@ -59,6 +59,7 @@ class MediaAttachment < ApplicationRecord
59 59
     format: 'mp4',
60 60
     convert_options: {
61 61
       output: {
62
+        'loglevel' => 'fatal',
62 63
         'movflags' => 'faststart',
63 64
         'pix_fmt'  => 'yuv420p',
64 65
         'vf'       => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',

+ 1
- 1
app/models/notification.rb View File

@@ -75,7 +75,7 @@ class Notification < ApplicationRecord
75 75
 
76 76
       return if account_ids.empty?
77 77
 
78
-      accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
78
+      accounts = Account.where(id: account_ids).each_with_object({}) { |a, h| h[a.id] = a }
79 79
 
80 80
       cached_items.each do |item|
81 81
         item.from_account = accounts[item.from_account_id]

+ 1
- 1
app/models/setting.rb View File

@@ -40,7 +40,7 @@ class Setting < RailsSettings::Base
40 40
 
41 41
     def all_as_records
42 42
       vars    = thing_scoped
43
-      records = vars.map { |r| [r.var, r] }.to_h
43
+      records = vars.each_with_object({}) { |r, h| h[r.var] = r }
44 44
 
45 45
       default_settings.each do |key, default_value|
46 46
         next if records.key?(key) || default_value.is_a?(Hash)

+ 5
- 5
app/models/status.rb View File

@@ -310,19 +310,19 @@ class Status < ApplicationRecord
310 310
     end
311 311
 
312 312
     def favourites_map(status_ids, account_id)
313
-      Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |f| [f.status_id, true] }.to_h
313
+      Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true }
314 314
     end
315 315
 
316 316
     def reblogs_map(status_ids, account_id)
317
-      select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).reorder(nil).map { |s| [s.reblog_of_id, true] }.to_h
317
+      select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).reorder(nil).each_with_object({}) { |s, h| h[s.reblog_of_id] = true }
318 318
     end
319 319
 
320 320
     def mutes_map(conversation_ids, account_id)
321
-      ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).map { |m| [m.conversation_id, true] }.to_h
321
+      ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).each_with_object({}) { |m, h| h[m.conversation_id] = true }
322 322
     end
323 323
 
324 324
     def pins_map(status_ids, account_id)
325
-      StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |p| [p.status_id, true] }.to_h
325
+      StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true }
326 326
     end
327 327
 
328 328
     def reload_stale_associations!(cached_items)
@@ -337,7 +337,7 @@ class Status < ApplicationRecord
337 337
 
338 338
       return if account_ids.empty?
339 339
 
340
-      accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
340
+      accounts = Account.where(id: account_ids).each_with_object({}) { |a, h| h[a.id] = a }
341 341
 
342 342
       cached_items.each do |item|
343 343
         item.account = accounts[item.account_id]

+ 1
- 1
app/models/trending_tags.rb View File

@@ -18,7 +18,7 @@ class TrendingTags
18 18
     def get(limit)
19 19
       key     = "#{KEY}:#{Time.now.utc.beginning_of_day.to_i}"
20 20
       tag_ids = redis.zrevrange(key, 0, limit - 1).map(&:to_i)
21
-      tags    = Tag.where(id: tag_ids).to_a.map { |tag| [tag.id, tag] }.to_h
21
+      tags    = Tag.where(id: tag_ids).to_a.each_with_object({}) { |tag, h| h[tag.id] = tag }
22 22
       tag_ids.map { |tag_id| tags[tag_id] }.compact
23 23
     end
24 24
 

+ 1
- 1
app/presenters/instance_presenter.rb View File

@@ -18,7 +18,7 @@ class InstancePresenter
18 18
   end
19 19
 
20 20
   def user_count
21
-    Rails.cache.fetch('user_count') { User.confirmed.count }
21
+    Rails.cache.fetch('user_count') { User.confirmed.joins(:account).merge(Account.without_suspended).count }
22 22
   end
23 23
 
24 24
   def status_count

+ 4
- 0
app/serializers/rest/filter_serializer.rb View File

@@ -3,4 +3,8 @@
3 3
 class REST::FilterSerializer < ActiveModel::Serializer
4 4
   attributes :id, :phrase, :context, :whole_word, :expires_at,
5 5
              :irreversible
6
+
7
+  def id
8
+    object.id.to_s
9
+  end
6 10
 end

+ 1
- 1
app/services/activitypub/process_account_service.rb View File

@@ -232,7 +232,7 @@ class ActivityPub::ProcessAccountService < BaseService
232 232
     updated   = tag['updated']
233 233
     emoji     = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
234 234
 
235
-    return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && emoji.updated_at >= updated)
235
+    return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && updated >= emoji.updated_at)
236 236
 
237 237
     emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
238 238
     emoji.image_remote_url = image_url

+ 3
- 3
app/services/batched_remove_status_service.rb View File

@@ -12,12 +12,12 @@ class BatchedRemoveStatusService < BaseService
12 12
   def call(statuses)
13 13
     statuses = Status.where(id: statuses.map(&:id)).includes(:account, :stream_entry).flat_map { |status| [status] + status.reblogs.includes(:account, :stream_entry).to_a }
14 14
 
15
-    @mentions = statuses.map { |s| [s.id, s.active_mentions.includes(:account).to_a] }.to_h
16
-    @tags     = statuses.map { |s| [s.id, s.tags.pluck(:name)] }.to_h
15
+    @mentions = statuses.each_with_object({}) { |s, h| h[s.id] = s.active_mentions.includes(:account).to_a }
16
+    @tags     = statuses.each_with_object({}) { |s, h| h[s.id] = s.tags.pluck(:name) }
17 17
 
18 18
     @stream_entry_batches  = []
19 19
     @salmon_batches        = []
20
-    @json_payloads         = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id.to_s)] }.to_h
20
+    @json_payloads         = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) }
21 21
     @activity_xml          = {}
22 22
 
23 23
     # Ensure that rendered XML reflects destroyed state

+ 1
- 1
app/services/concerns/author_extractor.rb View File

@@ -18,6 +18,6 @@ module AuthorExtractor
18 18
       acct   = "#{username}@#{domain}"
19 19
     end
20 20
 
21
-    ResolveAccountService.new.call(acct, update_profile)
21
+    ResolveAccountService.new.call(acct, update_profile: update_profile)
22 22
   end
23 23
 end

+ 2
- 2
app/services/fetch_atom_service.rb View File

@@ -29,7 +29,7 @@ class FetchAtomService < BaseService
29 29
 
30 30
   def perform_request(&block)
31 31
     accept = 'text/html'
32
-    accept = 'application/activity+json, application/ld+json, application/atom+xml, ' + accept unless @unsupported_activity
32
+    accept = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/atom+xml, ' + accept unless @unsupported_activity
33 33
 
34 34
     Request.new(:get, @url).add_headers('Accept' => accept).perform(&block)
35 35
   end
@@ -39,7 +39,7 @@ class FetchAtomService < BaseService
39 39
 
40 40
     if response.mime_type == 'application/atom+xml'
41 41
       [@url, { prefetched_body: response.body_with_limit }, :ostatus]
42
-    elsif ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(response.mime_type)
42
+    elsif ['application/activity+json', 'application/ld+json'].include?(response.mime_type)
43 43
       body = response.body_with_limit
44 44
       json = body_to_json(body)
45 45
       if supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && json['inbox'].present?

+ 5
- 4
app/services/fetch_link_card_service.rb View File

@@ -136,14 +136,15 @@ class FetchLinkCardService < BaseService
136 136
     detector = CharlockHolmes::EncodingDetector.new
137 137
     detector.strip_tags = true
138 138
 
139
-    guess = detector.detect(@html, @html_charset)
140
-    page  = Nokogiri::HTML(@html, nil, guess&.fetch(:encoding, nil))
139
+    guess      = detector.detect(@html, @html_charset)
140
+    page       = Nokogiri::HTML(@html, nil, guess&.fetch(:encoding, nil))
141
+    player_url = meta_property(page, 'twitter:player')
141 142
 
142
-    if meta_property(page, 'twitter:player')
143
+    if player_url && !bad_url?(Addressable::URI.parse(player_url))
143 144
       @card.type   = :video
144 145
       @card.width  = meta_property(page, 'twitter:player:width') || 0
145 146
       @card.height = meta_property(page, 'twitter:player:height') || 0
146
-      @card.html   = content_tag(:iframe, nil, src: meta_property(page, 'twitter:player'),
147
+      @card.html   = content_tag(:iframe, nil, src: player_url,
147 148
                                                width: @card.width,
148 149
                                                height: @card.height,
149 150
                                                allowtransparency: 'true',

+ 4
- 4
app/services/follow_service.rb View File

@@ -7,9 +7,9 @@ class FollowService < BaseService
7 7
   # @param [Account] source_account From which to follow
8 8
   # @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
9 9
   # @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true
10
-  def call(source_account, uri, reblogs: nil)
10
+  def call(source_account, target_account, reblogs: nil)
11 11
     reblogs = true if reblogs.nil?
12
-    target_account = uri.is_a?(Account) ? uri : ResolveAccountService.new.call(uri)
12
+    target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
13 13
 
14 14
     raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
15 15
     raise Mastodon::NotPermittedError  if target_account.blocking?(source_account) || source_account.blocking?(target_account)
@@ -42,7 +42,7 @@ class FollowService < BaseService
42 42
     follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs)
43 43
 
44 44
     if target_account.local?
45
-      NotifyService.new.call(target_account, follow_request)
45
+      LocalNotificationWorker.perform_async(target_account.id, follow_request.id, follow_request.class.name)
46 46
     elsif target_account.ostatus?
47 47
       NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id)
48 48
       AfterRemoteFollowRequestWorker.perform_async(follow_request.id)
@@ -57,7 +57,7 @@ class FollowService < BaseService
57 57
     follow = source_account.follow!(target_account, reblogs: reblogs)
58 58
 
59 59
     if target_account.local?
60
-      NotifyService.new.call(target_account, follow)
60
+      LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name)
61 61
     else
62 62
       Pubsubhubbub::SubscribeWorker.perform_async(target_account.id) unless target_account.subscribed?
63 63
       NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id)

+ 1
- 1
app/services/process_mentions_service.rb View File

@@ -47,7 +47,7 @@ class ProcessMentionsService < BaseService
47 47
     mentioned_account = mention.account
48 48
 
49 49
     if mentioned_account.local?
50
-      LocalNotificationWorker.perform_async(mention.id)
50
+      LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name)
51 51
     elsif mentioned_account.ostatus? && !@status.stream_entry.hidden?
52 52
       NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id)
53 53
     elsif mentioned_account.activitypub?

+ 21
- 11
app/services/resolve_account_service.rb View File

@@ -9,17 +9,27 @@ class ResolveAccountService < BaseService
9 9
   # Find or create a local account for a remote user.
10 10
   # When creating, look up the user's webfinger and fetch all
11 11
   # important information from their feed
12
-  # @param [String] uri User URI in the form of username@domain
12
+  # @param [String, Account] uri User URI in the form of username@domain
13
+  # @param [Hash] options
13 14
   # @return [Account]
14
-  def call(uri, update_profile = true, redirected = nil)
15
-    @username, @domain = uri.split('@')
16
-    @update_profile    = update_profile
15
+  def call(uri, options = {})
16
+    @options = options
17 17
 
18
-    return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
18
+    if uri.is_a?(Account)
19
+      @account  = uri
20
+      @username = @account.username
21
+      @domain   = @account.domain
22
+
23
+      return @account if @account.local? || !webfinger_update_due?
24
+    else
25
+      @username, @domain = uri.split('@')
19 26
 
20
-    @account = Account.find_remote(@username, @domain)
27
+      return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
21 28
 
22
-    return @account unless webfinger_update_due?
29
+      @account = Account.find_remote(@username, @domain)
30
+
31
+      return @account unless webfinger_update_due?
32
+    end
23 33
 
24 34
     Rails.logger.debug "Looking up webfinger for #{uri}"
25 35
 
@@ -30,8 +40,8 @@ class ResolveAccountService < BaseService
30 40
     if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
31 41
       @username = confirmed_username
32 42
       @domain   = confirmed_domain
33
-    elsif redirected.nil?
34
-      return call("#{confirmed_username}@#{confirmed_domain}", update_profile, true)
43
+    elsif options[:redirected].nil?
44
+      return call("#{confirmed_username}@#{confirmed_domain}", options.merge(redirected: true))
35 45
     else
36 46
       Rails.logger.debug 'Requested and returned acct URIs do not match'
37 47
       return
@@ -76,7 +86,7 @@ class ResolveAccountService < BaseService
76 86
   end
77 87
 
78 88
   def webfinger_update_due?
79
-    @account.nil? || @account.possibly_stale?
89
+    @account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?)
80 90
   end
81 91
 
82 92
   def activitypub_ready?
@@ -93,7 +103,7 @@ class ResolveAccountService < BaseService
93 103
   end
94 104
 
95 105
   def update_profile?
96
-    @update_profile
106
+    @options[:update_profile]
97 107
   end
98 108
 
99 109
   def handle_activitypub

+ 1
- 1
app/services/resolve_url_service.rb View File

@@ -20,7 +20,7 @@ class ResolveURLService < BaseService
20 20
   def process_url
21 21
     if equals_or_includes_any?(type, %w(Application Group Organization Person Service))
22 22
       FetchRemoteAccountService.new.call(atom_url, body, protocol)
23
-    elsif equals_or_includes_any?(type, %w(Note Article Image Video))
23
+    elsif equals_or_includes_any?(type, %w(Note Article Image Video Page))
24 24
       FetchRemoteStatusService.new.call(atom_url, body, protocol)
25 25
     end
26 26
   end

+ 1
- 1
app/validators/follow_limit_validator.rb View File

@@ -14,7 +14,7 @@ class FollowLimitValidator < ActiveModel::Validator
14 14
       if account.following_count < LIMIT
15 15
         LIMIT
16 16
       else
17
-        account.followers_count * RATIO
17
+        [(account.followers_count * RATIO).round, LIMIT].max
18 18
       end
19 19
     end
20 20
   end

+ 1
- 1
app/views/accounts/show.html.haml View File

@@ -8,8 +8,8 @@
8 8
     %meta{ name: 'robots', content: 'noindex' }/
9 9
 
10 10
   %link{ rel: 'salmon', href: api_salmon_url(@account.id) }/
11
-  %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/
12 11
   %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/
12
+  %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/
13 13
   %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
14 14
 
15 15
   - if @older_url

+ 1
- 1
app/views/shared/_error_messages.html.haml View File

@@ -1,3 +1,3 @@
1 1
 - if object.errors.any?
2
-  .flash-message#error_explanation
2
+  .flash-message.alert#error_explanation
3 3
     %strong= t('generic.validation_errors', count: object.errors.count)

+ 2
- 0
app/workers/activitypub/delivery_worker.rb View File

@@ -11,6 +11,8 @@ class ActivityPub::DeliveryWorker
11 11
   HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
12 12
 
13 13
   def perform(json, source_account_id, inbox_url, options = {})
14
+    return if DeliveryFailureTracker.unavailable?(inbox_url)
15
+
14 16
     @options        = options.with_indifferent_access
15 17
     @json           = json
16 18
     @source_account = Account.find(source_account_id)

+ 10
- 3
app/workers/local_notification_worker.rb View File

@@ -3,9 +3,16 @@
3 3
 class LocalNotificationWorker
4 4
   include Sidekiq::Worker
5 5
 
6
-  def perform(mention_id)
7
-    mention = Mention.find(mention_id)
8
-    NotifyService.new.call(mention.account, mention)
6
+  def perform(receiver_account_id, activity_id = nil, activity_class_name = nil)
7
+    if activity_id.nil? && activity_class_name.nil?
8
+      activity = Mention.find(receiver_account_id)
9
+      receiver = activity.account
10
+    else
11
+      receiver = Account.find(receiver_account_id)
12
+      activity = activity_class_name.constantize.find(activity_id)
13
+    end
14
+
15
+    NotifyService.new.call(receiver, activity)
9 16
   rescue ActiveRecord::RecordNotFound
10 17
     true
11 18
   end

+ 3
- 0
config/initializers/json_ld.rb View File

@@ -0,0 +1,3 @@
1
+# frozen_string_literal: true
2
+
3
+require_relative '../../lib/json_ld/security'

+ 3
- 1
config/puma.rb View File

@@ -13,7 +13,9 @@ workers     ENV.fetch('WEB_CONCURRENCY') { 2 }
13 13
 preload_app!
14 14
 
15 15
 on_worker_boot do
16
-  ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
16
+  ActiveSupport.on_load(:active_record) do
17
+    ActiveRecord::Base.establish_connection
18
+  end
17 19
 end
18 20
 
19 21
 plugin :tmp_restart

+ 50
- 0
lib/json_ld/security.rb View File

@@ -0,0 +1,50 @@
1
+# -*- encoding: utf-8 -*-
2
+# frozen_string_literal: true
3
+# This file generated automatically from https://w3id.org/security/v1
4
+require 'json/ld'
5
+class JSON::LD::Context
6
+  add_preloaded("https://w3id.org/security/v1") do
7
+    new(processingMode: "json-ld-1.0", term_definitions: {
8
+      "CryptographicKey" => TermDefinition.new("CryptographicKey", id: "https://w3id.org/security#Key", simple: true),
9
+      "EcdsaKoblitzSignature2016" => TermDefinition.new("EcdsaKoblitzSignature2016", id: "https://w3id.org/security#EcdsaKoblitzSignature2016", simple: true),
10
+      "EncryptedMessage" => TermDefinition.new("EncryptedMessage", id: "https://w3id.org/security#EncryptedMessage", simple: true),
11
+      "GraphSignature2012" => TermDefinition.new("GraphSignature2012", id: "https://w3id.org/security#GraphSignature2012", simple: true),
12
+      "LinkedDataSignature2015" => TermDefinition.new("LinkedDataSignature2015", id: "https://w3id.org/security#LinkedDataSignature2015", simple: true),
13
+      "LinkedDataSignature2016" => TermDefinition.new("LinkedDataSignature2016", id: "https://w3id.org/security#LinkedDataSignature2016", simple: true),
14
+      "authenticationTag" => TermDefinition.new("authenticationTag", id: "https://w3id.org/security#authenticationTag", simple: true),
15
+      "canonicalizationAlgorithm" => TermDefinition.new("canonicalizationAlgorithm", id: "https://w3id.org/security#canonicalizationAlgorithm", simple: true),
16
+      "cipherAlgorithm" => TermDefinition.new("cipherAlgorithm", id: "https://w3id.org/security#cipherAlgorithm", simple: true),
17
+      "cipherData" => TermDefinition.new("cipherData", id: "https://w3id.org/security#cipherData", simple: true),
18
+      "cipherKey" => TermDefinition.new("cipherKey", id: "https://w3id.org/security#cipherKey", simple: true),
19
+      "created" => TermDefinition.new("created", id: "http://purl.org/dc/terms/created", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
20
+      "creator" => TermDefinition.new("creator", id: "http://purl.org/dc/terms/creator", type_mapping: "@id"),
21
+      "dc" => TermDefinition.new("dc", id: "http://purl.org/dc/terms/", simple: true, prefix: true),
22
+      "digestAlgorithm" => TermDefinition.new("digestAlgorithm", id: "https://w3id.org/security#digestAlgorithm", simple: true),
23
+      "digestValue" => TermDefinition.new("digestValue", id: "https://w3id.org/security#digestValue", simple: true),
24
+      "domain" => TermDefinition.new("domain", id: "https://w3id.org/security#domain", simple: true),
25
+      "encryptionKey" => TermDefinition.new("encryptionKey", id: "https://w3id.org/security#encryptionKey", simple: true),
26
+      "expiration" => TermDefinition.new("expiration", id: "https://w3id.org/security#expiration", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
27
+      "expires" => TermDefinition.new("expires", id: "https://w3id.org/security#expiration", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
28
+      "id" => TermDefinition.new("id", id: "@id", simple: true),
29
+      "initializationVector" => TermDefinition.new("initializationVector", id: "https://w3id.org/security#initializationVector", simple: true),
30
+      "iterationCount" => TermDefinition.new("iterationCount", id: "https://w3id.org/security#iterationCount", simple: true),
31
+      "nonce" => TermDefinition.new("nonce", id: "https://w3id.org/security#nonce", simple: true),
32
+      "normalizationAlgorithm" => TermDefinition.new("normalizationAlgorithm", id: "https://w3id.org/security#normalizationAlgorithm", simple: true),
33
+      "owner" => TermDefinition.new("owner", id: "https://w3id.org/security#owner", type_mapping: "@id"),
34
+      "password" => TermDefinition.new("password", id: "https://w3id.org/security#password", simple: true),
35
+      "privateKey" => TermDefinition.new("privateKey", id: "https://w3id.org/security#privateKey", type_mapping: "@id"),
36
+      "privateKeyPem" => TermDefinition.new("privateKeyPem", id: "https://w3id.org/security#privateKeyPem", simple: true),
37
+      "publicKey" => TermDefinition.new("publicKey", id: "https://w3id.org/security#publicKey", type_mapping: "@id"),
38
+      "publicKeyPem" => TermDefinition.new("publicKeyPem", id: "https://w3id.org/security#publicKeyPem", simple: true),
39
+      "publicKeyService" => TermDefinition.new("publicKeyService", id: "https://w3id.org/security#publicKeyService", type_mapping: "@id"),
40
+      "revoked" => TermDefinition.new("revoked", id: "https://w3id.org/security#revoked", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
41
+      "salt" => TermDefinition.new("salt", id: "https://w3id.org/security#salt", simple: true),
42
+      "sec" => TermDefinition.new("sec", id: "https://w3id.org/security#", simple: true, prefix: true),
43
+      "signature" => TermDefinition.new("signature", id: "https://w3id.org/security#signature", simple: true),
44
+      "signatureAlgorithm" => TermDefinition.new("signatureAlgorithm", id: "https://w3id.org/security#signingAlgorithm", simple: true),
45
+      "signatureValue" => TermDefinition.new("signatureValue", id: "https://w3id.org/security#signatureValue", simple: true),
46
+      "type" => TermDefinition.new("type", id: "@type", simple: true),
47
+      "xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true, prefix: true)
48
+    })
49
+  end
50
+end

+ 2
- 1
lib/mastodon/accounts_cli.rb View File

@@ -242,8 +242,9 @@ module Mastodon
242 242
           end
243 243
 
244 244
           culled += 1
245
-          say('.', :green, false)
245
+          say('+', :green, false)
246 246
         else
247
+          account.touch # Touch account even during dry run to avoid getting the account into the window again
247 248
           say('.', nil, false)
248 249
         end
249 250
       end

+ 9
- 4
lib/mastodon/media_cli.rb View File

@@ -6,6 +6,8 @@ require_relative 'cli_helper'
6 6
 
7 7
 module Mastodon
8 8
   class MediaCLI < Thor
9
+    include ActionView::Helpers::NumberHelper
10
+
9 11
     def self.exit_on_failure?
10 12
       true
11 13
     end
@@ -36,16 +38,19 @@ module Mastodon
36 38
       time_ago  = options[:days].days.ago
37 39
       queued    = 0
38 40
       processed = 0
39
-      dry_run = options[:dry_run] ? '(DRY RUN)' : ''
41
+      size      = 0
42
+      dry_run   = options[:dry_run] ? '(DRY RUN)' : ''
40 43
 
41 44
       if options[:background]
42
-        MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).select(:id).reorder(nil).find_in_batches do |media_attachments|
45
+        MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).select(:id, :file_file_size).reorder(nil).find_in_batches do |media_attachments|
43 46
           queued += media_attachments.size
47
+          size   += media_attachments.reduce(0) { |sum, m| sum + (m.file_file_size || 0) }
44 48
           Maintenance::UncacheMediaWorker.push_bulk(media_attachments.map(&:id)) unless options[:dry_run]
45 49
         end
46 50
       else
47 51
         MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).reorder(nil).find_in_batches do |media_attachments|
48 52
           media_attachments.each do |m|
53
+            size += m.file_file_size || 0
49 54
             Maintenance::UncacheMediaWorker.new.perform(m) unless options[:dry_run]
50 55
             options[:verbose] ? say(m.id) : say('.', :green, false)
51 56
             processed += 1
@@ -56,9 +61,9 @@ module Mastodon
56 61
       say
57 62
 
58 63
       if options[:background]
59
-        say("Scheduled the deletion of #{queued} media attachments #{dry_run}", :green, true)
64
+        say("Scheduled the deletion of #{queued} media attachments (approx. #{number_to_human_size(size)}) #{dry_run}", :green, true)
60 65
       else
61
-        say("Removed #{processed} media attachments #{dry_run}", :green, true)
66
+        say("Removed #{processed} media attachments (approx. #{number_to_human_size(size)}) #{dry_run}", :green, true)
62 67
       end
63 68
     end
64 69
   end

+ 1
- 1
lib/mastodon/version.rb View File

@@ -13,7 +13,7 @@ module Mastodon
13 13
     end
14 14
 
15 15
     def patch
16
-      1
16
+      5
17 17
     end
18 18
 
19 19
     def pre

+ 1
- 1
nanobox/nginx-local.conf View File

@@ -38,7 +38,7 @@ http {
38 38
 
39 39
         root /app/public;
40 40
 
41
-        client_max_body_size 8M;
41
+        client_max_body_size 80M;
42 42
 
43 43
         location / {
44 44
             try_files $uri @rails;

+ 1
- 1
nanobox/nginx-stream.conf.erb View File

@@ -32,7 +32,7 @@ http {
32 32
         listen 8080;
33 33
 
34 34
         add_header Strict-Transport-Security "max-age=31536000";
35
-        add_header Content-Security-Policy "style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'self'; img-src data: https:; media-src data: https:; connect-src 'self' wss://<%= ENV["LOCAL_DOMAIN"] %>; upgrade-insecure-requests";
35
+        # add_header Content-Security-Policy "style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'self'; img-src data: https:; media-src data: https:; connect-src 'self' wss://<%= ENV["LOCAL_DOMAIN"] %>; upgrade-insecure-requests";
36 36
 
37 37
         root /app/public;
38 38
 

+ 2
- 2
nanobox/nginx-web.conf.erb View File

@@ -32,11 +32,11 @@ http {
32 32
         listen 8080;
33 33
 
34 34
         add_header Strict-Transport-Security "max-age=31536000";
35
-        add_header Content-Security-Policy "style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'self'; img-src data: https:; media-src data: https:; connect-src 'self' wss://<%= ENV["LOCAL_DOMAIN"] %>; upgrade-insecure-requests";
35
+        # add_header Content-Security-Policy "style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'self'; img-src data: https:; media-src data: https:; connect-src 'self' wss://<%= ENV["LOCAL_DOMAIN"] %>; upgrade-insecure-requests";
36 36
 
37 37
         root /app/public;
38 38
 
39
-        client_max_body_size 8M;
39
+        client_max_body_size 80M;
40 40
 
41 41
         location / {
42 42
             try_files $uri @rails;

+ 1
- 2
package.json View File

@@ -10,7 +10,7 @@
10 10
     "build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack",
11 11
     "manage:translations": "node ./config/webpack/translationRunner.js",
12 12
     "start": "node ./streaming/index.js",
13
-    "test": "npm-run-all test:lint test:jest",
13
+    "test": "npm run test:lint && npm run test:jest",
14 14
     "test:lint": "eslint -c .eslintrc.yml --ext=js app/javascript/ config/webpack/ streaming/",
15 15
     "test:jest": "cross-env NODE_ENV=test jest --coverage"
16 16
   },
@@ -77,7 +77,6 @@
77 77
     "mini-css-extract-plugin": "^0.4.2",
78 78
     "mkdirp": "^0.5.1",
79 79
     "node-sass": "^4.9.2",
80
-    "npm-run-all": "^4.1.2",
81 80
     "npmlog": "^4.1.2",
82 81
     "object-assign": "^4.1.1",
83 82
     "object-fit-images": "^3.2.3",

+ 3
- 1
spec/controllers/authorize_interactions_controller_spec.rb View File

@@ -99,10 +99,12 @@ describe AuthorizeInteractionsController do
99 99
 
100 100
         allow(ResolveAccountService).to receive(:new).and_return(service)
101 101
         allow(service).to receive(:call).with('user@hostname').and_return(target_account)
102
+        allow(service).to receive(:call).with(target_account, skip_webfinger: true).and_return(target_account)
103
+
102 104
 
103 105
         post :create, params: { acct: 'acct:user@hostname' }
104 106
 
105
-        expect(service).to have_received(:call).with('user@hostname')
107
+        expect(service).to have_received(:call).with(target_account, skip_webfinger: true)
106 108
         expect(account.following?(target_account)).to be true
107 109
         expect(response).to render_template(:success)
108 110
       end

+ 11
- 6
spec/lib/request_spec.rb View File

@@ -48,9 +48,11 @@ describe Request do
48 48
       end
49 49
 
50 50
       it 'executes a HTTP request when the first address is private' do
51
-        allow(Addrinfo).to receive(:foreach).with('example.com', nil, nil, :SOCK_STREAM)
52
-                                            .and_yield(Addrinfo.new(["AF_INET", 0, "example.com", "0.0.0.0"], :PF_INET, :SOCK_STREAM))
53
-                                            .and_yield(Addrinfo.new(["AF_INET6", 0, "example.com", "2001:4860:4860::8844"], :PF_INET6, :SOCK_STREAM))
51
+        resolver = double
52
+
53
+        allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:4860:4860::8844))
54
+        allow(resolver).to receive(:timeouts=).and_return(nil)
55
+        allow(Resolv::DNS).to receive(:open).and_yield(resolver)
54 56
 
55 57
         expect { |block| subject.perform &block }.to yield_control
56 58
         expect(a_request(:get, 'http://example.com')).to have_been_made.once
@@ -81,9 +83,12 @@ describe Request do
81 83
       end
82 84
 
83 85
       it 'raises Mastodon::ValidationError' do
84
-        allow(Addrinfo).to receive(:foreach).with('example.com', nil, nil, :SOCK_STREAM)
85
-                                            .and_yield(Addrinfo.new(["AF_INET", 0, "example.com", "0.0.0.0"], :PF_INET, :SOCK_STREAM))
86
-                                            .and_yield(Addrinfo.new(["AF_INET6", 0, "example.com", "2001:db8::face"], :PF_INET6, :SOCK_STREAM))
86
+        resolver = double
87
+
88
+        allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:db8::face))
89
+        allow(resolver).to receive(:timeouts=).and_return(nil)
90
+        allow(Resolv::DNS).to receive(:open).and_yield(resolver)
91
+
87 92
         expect { subject.perform }.to raise_error Mastodon::ValidationError
88 93
       end
89 94
     end

+ 7
- 1
spec/models/account_spec.rb View File

@@ -618,9 +618,15 @@ RSpec.describe Account, type: :model do
618 618
         expect(account).not_to model_have_error_on_field(:username)
619 619
       end
620 620
 
621
-      it 'is invalid if the username doesn\'t only contains letters, numbers and underscores' do
621
+      it 'is valid even if the username contains hyphens' do
622 622
         account = Fabricate.build(:account, domain: 'domain', username: 'the-doctor')
623 623
         account.valid?
624
+        expect(account).to_not model_have_error_on_field(:username)
625
+      end
626
+
627
+      it 'is invalid if the username doesn\'t only contains letters, numbers, underscores and hyphens' do
628
+        account = Fabricate.build(:account, domain: 'domain', username: 'the doctor')
629
+        account.valid?
624 630
         expect(account).to model_have_error_on_field(:username)
625 631
       end
626 632
 

+ 1
- 1
spec/models/notification_spec.rb View File

@@ -101,7 +101,7 @@ RSpec.describe Notification, type: :model do
101 101
       before do
102 102
         allow(accounts_with_ids).to receive(:[]).with(stale_account1.id).and_return(account1)
103 103
         allow(accounts_with_ids).to receive(:[]).with(stale_account2.id).and_return(account2)
104
-        allow(Account).to receive_message_chain(:where, :map, :to_h).and_return(accounts_with_ids)
104
+        allow(Account).to receive_message_chain(:where, :each_with_object).and_return(accounts_with_ids)
105 105
       end
106 106
 
107 107
       let(:cached_items) do

+ 9
- 2
spec/services/fetch_atom_service_spec.rb View File

@@ -60,8 +60,15 @@ RSpec.describe FetchAtomService, type: :service do
60 60
         it { is_expected.to eq [url, { :prefetched_body => "" }, :ostatus] }
61 61
       end
62 62
 
63
-      context 'content_type is json' do
64
-        let(:content_type) { 'application/activity+json' }
63
+      context 'content_type is activity+json' do
64
+        let(:content_type) { 'application/activity+json; charset=utf-8' }
65
+        let(:body) { json }
66
+
67
+        it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] }
68
+      end
69
+
70
+      context 'content_type is ld+json with profile' do
71
+        let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }
65 72
         let(:body) { json }
66 73
 
67 74
         it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] }

+ 5
- 136
yarn.lock View File

@@ -1109,11 +1109,6 @@ array-equal@^1.0.0:
1109 1109
   resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
1110 1110
   integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
1111 1111
 
1112
-array-filter@~0.0.0:
1113
-  version "0.0.1"
1114
-  resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
1115
-  integrity sha1-fajPLiZijtcygDWB/SH2fKzS7uw=
1116
-
1117 1112
 array-find-index@^1.0.1:
1118 1113
   version "1.0.2"
1119 1114
   resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
@@ -1137,16 +1132,6 @@ array-includes@^3.0.3:
1137 1132
     define-properties "^1.1.2"
1138 1133
     es-abstract "^1.7.0"
1139 1134
 
1140
-array-map@~0.0.0:
1141
-  version "0.0.0"
1142
-  resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
1143
-  integrity sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=
1144
-
1145
-array-reduce@~0.0.0:
1146
-  version "0.0.0"
1147
-  resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b"
1148
-  integrity sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=
1149
-
1150 1135
 array-union@^1.0.1:
1151 1136
   version "1.0.2"
1152 1137
   resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
@@ -2415,7 +2400,7 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0:
2415 2400
     shebang-command "^1.2.0"
2416 2401
     which "^1.2.9"
2417 2402
 
2418
-cross-spawn@^6.0.0, cross-spawn@^6.0.4, cross-spawn@^6.0.5:
2403
+cross-spawn@^6.0.0, cross-spawn@^6.0.5:
2419 2404
   version "6.0.5"
2420 2405
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
2421 2406
   integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
@@ -2924,7 +2909,7 @@ double-ended-queue@^2.1.0-0:
2924 2909
   resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
2925 2910
   integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=
2926 2911
 
2927
-duplexer@^0.1.1, duplexer@~0.1.1:
2912
+duplexer@^0.1.1:
2928 2913
   version "0.1.1"
2929 2914
   resolved "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
2930 2915
   integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=
@@ -3083,7 +3068,7 @@ error-ex@^1.2.0, error-ex@^1.3.1:
3083 3068
   dependencies:
3084 3069
     is-arrayish "^0.2.1"
3085 3070
 
3086
-es-abstract@^1.10.0, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
3071
+es-abstract@^1.10.0, es-abstract@^1.5.0, es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
3087 3072
   version "1.12.0"
3088 3073
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
3089 3074
   integrity sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==
@@ -3333,20 +3318,6 @@ etag@~1.8.1:
3333 3318
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
3334 3319
   integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
3335 3320
 
3336
-event-stream@~3.3.0:
3337
-  version "3.3.6"
3338
-  resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.6.tgz#cac1230890e07e73ec9cacd038f60a5b66173eef"
3339
-  integrity sha512-dGXNg4F/FgVzlApjzItL+7naHutA3fDqbV/zAZqDDlXTjiMnQmZKu+prImWKszeBM5UQeGvAl3u1wBiKeDh61g==
3340
-  dependencies:
3341
-    duplexer "^0.1.1"
3342
-    flatmap-stream "^0.1.0"
3343
-    from "^0.1.7"
3344
-    map-stream "0.0.7"
3345
-    pause-stream "^0.0.11"
3346
-    split "^1.0.1"
3347
-    stream-combiner "^0.2.2"
3348
-    through "^2.3.8"
3349
-
3350 3321
 eventemitter3@^3.0.0:
3351 3322
   version "3.1.0"
3352 3323
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
@@ -3748,11 +3719,6 @@ flat-cache@^1.2.1:
3748 3719
     graceful-fs "^4.1.2"
3749 3720
     write "^0.2.1"
3750 3721
 
3751
-flatmap-stream@^0.1.0:
3752
-  version "0.1.0"
3753
-  resolved "https://registry.yarnpkg.com/flatmap-stream/-/flatmap-stream-0.1.0.tgz#ed54e01422cd29281800914fcb968d58b685d5f1"
3754
-  integrity sha512-Nlic4ZRYxikqnK5rj3YoxDVKGGtUjcNDUtvQ7XsdGLZmMwdUYnXf10o1zcXtzEZTBgc6GxeRpQxV/Wu3WPIIHA==
3755
-
3756 3722
 flatten@^1.0.2:
3757 3723
   version "1.0.2"
3758 3724
   resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
@@ -3850,11 +3816,6 @@ from2@^2.1.0:
3850 3816
     inherits "^2.0.1"
3851 3817
     readable-stream "^2.0.0"
3852 3818
 
3853
-from@^0.1.7:
3854
-  version "0.1.7"
3855
-  resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
3856
-  integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=
3857
-
3858 3819
 fs-extra@^7.0.0:
3859 3820
   version "7.0.0"
3860 3821
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.0.tgz#8cc3f47ce07ef7b3593a11b9fb245f7e34c041d6"
@@ -5646,16 +5607,6 @@ load-json-file@^2.0.0:
5646 5607
     pify "^2.0.0"
5647 5608
     strip-bom "^3.0.0"
5648 5609
 
5649
-load-json-file@^4.0.0:
5650
-  version "4.0.0"
5651
-  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
5652
-  integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
5653
-  dependencies:
5654
-    graceful-fs "^4.1.2"
5655
-    parse-json "^4.0.0"
5656
-    pify "^3.0.0"
5657
-    strip-bom "^3.0.0"
5658
-
5659 5610
 loader-runner@^2.3.0:
5660 5611
   version "2.3.0"
5661 5612
   resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
@@ -5845,11 +5796,6 @@ map-obj@^1.0.0, map-obj@^1.0.1:
5845 5796
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
5846 5797
   integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
5847 5798
 
5848
-map-stream@0.0.7:
5849
-  version "0.0.7"
5850
-  resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8"
5851
-  integrity sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=
5852
-
5853 5799
 map-visit@^1.0.0:
5854 5800
   version "1.0.0"
5855 5801
   resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@@ -5909,11 +5855,6 @@ memory-fs@^0.4.0, memory-fs@~0.4.1:
5909 5855
     errno "^0.1.3"
5910 5856
     readable-stream "^2.0.1"
5911 5857
 
5912
-memorystream@^0.3.1:
5913
-  version "0.3.1"
5914
-  resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
5915
-  integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
5916
-
5917 5858
 meow@^3.7.0:
5918 5859
   version "3.7.0"
5919 5860
   resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
@@ -6466,21 +6407,6 @@ npm-packlist@^1.1.6:
6466 6407
     ignore-walk "^3.0.1"
6467 6408
     npm-bundled "^1.0.1"
6468 6409
 
6469
-npm-run-all@^4.1.2:
6470
-  version "4.1.3"
6471
-  resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.3.tgz#49f15b55a66bb4101664ce270cb18e7103f8f185"
6472
-  integrity sha512-aOG0N3Eo/WW+q6sUIdzcV2COS8VnTZCmdji0VQIAZF3b+a3YWb0AD0vFIyjKec18A7beLGbaQ5jFTNI2bPt9Cg==
6473
-  dependencies:
6474
-    ansi-styles "^3.2.0"
6475
-    chalk "^2.1.0"
6476
-    cross-spawn "^6.0.4"
6477
-    memorystream "^0.3.1"
6478
-    minimatch "^3.0.4"
6479
-    ps-tree "^1.1.0"
6480
-    read-pkg "^3.0.0"
6481
-    shell-quote "^1.6.1"
6482
-    string.prototype.padend "^3.0.0"
6483
-
6484 6410
 npm-run-path@^2.0.0:
6485 6411
   version "2.0.2"
6486 6412
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -6983,20 +6909,6 @@ path-type@^2.0.0:
6983 6909
   dependencies:
6984 6910
     pify "^2.0.0"
6985 6911
 
6986
-path-type@^3.0.0:
6987
-  version "3.0.0"
6988
-  resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
6989
-  integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
6990
-  dependencies:
6991
-    pify "^3.0.0"
6992
-
6993
-pause-stream@^0.0.11:
6994
-  version "0.0.11"
6995
-  resolved "http://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
6996
-  integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=
6997
-  dependencies:
6998
-    through "~2.3"
6999
-
7000 6912
 pbkdf2@^3.0.3:
7001 6913
   version "3.0.16"
7002 6914
   resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c"
@@ -7667,13 +7579,6 @@ prr@~1.0.1:
7667 7579
   resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
7668 7580
   integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
7669 7581
 
7670
-ps-tree@^1.1.0:
7671
-  version "1.1.0"
7672
-  resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014"
7673
-  integrity sha1-tCGyQUDWID8e08dplrRCewjowBQ=
7674
-  dependencies:
7675
-    event-stream "~3.3.0"
7676
-
7677 7582
 pseudomap@^1.0.2:
7678 7583
   version "1.0.2"
7679 7584
   resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
@@ -8119,15 +8024,6 @@ read-pkg@^2.0.0:
8119 8024
     normalize-package-data "^2.3.2"
8120 8025
     path-type "^2.0.0"
8121 8026
 
8122
-read-pkg@^3.0.0:
8123
-  version "3.0.0"
8124
-  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
8125
-  integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
8126
-  dependencies:
8127
-    load-json-file "^4.0.0"
8128
-    normalize-package-data "^2.3.2"
8129
-    path-type "^3.0.0"
8130
-
8131 8027
 "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3, readable-stream@^2.3.6:
8132 8028
   version "2.3.6"
8133 8029
   resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
@@ -8831,16 +8727,6 @@ shebang-regex@^1.0.0:
8831 8727
   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
8832 8728
   integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
8833 8729
 
8834
-shell-quote@^1.6.1:
8835
-  version "1.6.1"
8836
-  resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"
8837
-  integrity sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=
8838
-  dependencies:
8839
-    array-filter "~0.0.0"
8840
-    array-map "~0.0.0"
8841
-    array-reduce "~0.0.0"
8842
-    jsonify "~0.0.0"
8843
-
8844 8730
 shellwords@^0.1.1:
8845 8731
   version "0.1.1"
8846 8732
   resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
@@ -9043,7 +8929,7 @@ split-string@^3.0.1, split-string@^3.0.2:
9043 8929
   dependencies:
9044 8930
     extend-shallow "^3.0.0"
9045 8931
 
9046
-split@^1.0.0, split@^1.0.1:
8932
+split@^1.0.0:
9047 8933
   version "1.0.1"
9048 8934
   resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
9049 8935
   integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==
@@ -9128,14 +9014,6 @@ stream-browserify@^2.0.1:
9128 9014
     inherits "~2.0.1"
9129 9015
     readable-stream "^2.0.2"
9130 9016
 
9131
-stream-combiner@^0.2.2:
9132
-  version "0.2.2"
9133
-  resolved "http://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858"
9134
-  integrity sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=
9135
-  dependencies:
9136
-    duplexer "~0.1.1"
9137
-    through "~2.3.4"
9138
-
9139 9017
 stream-each@^1.1.0:
9140 9018
   version "1.2.3"
9141 9019
   resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
@@ -9185,15 +9063,6 @@ string-width@^1.0.1, string-width@^1.0.2:
9185 9063
     is-fullwidth-code-point "^2.0.0"
9186 9064
     strip-ansi "^4.0.0"
9187 9065
 
9188
-string.prototype.padend@^3.0.0:
9189
-  version "3.0.0"
9190
-  resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0"
9191
-  integrity sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=
9192
-  dependencies:
9193
-    define-properties "^1.1.2"
9194
-    es-abstract "^1.4.3"
9195
-    function-bind "^1.0.2"
9196
-
9197 9066
 string.prototype.trim@^1.1.2:
9198 9067
   version "1.1.2"
9199 9068
   resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea"
@@ -9411,7 +9280,7 @@ through2@^2.0.0:
9411 9280
     readable-stream "^2.1.5"
9412 9281
     xtend "~4.0.1"
9413 9282
 
9414
-through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.4:
9283
+through@2, through@^2.3.6:
9415 9284
   version "2.3.8"
9416 9285
   resolved "http://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
9417 9286
   integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=

Loading…
Cancel
Save