commit
a2b4b82f0f
|
@ -6,6 +6,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: doorkeeper_token.application, serializer: REST::StatusSerializer::ApplicationSerializer
|
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -83,7 +83,11 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
]
|
]
|
||||||
|
|
||||||
handleReplyClick = () => {
|
handleReplyClick = () => {
|
||||||
this.props.onReply(this.props.status, this.context.router.history);
|
if (me) {
|
||||||
|
this.props.onReply(this.props.status, this.context.router.history);
|
||||||
|
} else {
|
||||||
|
this._openInteractionDialog('reply');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShareClick = () => {
|
handleShareClick = () => {
|
||||||
|
@ -94,17 +98,29 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFavouriteClick = (e) => {
|
handleFavouriteClick = (e) => {
|
||||||
this.props.onFavourite(this.props.status, e);
|
if (me) {
|
||||||
|
this.props.onFavourite(this.props.status, e);
|
||||||
|
} else {
|
||||||
|
this._openInteractionDialog('favourite');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBookmarkClick = (e) => {
|
handleBookmarkClick = (e) => {
|
||||||
this.props.onBookmark(this.props.status, e);
|
this.props.onBookmark(this.props.status, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReblogClick = (e) => {
|
handleReblogClick = e => {
|
||||||
this.props.onReblog(this.props.status, e);
|
if (me) {
|
||||||
|
this.props.onReblog(this.props.status, e);
|
||||||
|
} else {
|
||||||
|
this._openInteractionDialog('reblog');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_openInteractionDialog = type => {
|
||||||
|
window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
||||||
|
}
|
||||||
|
|
||||||
handleDeleteClick = () => {
|
handleDeleteClick = () => {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history);
|
this.props.onDelete(this.props.status, this.context.router.history);
|
||||||
}
|
}
|
||||||
|
@ -174,7 +190,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const anonymousAccess = !me;
|
const anonymousAccess = !me;
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
const reblogDisabled = anonymousAccess || (status.get('visibility') === 'direct' || (status.get('visibility') === 'private' && me !== status.getIn(['account', 'id'])));
|
const reblogDisabled = status.get('visibility') === 'direct' || (status.get('visibility') === 'private' && me !== status.getIn(['account', 'id']));
|
||||||
const reblogMessage = status.get('visibility') === 'private' ? messages.reblog_private : messages.reblog;
|
const reblogMessage = status.get('visibility') === 'private' ? messages.reblog_private : messages.reblog;
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
@ -243,7 +259,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
let replyButton = (
|
let replyButton = (
|
||||||
<IconButton
|
<IconButton
|
||||||
className='status__action-bar-button'
|
className='status__action-bar-button'
|
||||||
disabled={anonymousAccess}
|
|
||||||
title={replyTitle}
|
title={replyTitle}
|
||||||
icon={replyIcon}
|
icon={replyIcon}
|
||||||
onClick={this.handleReplyClick}
|
onClick={this.handleReplyClick}
|
||||||
|
@ -262,7 +277,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
{replyButton}
|
{replyButton}
|
||||||
<IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
<IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
||||||
<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='star' onClick={this.handleFavouriteClick} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||||
{shareButton}
|
{shareButton}
|
||||||
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
|
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||||
const { expanded, onToggleHidden, settings } = this.props;
|
const { expanded, onToggleHidden, settings } = this.props;
|
||||||
const outerStyle = { boxSizing: 'border-box' };
|
const outerStyle = { boxSizing: 'border-box' };
|
||||||
const { compact } = this.props;
|
const { compact } = this.props;
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import ready from 'flavours/glitch/util/ready';
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
|
const image = document.querySelector('img');
|
||||||
|
|
||||||
|
image.addEventListener('mouseenter', () => {
|
||||||
|
image.src = '/oops.gif';
|
||||||
|
});
|
||||||
|
|
||||||
|
image.addEventListener('mouseleave', () => {
|
||||||
|
image.src = '/oops.png';
|
||||||
|
});
|
||||||
|
});
|
|
@ -7,7 +7,7 @@ pack:
|
||||||
filename: packs/common.js
|
filename: packs/common.js
|
||||||
stylesheet: true
|
stylesheet: true
|
||||||
embed: packs/public.js
|
embed: packs/public.js
|
||||||
error:
|
error: packs/error.js
|
||||||
home:
|
home:
|
||||||
filename: packs/home.js
|
filename: packs/home.js
|
||||||
preload:
|
preload:
|
||||||
|
|
|
@ -7,7 +7,7 @@ pack:
|
||||||
filename: common.js
|
filename: common.js
|
||||||
stylesheet: true
|
stylesheet: true
|
||||||
embed: public.js
|
embed: public.js
|
||||||
error:
|
error: error.js
|
||||||
home:
|
home:
|
||||||
filename: application.js
|
filename: application.js
|
||||||
preload:
|
preload:
|
||||||
|
|
|
@ -78,7 +78,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
]
|
]
|
||||||
|
|
||||||
handleReplyClick = () => {
|
handleReplyClick = () => {
|
||||||
this.props.onReply(this.props.status, this.context.router.history);
|
if (me) {
|
||||||
|
this.props.onReply(this.props.status, this.context.router.history);
|
||||||
|
} else {
|
||||||
|
this._openInteractionDialog('reply');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShareClick = () => {
|
handleShareClick = () => {
|
||||||
|
@ -91,11 +95,23 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFavouriteClick = () => {
|
handleFavouriteClick = () => {
|
||||||
this.props.onFavourite(this.props.status);
|
if (me) {
|
||||||
|
this.props.onFavourite(this.props.status);
|
||||||
|
} else {
|
||||||
|
this._openInteractionDialog('favourite');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReblogClick = (e) => {
|
handleReblogClick = e => {
|
||||||
this.props.onReblog(this.props.status, e);
|
if (me) {
|
||||||
|
this.props.onReblog(this.props.status, e);
|
||||||
|
} else {
|
||||||
|
this._openInteractionDialog('reblog');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_openInteractionDialog = type => {
|
||||||
|
window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteClick = () => {
|
handleDeleteClick = () => {
|
||||||
|
@ -233,9 +249,9 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
|
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
|
||||||
<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} />
|
<IconButton className='status__action-bar-button' disabled={!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} />
|
||||||
<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='star' onClick={this.handleFavouriteClick} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
||||||
<div className='status__action-bar-dropdown'>
|
<div className='status__action-bar-dropdown'>
|
||||||
|
|
|
@ -18,6 +18,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
accountIds: state.getIn(['user_lists', 'blocks', 'items']),
|
accountIds: state.getIn(['user_lists', 'blocks', 'items']),
|
||||||
|
hasMore: !!state.getIn(['user_lists', 'blocks', 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
shouldUpdateScroll: PropTypes.func,
|
shouldUpdateScroll: PropTypes.func,
|
||||||
accountIds: ImmutablePropTypes.list,
|
accountIds: ImmutablePropTypes.list,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,7 +43,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, accountIds, shouldUpdateScroll } = this.props;
|
const { intl, accountIds, shouldUpdateScroll, hasMore } = this.props;
|
||||||
|
|
||||||
if (!accountIds) {
|
if (!accountIds) {
|
||||||
return (
|
return (
|
||||||
|
@ -59,6 +61,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='blocks'
|
scrollKey='blocks'
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
shouldUpdateScroll={shouldUpdateScroll}
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
>
|
>
|
||||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
domains: state.getIn(['domain_lists', 'blocks', 'items']),
|
domains: state.getIn(['domain_lists', 'blocks', 'items']),
|
||||||
|
hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
shouldUpdateScroll: PropTypes.func,
|
shouldUpdateScroll: PropTypes.func,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
domains: ImmutablePropTypes.orderedSet,
|
domains: ImmutablePropTypes.orderedSet,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -42,7 +44,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, domains, shouldUpdateScroll } = this.props;
|
const { intl, domains, shouldUpdateScroll, hasMore } = this.props;
|
||||||
|
|
||||||
if (!domains) {
|
if (!domains) {
|
||||||
return (
|
return (
|
||||||
|
@ -60,6 +62,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='domain_blocks'
|
scrollKey='domain_blocks'
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
shouldUpdateScroll={shouldUpdateScroll}
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
>
|
>
|
||||||
|
|
|
@ -18,6 +18,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
|
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
|
||||||
|
hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -28,6 +29,7 @@ class FollowRequests extends ImmutablePureComponent {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
shouldUpdateScroll: PropTypes.func,
|
shouldUpdateScroll: PropTypes.func,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
accountIds: ImmutablePropTypes.list,
|
accountIds: ImmutablePropTypes.list,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -41,7 +43,7 @@ class FollowRequests extends ImmutablePureComponent {
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, shouldUpdateScroll, accountIds } = this.props;
|
const { intl, shouldUpdateScroll, accountIds, hasMore } = this.props;
|
||||||
|
|
||||||
if (!accountIds) {
|
if (!accountIds) {
|
||||||
return (
|
return (
|
||||||
|
@ -59,6 +61,7 @@ class FollowRequests extends ImmutablePureComponent {
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='follow_requests'
|
scrollKey='follow_requests'
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
shouldUpdateScroll={shouldUpdateScroll}
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
>
|
>
|
||||||
|
|
|
@ -18,6 +18,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
accountIds: state.getIn(['user_lists', 'mutes', 'items']),
|
accountIds: state.getIn(['user_lists', 'mutes', 'items']),
|
||||||
|
hasMore: !!state.getIn(['user_lists', 'mutes', 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -28,6 +29,7 @@ class Mutes extends ImmutablePureComponent {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
shouldUpdateScroll: PropTypes.func,
|
shouldUpdateScroll: PropTypes.func,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
accountIds: ImmutablePropTypes.list,
|
accountIds: ImmutablePropTypes.list,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -41,7 +43,7 @@ class Mutes extends ImmutablePureComponent {
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, shouldUpdateScroll, accountIds } = this.props;
|
const { intl, shouldUpdateScroll, hasMore, accountIds } = this.props;
|
||||||
|
|
||||||
if (!accountIds) {
|
if (!accountIds) {
|
||||||
return (
|
return (
|
||||||
|
@ -59,6 +61,7 @@ class Mutes extends ImmutablePureComponent {
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='mutes'
|
scrollKey='mutes'
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
shouldUpdateScroll={shouldUpdateScroll}
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
>
|
>
|
||||||
|
|
|
@ -87,7 +87,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
|
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||||
const outerStyle = { boxSizing: 'border-box' };
|
const outerStyle = { boxSizing: 'border-box' };
|
||||||
const { compact } = this.props;
|
const { compact } = this.props;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
|
const image = document.querySelector('img');
|
||||||
|
|
||||||
|
image.addEventListener('mouseenter', () => {
|
||||||
|
image.src = '/oops.gif';
|
||||||
|
});
|
||||||
|
|
||||||
|
image.addEventListener('mouseleave', () => {
|
||||||
|
image.src = '/oops.png';
|
||||||
|
});
|
||||||
|
});
|
|
@ -100,12 +100,14 @@ body {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
|
|
||||||
img {
|
&__illustration {
|
||||||
display: block;
|
img {
|
||||||
max-width: 470px;
|
display: block;
|
||||||
width: 100%;
|
max-width: 470px;
|
||||||
height: auto;
|
width: 100%;
|
||||||
margin-top: -120px;
|
height: auto;
|
||||||
|
margin-top: -120px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
|
|
@ -138,11 +138,13 @@ class ActivityPub::Activity
|
||||||
def status_from_object
|
def status_from_object
|
||||||
# If the status is already known, return it
|
# If the status is already known, return it
|
||||||
status = status_from_uri(object_uri)
|
status = status_from_uri(object_uri)
|
||||||
|
|
||||||
return status unless status.nil?
|
return status unless status.nil?
|
||||||
|
|
||||||
# If the boosted toot is embedded and it is a self-boost, handle it like a Create
|
# If the boosted toot is embedded and it is a self-boost, handle it like a Create
|
||||||
unless unsupported_object_type?
|
unless unsupported_object_type?
|
||||||
actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri
|
actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri
|
||||||
|
|
||||||
if actor_id == @account.uri
|
if actor_id == @account.uri
|
||||||
return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform
|
return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform
|
||||||
end
|
end
|
||||||
|
@ -166,4 +168,21 @@ class ActivityPub::Activity
|
||||||
ensure
|
ensure
|
||||||
redis.del(key)
|
redis.del(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fetch?
|
||||||
|
!@options[:delivery]
|
||||||
|
end
|
||||||
|
|
||||||
|
def followed_by_local_accounts?
|
||||||
|
@account.passive_relationships.exists?
|
||||||
|
end
|
||||||
|
|
||||||
|
def requested_through_relay?
|
||||||
|
@options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject_payload!
|
||||||
|
Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}")
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,8 +2,11 @@
|
||||||
|
|
||||||
class ActivityPub::Activity::Announce < ActivityPub::Activity
|
class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||||
def perform
|
def perform
|
||||||
|
return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
|
||||||
|
|
||||||
original_status = status_from_object
|
original_status = status_from_object
|
||||||
return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status)
|
|
||||||
|
return reject_payload! if original_status.nil? || !announceable?(original_status)
|
||||||
|
|
||||||
status = Status.find_by(account: @account, reblog: original_status)
|
status = Status.find_by(account: @account, reblog: original_status)
|
||||||
|
|
||||||
|
@ -39,4 +42,12 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||||
def announceable?(status)
|
def announceable?(status)
|
||||||
status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility?
|
status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def related_to_local_activity?
|
||||||
|
followed_by_local_accounts? || requested_through_relay? || reblog_of_local_status?
|
||||||
|
end
|
||||||
|
|
||||||
|
def reblog_of_local_status?
|
||||||
|
status_from_uri(object_uri)&.account&.local?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class ActivityPub::Activity::Create < ActivityPub::Activity
|
class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
def perform
|
def perform
|
||||||
return if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
|
return reject_payload! if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
RedisLock.acquire(lock_options) do |lock|
|
||||||
if lock.acquired?
|
if lock.acquired?
|
||||||
|
@ -341,18 +341,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
responds_to_followed_account? || addresses_local_accounts?
|
responds_to_followed_account? || addresses_local_accounts?
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch?
|
|
||||||
!@options[:delivery]
|
|
||||||
end
|
|
||||||
|
|
||||||
def followed_by_local_accounts?
|
|
||||||
@account.passive_relationships.exists?
|
|
||||||
end
|
|
||||||
|
|
||||||
def requested_through_relay?
|
|
||||||
@options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
|
|
||||||
end
|
|
||||||
|
|
||||||
def responds_to_followed_account?
|
def responds_to_followed_account?
|
||||||
!replied_to_status.nil? && (replied_to_status.account.local? || replied_to_status.account.passive_relationships.exists?)
|
!replied_to_status.nil? && (replied_to_status.account.local? || replied_to_status.account.passive_relationships.exists?)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class REST::ApplicationSerializer < ActiveModel::Serializer
|
class REST::ApplicationSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :name, :website, :redirect_uri,
|
attributes :id, :name, :website, :redirect_uri,
|
||||||
:client_id, :client_secret
|
:client_id, :client_secret, :vapid_key
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
|
@ -19,4 +19,8 @@ class REST::ApplicationSerializer < ActiveModel::Serializer
|
||||||
def website
|
def website
|
||||||
object.website.presence
|
object.website.presence
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def vapid_key
|
||||||
|
Rails.configuration.x.vapid_public_key
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
|
|
||||||
attributes :uri, :title, :description, :email,
|
attributes :uri, :title, :description, :email,
|
||||||
:version, :urls, :stats, :thumbnail, :max_toot_chars,
|
:version, :urls, :stats, :thumbnail, :max_toot_chars,
|
||||||
:languages
|
:languages, :registrations
|
||||||
|
|
||||||
has_one :contact_account, serializer: REST::AccountSerializer
|
has_one :contact_account, serializer: REST::AccountSerializer
|
||||||
|
|
||||||
|
@ -55,6 +55,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
[I18n.default_locale]
|
[I18n.default_locale]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def registrations
|
||||||
|
Setting.open_registrations && !Rails.configuration.x.single_user_mode
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def instance_presenter
|
def instance_presenter
|
||||||
|
|
|
@ -5,10 +5,12 @@
|
||||||
%meta{ charset: 'utf-8' }/
|
%meta{ charset: 'utf-8' }/
|
||||||
%title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
|
%title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
|
||||||
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
|
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
|
||||||
|
= javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
|
||||||
= render partial: 'layouts/theme', object: (@core || { pack: 'common' })
|
= render partial: 'layouts/theme', object: (@core || { pack: 'common' })
|
||||||
= render partial: 'layouts/theme', object: (@theme || { pack: 'common', flavour: 'glitch', skin: 'default' })
|
= render partial: 'layouts/theme', object: (@theme || { pack: 'error', flavour: 'glitch', common: { pack: 'common', flavour: 'glitch', skin: 'default' } })
|
||||||
%body.error
|
%body.error
|
||||||
.dialog
|
.dialog
|
||||||
%img{ alt: Setting.default_settings['site_title'], src: current_user&.setting_auto_play_gif ? '/oops.gif' : '/oops.png' }/
|
.dialog__illustration
|
||||||
%div
|
%img{ alt: Setting.default_settings['site_title'], src: '/oops.png' }/
|
||||||
|
.dialog__message
|
||||||
%h1= yield :content
|
%h1= yield :content
|
||||||
|
|
|
@ -22,11 +22,59 @@ RSpec.describe ActivityPub::Activity::Announce do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
before do
|
context 'when sender is followed by a local account' do
|
||||||
subject.perform
|
before do
|
||||||
|
Fabricate(:account).follow!(sender)
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'a known status' do
|
||||||
|
let(:object_json) do
|
||||||
|
ActivityPub::TagManager.instance.uri_for(status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a reblog by sender of status' do
|
||||||
|
expect(sender.reblogged?(status)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'self-boost of a previously unknown status with missing attributedTo' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a reblog by sender of status' do
|
||||||
|
expect(sender.reblogged?(sender.statuses.first)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'self-boost of a previously unknown status with correct attributedTo' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a reblog by sender of status' do
|
||||||
|
expect(sender.reblogged?(sender.statuses.first)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'a known status' do
|
context 'when the status belongs to a local user' do
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
ActivityPub::TagManager.instance.uri_for(status)
|
ActivityPub::TagManager.instance.uri_for(status)
|
||||||
end
|
end
|
||||||
|
@ -36,34 +84,68 @@ RSpec.describe ActivityPub::Activity::Announce do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'self-boost of a previously unknown status with missing attributedTo' do
|
context 'when the sender is relayed' do
|
||||||
let(:object_json) do
|
let!(:relay_account) { Fabricate(:account, inbox_url: 'https://relay.example.com/inbox') }
|
||||||
{
|
let!(:relay) { Fabricate(:relay, inbox_url: 'https://relay.example.com/inbox') }
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
|
||||||
type: 'Note',
|
subject { described_class.new(json, sender, relayed_through_account: relay_account) }
|
||||||
content: 'Lorem ipsum',
|
|
||||||
to: 'http://example.com/followers',
|
context 'and the relay is enabled' do
|
||||||
}
|
before do
|
||||||
|
relay.update(state: :accepted)
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a reblog by sender of status' do
|
||||||
|
expect(sender.statuses.count).to eq 2
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a reblog by sender of status' do
|
context 'and the relay is disabled' do
|
||||||
expect(sender.reblogged?(sender.statuses.first)).to be true
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create anything' do
|
||||||
|
expect(sender.statuses.count).to eq 0
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'self-boost of a previously unknown status with correct attributedTo' do
|
context 'when the sender has no relevance to local activity' do
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
|
|
||||||
to: 'http://example.com/followers',
|
to: 'http://example.com/followers',
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a reblog by sender of status' do
|
it 'does not create anything' do
|
||||||
expect(sender.reblogged?(sender.statuses.first)).to be true
|
expect(sender.statuses.count).to eq 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,8 +13,6 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
subject { described_class.new(json, sender) }
|
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
||||||
|
|
||||||
|
@ -23,11 +21,402 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
before do
|
context 'when fetching' do
|
||||||
subject.perform
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'standalone' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'missing to/cc defaults to direct privacy' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.visibility).to eq 'direct'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'public' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.visibility).to eq 'public'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'unlisted' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
cc: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.visibility).to eq 'unlisted'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'private' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.visibility).to eq 'private'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'limited' do
|
||||||
|
let(:recipient) { Fabricate(:account) }
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.visibility).to eq 'limited'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates silent mention' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
expect(status.mentions.first).to be_silent
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'direct' do
|
||||||
|
let(:recipient) { Fabricate(:account) }
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||||
|
tag: {
|
||||||
|
type: 'Mention',
|
||||||
|
href: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.visibility).to eq 'direct'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as a reply' do
|
||||||
|
let(:original_status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.thread).to eq original_status
|
||||||
|
expect(status.reply?).to be true
|
||||||
|
expect(status.in_reply_to_account).to eq original_status.account
|
||||||
|
expect(status.conversation).to eq original_status.conversation
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with mentions' do
|
||||||
|
let(:recipient) { Fabricate(:account) }
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
tag: [
|
||||||
|
{
|
||||||
|
type: 'Mention',
|
||||||
|
href: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.mentions.map(&:account)).to include(recipient)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with mentions missing href' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
tag: [
|
||||||
|
{
|
||||||
|
type: 'Mention',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with media attachments' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
attachment: [
|
||||||
|
{
|
||||||
|
type: 'Document',
|
||||||
|
mediaType: 'image/png',
|
||||||
|
url: 'http://example.com/attachment.png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with media attachments with focal points' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
attachment: [
|
||||||
|
{
|
||||||
|
type: 'Document',
|
||||||
|
mediaType: 'image/png',
|
||||||
|
url: 'http://example.com/attachment.png',
|
||||||
|
focalPoint: [0.5, -0.7],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with media attachments missing url' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
attachment: [
|
||||||
|
{
|
||||||
|
type: 'Document',
|
||||||
|
mediaType: 'image/png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with hashtags' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
tag: [
|
||||||
|
{
|
||||||
|
type: 'Hashtag',
|
||||||
|
href: 'http://example.com/blah',
|
||||||
|
name: '#test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.tags.map(&:name)).to include('test')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with hashtags missing name' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
tag: [
|
||||||
|
{
|
||||||
|
type: 'Hashtag',
|
||||||
|
href: 'http://example.com/blah',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with emojis' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum :tinking:',
|
||||||
|
tag: [
|
||||||
|
{
|
||||||
|
type: 'Emoji',
|
||||||
|
icon: {
|
||||||
|
url: 'http://example.com/emoji.png',
|
||||||
|
},
|
||||||
|
name: 'tinking',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.emojis.map(&:shortcode)).to include('tinking')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with emojis missing name' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum :tinking:',
|
||||||
|
tag: [
|
||||||
|
{
|
||||||
|
type: 'Emoji',
|
||||||
|
icon: {
|
||||||
|
url: 'http://example.com/emoji.png',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with emojis missing icon' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum :tinking:',
|
||||||
|
tag: [
|
||||||
|
{
|
||||||
|
type: 'Emoji',
|
||||||
|
name: 'tinking',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'standalone' do
|
context 'when sender is followed by local users' do
|
||||||
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:account).follow!(sender)
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
@ -42,78 +431,23 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
expect(status).to_not be_nil
|
expect(status).to_not be_nil
|
||||||
expect(status.text).to eq 'Lorem ipsum'
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'missing to/cc defaults to direct privacy' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
expect(status.visibility).to eq 'direct'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'public' do
|
context 'when sender replies to local status' do
|
||||||
let(:object_json) do
|
let!(:local_status) { Fabricate(:status) }
|
||||||
{
|
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
type: 'Note',
|
|
||||||
content: 'Lorem ipsum',
|
before do
|
||||||
to: 'https://www.w3.org/ns/activitystreams#Public',
|
subject.perform
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates status' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
expect(status.visibility).to eq 'public'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'unlisted' do
|
|
||||||
let(:object_json) do
|
|
||||||
{
|
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
|
||||||
type: 'Note',
|
|
||||||
content: 'Lorem ipsum',
|
|
||||||
cc: 'https://www.w3.org/ns/activitystreams#Public',
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates status' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
expect(status.visibility).to eq 'unlisted'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'private' do
|
|
||||||
let(:object_json) do
|
|
||||||
{
|
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
|
||||||
type: 'Note',
|
|
||||||
content: 'Lorem ipsum',
|
|
||||||
to: 'http://example.com/followers',
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates status' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
expect(status.visibility).to eq 'private'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'limited' do
|
|
||||||
let(:recipient) { Fabricate(:account) }
|
|
||||||
|
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
to: ActivityPub::TagManager.instance.uri_for(recipient),
|
inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -121,28 +455,25 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
status = sender.statuses.first
|
status = sender.statuses.first
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
expect(status).to_not be_nil
|
||||||
expect(status.visibility).to eq 'limited'
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates silent mention' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
expect(status.mentions.first).to be_silent
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'direct' do
|
context 'when sender targets a local user' do
|
||||||
let(:recipient) { Fabricate(:account) }
|
let!(:local_account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
to: ActivityPub::TagManager.instance.uri_for(recipient),
|
to: ActivityPub::TagManager.instance.uri_for(local_account),
|
||||||
tag: {
|
|
||||||
type: 'Mention',
|
|
||||||
href: ActivityPub::TagManager.instance.uri_for(recipient),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -150,19 +481,25 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
status = sender.statuses.first
|
status = sender.statuses.first
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
expect(status).to_not be_nil
|
||||||
expect(status.visibility).to eq 'direct'
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'as a reply' do
|
context 'when sender cc\'s a local user' do
|
||||||
let(:original_status) { Fabricate(:status) }
|
let!(:local_account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
|
cc: ActivityPub::TagManager.instance.uri_for(local_account),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -170,240 +507,27 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
status = sender.statuses.first
|
status = sender.statuses.first
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
expect(status).to_not be_nil
|
||||||
expect(status.thread).to eq original_status
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
expect(status.reply?).to be true
|
|
||||||
expect(status.in_reply_to_account).to eq original_status.account
|
|
||||||
expect(status.conversation).to eq original_status.conversation
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with mentions' do
|
context 'when the sender has no relevance to local activity' do
|
||||||
let(:recipient) { Fabricate(:account) }
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
tag: [
|
|
||||||
{
|
|
||||||
type: 'Mention',
|
|
||||||
href: ActivityPub::TagManager.instance.uri_for(recipient),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates status' do
|
it 'does not create anything' do
|
||||||
status = sender.statuses.first
|
expect(sender.statuses.count).to eq 0
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
expect(status.mentions.map(&:account)).to include(recipient)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with mentions missing href' do
|
|
||||||
let(:object_json) do
|
|
||||||
{
|
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
|
||||||
type: 'Note',
|
|
||||||
content: 'Lorem ipsum',
|
|
||||||
tag: [
|
|
||||||
{
|
|
||||||
type: 'Mention',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates status' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with media attachments' do
|
|
||||||
let(:object_json) do
|
|
||||||
{
|
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
|
||||||
type: 'Note',
|
|
||||||
content: 'Lorem ipsum',
|
|
||||||
attachment: [
|
|
||||||
{
|
|
||||||
type: 'Document',
|
|
||||||
mediaType: 'image/png',
|
|
||||||
url: 'http://example.com/attachment.png',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates status' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with media attachments with focal points' do
|
|
||||||
let(:object_json) do
|
|
||||||
{
|
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
|
||||||
type: 'Note',
|
|
||||||
content: 'Lorem ipsum',
|
|
||||||
attachment: [
|
|
||||||
{
|
|
||||||
type: 'Document',
|
|
||||||
mediaType: 'image/png',
|
|
||||||
url: 'http://example.com/attachment.png',
|
|
||||||
focalPoint: [0.5, -0.7],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates status' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with media attachments missing url' do
|
|
||||||
let(:object_json) do
|
|
||||||
{
|
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
|
||||||
type: 'Note',
|
|
||||||
content: 'Lorem ipsum',
|
|
||||||
attachment: [
|
|
||||||
{
|
|
||||||
type: 'Document',
|
|
||||||
mediaType: 'image/png',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates status' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with hashtags' do
|
|
||||||
let(:object_json) do
|
|
||||||
{
|
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
|
||||||
type: 'Note',
|
|
||||||
content: 'Lorem ipsum',
|
|
||||||
tag: [
|
|
||||||
{
|
|
||||||
type: 'Hashtag',
|
|
||||||
href: 'http://example.com/blah',
|
|
||||||
name: '#test',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates status' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
expect(status.tags.map(&:name)).to include('test')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with hashtags missing name' do
|
|
||||||
let(:object_json) do
|
|
||||||
{
|
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
|
||||||
type: 'Note',
|
|
||||||
content: 'Lorem ipsum',
|
|
||||||
tag: [
|
|
||||||
{
|
|
||||||
type: 'Hashtag',
|
|
||||||
href: 'http://example.com/blah',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates status' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with emojis' do
|
|
||||||
let(:object_json) do
|
|
||||||
{
|
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
|
||||||
type: 'Note',
|
|
||||||
content: 'Lorem ipsum :tinking:',
|
|
||||||
tag: [
|
|
||||||
{
|
|
||||||
type: 'Emoji',
|
|
||||||
icon: {
|
|
||||||
url: 'http://example.com/emoji.png',
|
|
||||||
},
|
|
||||||
name: 'tinking',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates status' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
expect(status.emojis.map(&:shortcode)).to include('tinking')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with emojis missing name' do
|
|
||||||
let(:object_json) do
|
|
||||||
{
|
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
|
||||||
type: 'Note',
|
|
||||||
content: 'Lorem ipsum :tinking:',
|
|
||||||
tag: [
|
|
||||||
{
|
|
||||||
type: 'Emoji',
|
|
||||||
icon: {
|
|
||||||
url: 'http://example.com/emoji.png',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates status' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with emojis missing icon' do
|
|
||||||
let(:object_json) do
|
|
||||||
{
|
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
|
||||||
type: 'Note',
|
|
||||||
content: 'Lorem ipsum :tinking:',
|
|
||||||
tag: [
|
|
||||||
{
|
|
||||||
type: 'Emoji',
|
|
||||||
name: 'tinking',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates status' do
|
|
||||||
status = sender.statuses.first
|
|
||||||
expect(status).to_not be_nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue