2021-05-08 05:55:06 +00:00
< ? php
/**
2023-01-01 09:36:24 -05:00
* @ copyright Copyright ( C ) 2010 - 2023 , the Friendica project
2021-05-08 05:55:06 +00:00
*
* @ license GNU AGPL version 3 or any later version
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*
*/
namespace Friendica\Module\Api\Mastodon ;
2023-02-28 15:35:37 -05:00
use Friendica\Content\Text\BBCode ;
2021-05-15 22:40:57 +00:00
use Friendica\Content\Text\Markdown ;
2022-03-02 17:17:07 +00:00
use Friendica\Core\Protocol ;
2021-05-08 05:55:06 +00:00
use Friendica\Core\System ;
2022-10-17 05:49:55 +00:00
use Friendica\Core\Worker ;
2021-05-15 22:40:57 +00:00
use Friendica\Database\DBA ;
2021-05-08 05:55:06 +00:00
use Friendica\DI ;
2021-05-15 22:40:57 +00:00
use Friendica\Model\Contact ;
use Friendica\Model\Group ;
2021-05-15 10:08:47 +00:00
use Friendica\Model\Item ;
2021-05-16 14:30:15 +00:00
use Friendica\Model\Photo ;
2021-05-15 10:08:47 +00:00
use Friendica\Model\Post ;
2021-05-15 22:40:57 +00:00
use Friendica\Model\User ;
2021-05-08 05:55:06 +00:00
use Friendica\Module\BaseApi ;
2022-11-25 19:35:40 +00:00
use Friendica\Network\HTTPException ;
2021-05-15 22:40:57 +00:00
use Friendica\Protocol\Activity ;
use Friendica\Util\Images ;
2021-05-08 05:55:06 +00:00
/**
* @ see https :// docs . joinmastodon . org / methods / statuses /
*/
class Statuses extends BaseApi
{
2022-04-21 19:58:56 +00:00
public function put ( array $request = [])
{
self :: checkAllowedScope ( self :: SCOPE_WRITE );
$uid = self :: getCurrentUserID ();
2022-11-25 19:35:40 +00:00
$request = $this -> getRequest ([
'status' => '' , // Text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.
'spoiler_text' => '' , // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field.
'language' => '' , // ISO 639 language code for this status.
2023-02-28 15:35:37 -05:00
'friendica' => [],
2022-11-25 19:35:40 +00:00
], $request );
$owner = User :: getOwnerDataById ( $uid );
$condition = [
'uid' => $uid ,
'uri-id' => $this -> parameters [ 'id' ],
'contact-id' => $owner [ 'id' ],
'author-id' => Contact :: getPublicIdByUserId ( $uid ),
'origin' => true ,
];
2023-02-28 15:35:37 -05:00
$post = Post :: selectFirst ([ 'uri-id' , 'id' , 'gravity' ], $condition );
2022-11-25 19:35:40 +00:00
if ( empty ( $post [ 'id' ])) {
throw new HTTPException\NotFoundException ( 'Item with URI ID ' . $this -> parameters [ 'id' ] . ' not found for user ' . $uid . '.' );
}
// The imput is defined as text. So we can use Markdown for some enhancements
2023-01-22 17:10:31 +00:00
$item = [ 'body' => Markdown :: toBBCode ( $request [ 'status' ]), 'app' => $this -> getApp (), 'title' => '' ];
2022-11-25 19:35:40 +00:00
if ( ! empty ( $request [ 'language' ])) {
$item [ 'language' ] = json_encode ([ $request [ 'language' ] => 1 ]);
}
2023-02-28 15:35:37 -05:00
if ( $post [ 'gravity' ] == 0 ) {
$item [ 'title' ] = $request [ 'friendica' ][ 'title' ] ? ? '' ;
}
$spoiler_text = $request [ 'spoiler_text' ];
if ( ! empty ( $spoiler_text )) {
if ( ! isset ( $request [ 'friendica' ][ 'title' ]) && $post [ 'gravity' ] == 0 && DI :: pConfig () -> get ( $uid , 'system' , 'api_spoiler_title' , true )) {
$item [ 'title' ] = $spoiler_text ;
2023-01-22 17:10:31 +00:00
} else {
2023-02-28 15:35:37 -05:00
$item [ 'body' ] = '[abstract=' . Protocol :: ACTIVITYPUB . ']' . $spoiler_text . " [/abstract] \n " . $item [ 'body' ];
$item [ 'content-warning' ] = BBCode :: toPlaintext ( $spoiler_text );
2022-11-25 19:35:40 +00:00
}
}
2023-02-28 15:35:37 -05:00
if ( ! Item :: isValid ( $item )) {
throw new \Exception ( 'Missing parameters in definitien' );
}
2022-11-25 19:35:40 +00:00
Item :: update ( $item , [ 'id' => $post [ 'id' ]]);
Item :: updateDisplayCache ( $post [ 'uri-id' ]);
2023-01-25 20:14:33 +00:00
System :: jsonExit ( DI :: mstdnStatus () -> createFromUriId ( $post [ 'uri-id' ], $uid , self :: appSupportsQuotes ()));
2022-04-21 19:58:56 +00:00
}
2021-11-28 13:44:42 +01:00
protected function post ( array $request = [])
2021-05-13 21:15:32 +00:00
{
2021-06-08 12:00:22 +00:00
self :: checkAllowedScope ( self :: SCOPE_WRITE );
2021-05-15 22:40:57 +00:00
$uid = self :: getCurrentUserID ();
2021-11-28 13:22:27 +01:00
$request = $this -> getRequest ([
2021-05-28 06:10:32 +00:00
'status' => '' , // Text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.
'media_ids' => [], // Array of Attachment ids to be attached as media. If provided, status becomes optional, and poll cannot be used.
'poll' => [], // Poll data. If provided, media_ids cannot be used, and poll[expires_in] must be provided.
'in_reply_to_id' => 0 , // ID of the status being replied to, if status is a reply
2023-01-23 21:24:50 +00:00
'quote_id' => 0 , // ID of the message to quote
2021-05-28 06:10:32 +00:00
'sensitive' => false , // Mark status and attached media as sensitive?
'spoiler_text' => '' , // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field.
2021-05-29 14:32:31 +02:00
'visibility' => '' , // Visibility of the posted status. One of: "public", "unlisted", "private" or "direct".
2021-05-28 06:10:32 +00:00
'scheduled_at' => '' , // ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future.
'language' => '' , // ISO 639 language code for this status.
2023-02-19 07:50:39 -05:00
'friendica' => [], // Friendica extensions to the standard Mastodon API spec
2021-11-27 18:30:41 -05:00
], $request );
2021-05-15 22:40:57 +00:00
$owner = User :: getOwnerDataById ( $uid );
// The imput is defined as text. So we can use Markdown for some enhancements
2021-05-28 06:10:32 +00:00
$body = Markdown :: toBBCode ( $request [ 'status' ]);
2021-05-15 22:40:57 +00:00
2022-03-02 17:17:07 +00:00
$item = [];
$item [ 'network' ] = Protocol :: DFRN ;
2021-05-15 22:40:57 +00:00
$item [ 'uid' ] = $uid ;
$item [ 'verb' ] = Activity :: POST ;
$item [ 'contact-id' ] = $owner [ 'id' ];
$item [ 'author-id' ] = $item [ 'owner-id' ] = Contact :: getPublicIdByUserId ( $uid );
2023-01-22 17:10:31 +00:00
$item [ 'title' ] = '' ;
2021-05-15 22:40:57 +00:00
$item [ 'body' ] = $body ;
2022-11-25 19:35:40 +00:00
$item [ 'app' ] = $this -> getApp ();
2021-05-15 22:40:57 +00:00
2021-05-28 06:10:32 +00:00
switch ( $request [ 'visibility' ]) {
2021-05-15 22:40:57 +00:00
case 'public' :
$item [ 'allow_cid' ] = '' ;
$item [ 'allow_gid' ] = '' ;
$item [ 'deny_cid' ] = '' ;
$item [ 'deny_gid' ] = '' ;
$item [ 'private' ] = Item :: PUBLIC ;
break ;
case 'unlisted' :
$item [ 'allow_cid' ] = '' ;
$item [ 'allow_gid' ] = '' ;
$item [ 'deny_cid' ] = '' ;
$item [ 'deny_gid' ] = '' ;
$item [ 'private' ] = Item :: UNLISTED ;
break ;
case 'private' :
if ( ! empty ( $owner [ 'allow_cid' ] . $owner [ 'allow_gid' ] . $owner [ 'deny_cid' ] . $owner [ 'deny_gid' ])) {
$item [ 'allow_cid' ] = $owner [ 'allow_cid' ];
$item [ 'allow_gid' ] = $owner [ 'allow_gid' ];
$item [ 'deny_cid' ] = $owner [ 'deny_cid' ];
$item [ 'deny_gid' ] = $owner [ 'deny_gid' ];
} else {
$item [ 'allow_cid' ] = '' ;
2021-07-30 06:22:32 +00:00
$item [ 'allow_gid' ] = '<' . Group :: FOLLOWERS . '>' ;
2021-05-15 22:40:57 +00:00
$item [ 'deny_cid' ] = '' ;
$item [ 'deny_gid' ] = '' ;
}
$item [ 'private' ] = Item :: PRIVATE ;
break ;
case 'direct' :
2023-01-23 21:24:50 +00:00
$item [ 'private' ] = Item :: PRIVATE ;
2022-03-05 06:14:30 +00:00
// The permissions are assigned in "expandTags"
2021-05-28 06:10:32 +00:00
break ;
2021-05-15 22:40:57 +00:00
default :
2022-03-04 05:50:33 +00:00
if ( is_numeric ( $request [ 'visibility' ]) && Group :: exists ( $request [ 'visibility' ], $uid )) {
$item [ 'allow_cid' ] = '' ;
$item [ 'allow_gid' ] = '<' . $request [ 'visibility' ] . '>' ;
$item [ 'deny_cid' ] = '' ;
$item [ 'deny_gid' ] = '' ;
} else {
$item [ 'allow_cid' ] = $owner [ 'allow_cid' ];
$item [ 'allow_gid' ] = $owner [ 'allow_gid' ];
$item [ 'deny_cid' ] = $owner [ 'deny_cid' ];
$item [ 'deny_gid' ] = $owner [ 'deny_gid' ];
}
2021-05-15 22:40:57 +00:00
if ( ! empty ( $item [ 'allow_cid' ] . $item [ 'allow_gid' ] . $item [ 'deny_cid' ] . $item [ 'deny_gid' ])) {
$item [ 'private' ] = Item :: PRIVATE ;
} elseif ( DI :: pConfig () -> get ( $uid , 'system' , 'unlisted' )) {
$item [ 'private' ] = Item :: UNLISTED ;
} else {
$item [ 'private' ] = Item :: PUBLIC ;
}
break ;
}
2021-05-28 06:10:32 +00:00
if ( ! empty ( $request [ 'language' ])) {
$item [ 'language' ] = json_encode ([ $request [ 'language' ] => 1 ]);
2021-05-15 22:40:57 +00:00
}
2021-05-28 06:10:32 +00:00
if ( $request [ 'in_reply_to_id' ]) {
2023-01-23 21:24:50 +00:00
$parent = Post :: selectFirst ([ 'uri' , 'private' ], [ 'uri-id' => $request [ 'in_reply_to_id' ], 'uid' => [ 0 , $uid ]]);
2022-03-08 18:32:09 +00:00
2021-05-15 22:40:57 +00:00
$item [ 'thr-parent' ] = $parent [ 'uri' ];
2022-09-12 23:12:11 +02:00
$item [ 'gravity' ] = Item :: GRAVITY_COMMENT ;
2021-05-15 22:40:57 +00:00
$item [ 'object-type' ] = Activity\ObjectType :: COMMENT ;
2023-01-23 21:24:50 +00:00
if ( in_array ( $parent [ 'private' ], [ Item :: UNLISTED , Item :: PUBLIC ]) && ( $item [ 'private' ] == Item :: PRIVATE )) {
throw new HTTPException\NotImplementedException ( 'Private replies for public posts are not implemented.' );
}
2021-05-15 22:40:57 +00:00
} else {
2021-07-08 13:47:46 +00:00
self :: checkThrottleLimit ();
2022-09-12 23:12:11 +02:00
$item [ 'gravity' ] = Item :: GRAVITY_PARENT ;
2021-05-15 22:40:57 +00:00
$item [ 'object-type' ] = Activity\ObjectType :: NOTE ;
2023-01-22 17:10:31 +00:00
}
2023-01-23 21:24:50 +00:00
if ( $request [ 'quote_id' ]) {
if ( ! Post :: exists ([ 'uri-id' => $request [ 'quote_id' ], 'uid' => [ 0 , $uid ]])) {
throw new HTTPException\NotFoundException ( 'Item with URI ID ' . $request [ 'quote_id' ] . ' not found for user ' . $uid . '.' );
}
$item [ 'quote-uri-id' ] = $request [ 'quote_id' ];
}
2023-02-18 10:07:08 -05:00
$item [ 'title' ] = $request [ 'friendica' ][ 'title' ] ? ? '' ;
2023-02-17 17:42:55 -05:00
2023-01-22 17:10:31 +00:00
if ( ! empty ( $request [ 'spoiler_text' ])) {
2023-02-18 10:07:08 -05:00
if ( ! isset ( $request [ 'friendica' ][ 'title' ]) && ! $request [ 'in_reply_to_id' ] && DI :: pConfig () -> get ( $uid , 'system' , 'api_spoiler_title' , true )) {
2023-01-22 17:10:31 +00:00
$item [ 'title' ] = $request [ 'spoiler_text' ];
} else {
$item [ 'body' ] = '[abstract=' . Protocol :: ACTIVITYPUB . ']' . $request [ 'spoiler_text' ] . " [/abstract] \n " . $item [ 'body' ];
}
2021-05-15 22:40:57 +00:00
}
2022-03-05 06:14:30 +00:00
$item = DI :: contentItem () -> expandTags ( $item , $request [ 'visibility' ] == 'direct' );
2022-03-02 17:17:07 +00:00
2021-05-28 06:10:32 +00:00
if ( ! empty ( $request [ 'media_ids' ])) {
2021-05-15 22:40:57 +00:00
$item [ 'object-type' ] = Activity\ObjectType :: IMAGE ;
$item [ 'post-type' ] = Item :: PT_IMAGE ;
$item [ 'attachments' ] = [];
2021-05-28 06:10:32 +00:00
foreach ( $request [ 'media_ids' ] as $id ) {
2021-05-15 22:40:57 +00:00
$media = DBA :: toArray ( DBA :: p ( " SELECT `resource-id`, `scale`, `type`, `desc`, `filename`, `datasize`, `width`, `height` FROM `photo`
WHERE `resource-id` IN ( SELECT `resource-id` FROM `photo` WHERE `id` = ? ) AND `photo` . `uid` = ?
ORDER BY `photo` . `width` DESC LIMIT 2 " , $id , $uid ));
2021-05-28 06:10:32 +00:00
2021-05-15 22:40:57 +00:00
if ( empty ( $media )) {
continue ;
}
2021-05-16 14:30:15 +00:00
Photo :: setPermissionForRessource ( $media [ 0 ][ 'resource-id' ], $uid , $item [ 'allow_cid' ], $item [ 'allow_gid' ], $item [ 'deny_cid' ], $item [ 'deny_gid' ]);
2021-05-15 22:40:57 +00:00
$ressources [] = $media [ 0 ][ 'resource-id' ];
$phototypes = Images :: supportedTypes ();
$ext = $phototypes [ $media [ 0 ][ 'type' ]];
2021-05-28 06:10:32 +00:00
2021-05-15 22:40:57 +00:00
$attachment = [ 'type' => Post\Media :: IMAGE , 'mimetype' => $media [ 0 ][ 'type' ],
'url' => DI :: baseUrl () . '/photo/' . $media [ 0 ][ 'resource-id' ] . '-' . $media [ 0 ][ 'scale' ] . '.' . $ext ,
'size' => $media [ 0 ][ 'datasize' ],
'name' => $media [ 0 ][ 'filename' ] ? : $media [ 0 ][ 'resource-id' ],
'description' => $media [ 0 ][ 'desc' ] ? ? '' ,
'width' => $media [ 0 ][ 'width' ],
'height' => $media [ 0 ][ 'height' ]];
2021-05-28 06:10:32 +00:00
2021-05-15 22:40:57 +00:00
if ( count ( $media ) > 1 ) {
$attachment [ 'preview' ] = DI :: baseUrl () . '/photo/' . $media [ 1 ][ 'resource-id' ] . '-' . $media [ 1 ][ 'scale' ] . '.' . $ext ;
$attachment [ 'preview-width' ] = $media [ 1 ][ 'width' ];
$attachment [ 'preview-height' ] = $media [ 1 ][ 'height' ];
}
$item [ 'attachments' ][] = $attachment ;
}
}
2021-07-30 06:22:32 +00:00
if ( ! empty ( $request [ 'scheduled_at' ])) {
$item [ 'guid' ] = Item :: guid ( $item , true );
2022-07-09 07:32:32 -04:00
$item [ 'uri' ] = Item :: newURI ( $item [ 'guid' ]);
2022-10-17 05:49:55 +00:00
$id = Post\Delayed :: add ( $item [ 'uri' ], $item , Worker :: PRIORITY_HIGH , Post\Delayed :: PREPARED , $request [ 'scheduled_at' ]);
2021-07-30 06:22:32 +00:00
if ( empty ( $id )) {
DI :: mstdnError () -> InternalError ();
}
2021-07-30 10:24:08 +00:00
System :: jsonExit ( DI :: mstdnScheduledStatus () -> createFromDelayedPostId ( $id , $uid ) -> toArray ());
2021-07-30 06:22:32 +00:00
}
2021-05-15 22:40:57 +00:00
$id = Item :: insert ( $item , true );
if ( ! empty ( $id )) {
$item = Post :: selectFirst ([ 'uri-id' ], [ 'id' => $id ]);
if ( ! empty ( $item [ 'uri-id' ])) {
2023-01-25 20:14:33 +00:00
System :: jsonExit ( DI :: mstdnStatus () -> createFromUriId ( $item [ 'uri-id' ], $uid , self :: appSupportsQuotes ()));
2021-05-15 22:40:57 +00:00
}
}
DI :: mstdnError () -> InternalError ();
2021-05-13 21:15:32 +00:00
}
2021-11-28 13:44:42 +01:00
protected function delete ( array $request = [])
2021-05-08 09:14:19 +00:00
{
2021-06-08 12:00:22 +00:00
self :: checkAllowedScope ( self :: SCOPE_READ );
2021-05-15 10:08:47 +00:00
$uid = self :: getCurrentUserID ();
2021-11-14 23:19:25 +01:00
if ( empty ( $this -> parameters [ 'id' ])) {
2021-05-15 10:08:47 +00:00
DI :: mstdnError () -> UnprocessableEntity ();
}
2021-11-14 23:19:25 +01:00
$item = Post :: selectFirstForUser ( $uid , [ 'id' ], [ 'uri-id' => $this -> parameters [ 'id' ], 'uid' => $uid ]);
2021-05-15 10:08:47 +00:00
if ( empty ( $item [ 'id' ])) {
DI :: mstdnError () -> RecordNotFound ();
}
if ( ! Item :: markForDeletionById ( $item [ 'id' ])) {
DI :: mstdnError () -> RecordNotFound ();
}
System :: jsonExit ([]);
2021-05-08 09:14:19 +00:00
}
2021-05-08 05:55:06 +00:00
/**
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
2021-11-20 15:38:03 +01:00
protected function rawContent ( array $request = [])
2021-05-08 05:55:06 +00:00
{
2021-06-08 08:28:14 +00:00
$uid = self :: getCurrentUserID ();
2021-11-14 23:19:25 +01:00
if ( empty ( $this -> parameters [ 'id' ])) {
2021-05-12 14:00:15 +00:00
DI :: mstdnError () -> UnprocessableEntity ();
2021-05-08 05:55:06 +00:00
}
2023-01-25 20:14:33 +00:00
System :: jsonExit ( DI :: mstdnStatus () -> createFromUriId ( $this -> parameters [ 'id' ], $uid , self :: appSupportsQuotes (), false ));
2021-05-08 05:55:06 +00:00
}
2022-11-25 19:35:40 +00:00
private function getApp () : string
{
if ( ! empty ( self :: getCurrentApplication ()[ 'name' ])) {
return self :: getCurrentApplication ()[ 'name' ];
} else {
return 'API' ;
}
}
2021-05-08 05:55:06 +00:00
}