2020-09-30 17:37:46 +00:00
< ? php
/**
2023-01-01 14:36:24 +00:00
* @ copyright Copyright ( C ) 2010 - 2023 , the Friendica project
2020-09-30 17:37:46 +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\Protocol ;
2023-08-28 15:37:20 +00:00
use Friendica\Content\Smilies ;
2020-09-30 17:37:46 +00:00
use Friendica\Content\Text\BBCode ;
2023-11-03 22:54:29 +00:00
use Friendica\Core\Cache\Enum\Duration ;
2020-09-30 17:37:46 +00:00
use Friendica\Core\Logger ;
2020-11-15 23:28:05 +00:00
use Friendica\Core\Protocol ;
use Friendica\Database\DBA ;
2020-09-30 17:37:46 +00:00
use Friendica\DI ;
2020-11-15 23:28:05 +00:00
use Friendica\Model\APContact ;
2020-10-02 03:35:22 +00:00
use Friendica\Model\Contact ;
2020-11-15 23:28:05 +00:00
use Friendica\Model\GServer ;
2022-09-19 10:36:12 +00:00
use Friendica\Model\Item ;
2021-01-16 04:14:58 +00:00
use Friendica\Model\Post ;
2020-09-30 17:37:46 +00:00
use Friendica\Model\Search ;
2020-11-15 23:28:05 +00:00
use Friendica\Model\Tag ;
2023-11-03 22:54:29 +00:00
use Friendica\Model\User ;
2020-11-15 23:28:05 +00:00
use Friendica\Util\DateTimeFormat ;
use Friendica\Util\Strings ;
2020-09-30 17:37:46 +00:00
/**
* Base class for relay handling
2021-11-04 20:29:59 +00:00
* @ see https :// github . com / jaywink / social - relay
* @ see https :// wiki . diasporafoundation . org / Relay_servers_for_public_posts
2020-09-30 17:37:46 +00:00
*/
2020-10-01 21:14:26 +00:00
class Relay
2020-09-30 17:37:46 +00:00
{
2021-11-04 20:29:59 +00:00
const SCOPE_NONE = '' ;
const SCOPE_ALL = 'all' ;
const SCOPE_TAGS = 'tags' ;
2020-09-30 17:37:46 +00:00
/**
* Check if a post is wanted
*
2023-11-05 19:18:10 +00:00
* @ param array $tags
2020-09-30 17:37:46 +00:00
* @ param string $body
2023-11-05 19:18:10 +00:00
* @ param int $authorid
2020-09-30 17:37:46 +00:00
* @ param string $url
2023-11-05 19:18:10 +00:00
* @ param string $network
* @ param int $causerid
* @ param array $languages
2020-09-30 17:37:46 +00:00
* @ return boolean " true " is the post is wanted by the system
*/
2023-11-05 19:18:10 +00:00
public static function isSolicitedPost ( array $tags , string $body , int $authorid , string $url , string $network = '' , int $causerid = 0 , array $languages = []) : bool
2020-09-30 17:37:46 +00:00
{
$config = DI :: config ();
2023-11-21 23:13:26 +00:00
if ( Contact :: hasFollowers ( $authorid )) {
Logger :: info ( 'Author has got followers on this server - accepted' , [ 'network' => $network , 'url' => $url , 'author' => $authorid , 'tags' => $tags ]);
return true ;
}
2021-06-13 11:15:04 +00:00
$scope = $config -> get ( 'system' , 'relay_scope' );
2020-09-30 17:37:46 +00:00
2021-11-04 20:29:59 +00:00
if ( $scope == self :: SCOPE_NONE ) {
2020-09-30 17:37:46 +00:00
Logger :: info ( 'Server does not accept relay posts - rejected' , [ 'network' => $network , 'url' => $url ]);
return false ;
}
2020-10-02 03:35:22 +00:00
if ( Contact :: isBlocked ( $authorid )) {
Logger :: info ( 'Author is blocked - rejected' , [ 'author' => $authorid , 'network' => $network , 'url' => $url ]);
return false ;
}
if ( Contact :: isHidden ( $authorid )) {
Logger :: info ( 'Author is hidden - rejected' , [ 'author' => $authorid , 'network' => $network , 'url' => $url ]);
return false ;
}
2022-12-06 17:45:18 +00:00
if ( ! empty ( $causerid )) {
$contact = Contact :: getById ( $causerid , [ 'url' ]);
$causer = $contact [ 'url' ] ? ? '' ;
} else {
$causer = '' ;
}
2022-09-19 10:36:12 +00:00
$body = ActivityPub\Processor :: normalizeMentionLinks ( $body );
2020-09-30 17:37:46 +00:00
$denyTags = [];
2021-11-04 20:29:59 +00:00
if ( $scope == self :: SCOPE_TAGS ) {
2023-09-17 19:28:38 +00:00
$tagList = self :: getSubscribedTags ();
} else {
$tagList = [];
2020-09-30 17:37:46 +00:00
}
$deny_tags = $config -> get ( 'system' , 'relay_deny_tags' );
$tagitems = explode ( ',' , mb_strtolower ( $deny_tags ));
2021-10-03 10:34:41 +00:00
foreach ( $tagitems as $tag ) {
2020-09-30 17:37:46 +00:00
$tag = trim ( $tag , '# ' );
$denyTags [] = $tag ;
}
if ( ! empty ( $tagList ) || ! empty ( $denyTags )) {
$content = mb_strtolower ( BBCode :: toPlaintext ( $body , false ));
foreach ( $tags as $tag ) {
2020-10-01 21:14:26 +00:00
$tag = mb_strtolower ( $tag );
2020-09-30 17:37:46 +00:00
if ( in_array ( $tag , $denyTags )) {
2022-12-06 17:45:18 +00:00
Logger :: info ( 'Unwanted hashtag found - rejected' , [ 'hashtag' => $tag , 'network' => $network , 'url' => $url , 'causer' => $causer ]);
2020-09-30 17:37:46 +00:00
return false ;
}
if ( in_array ( $tag , $tagList )) {
2022-12-06 17:45:18 +00:00
Logger :: info ( 'Subscribed hashtag found - accepted' , [ 'hashtag' => $tag , 'network' => $network , 'url' => $url , 'causer' => $causer ]);
2020-09-30 17:37:46 +00:00
return true ;
}
// We check with "strpos" for performance issues. Only when this is true, the regular expression check is used
// RegExp is taken from here: https://medium.com/@shiba1014/regex-word-boundaries-with-unicode-207794f6e7ed
if (( strpos ( $content , $tag ) !== false ) && preg_match ( '/(?<=[\s,.:;"\']|^)' . preg_quote ( $tag , '/' ) . '(?=[\s,.:;"\']|$)/' , $content )) {
2022-12-06 17:45:18 +00:00
Logger :: info ( 'Subscribed hashtag found in content - accepted' , [ 'hashtag' => $tag , 'network' => $network , 'url' => $url , 'causer' => $causer ]);
2020-09-30 17:37:46 +00:00
return true ;
}
}
}
2023-11-05 19:18:10 +00:00
if ( ! self :: isWantedLanguage ( $body , 0 , $authorid , $languages )) {
2023-08-02 21:48:31 +00:00
Logger :: info ( 'Unwanted or Undetected language found - rejected' , [ 'network' => $network , 'url' => $url , 'causer' => $causer , 'tags' => $tags ]);
2023-06-04 17:18:43 +00:00
return false ;
}
if ( $scope == self :: SCOPE_ALL ) {
2023-08-02 21:48:31 +00:00
Logger :: info ( 'Server accept all posts - accepted' , [ 'network' => $network , 'url' => $url , 'causer' => $causer , 'tags' => $tags ]);
2023-06-04 17:18:43 +00:00
return true ;
}
2023-08-02 21:48:31 +00:00
Logger :: info ( 'No matching hashtags found - rejected' , [ 'network' => $network , 'url' => $url , 'causer' => $causer , 'tags' => $tags ]);
2023-06-04 17:18:43 +00:00
return false ;
}
2023-09-17 19:28:38 +00:00
/**
* Get a list of subscribed tags by both the users and the tags that are defined by the admin
*
* @ return array
*/
public static function getSubscribedTags () : array
{
$systemTags = [];
$server_tags = DI :: config () -> get ( 'system' , 'relay_server_tags' );
foreach ( explode ( ',' , mb_strtolower ( $server_tags )) as $tag ) {
$systemTags [] = trim ( $tag , '# ' );
}
if ( DI :: config () -> get ( 'system' , 'relay_user_tags' )) {
$userTags = Search :: getUserTags ();
} else {
$userTags = [];
}
return array_unique ( array_merge ( $systemTags , $userTags ));
}
2023-06-04 17:18:43 +00:00
/**
* Detect the language of a post and decide if the post should be accepted
*
* @ param string $body
2023-10-08 06:44:37 +00:00
* @ param int $uri_id
* @ param int $author_id
2023-11-05 19:18:10 +00:00
* @ param array $languages
2023-06-04 17:18:43 +00:00
* @ return boolean
*/
2023-11-05 19:18:10 +00:00
public static function isWantedLanguage ( string $body , int $uri_id = 0 , int $author_id = 0 , array $languages = [])
2023-06-04 17:18:43 +00:00
{
2023-11-05 19:18:10 +00:00
$detected = [];
$quality = DI :: config () -> get ( 'system' , 'relay_language_quality' );
2023-11-05 20:18:01 +00:00
foreach ( Item :: getLanguageArray ( $body , DI :: config () -> get ( 'system' , 'relay_languages' ), $uri_id , $author_id ) as $language => $reliability ) {
2023-11-05 19:18:10 +00:00
if (( $reliability >= $quality ) && ( $quality > 0 )) {
$detected [] = $language ;
2022-09-19 10:36:12 +00:00
}
}
2023-11-05 23:05:33 +00:00
if ( empty ( $languages ) && empty ( $detected ) && ( empty ( $body ) || Smilies :: isEmojiPost ( $body ))) {
Logger :: debug ( 'Empty body or only emojis' , [ 'body' => $body ]);
return true ;
}
2023-11-05 19:18:10 +00:00
if ( ! empty ( $languages ) || ! empty ( $detected )) {
2023-11-15 16:19:05 +00:00
$user_languages = User :: getLanguages ();
2023-11-03 22:54:29 +00:00
2023-11-05 19:18:10 +00:00
foreach ( $detected as $language ) {
if ( in_array ( $language , $user_languages )) {
Logger :: debug ( 'Wanted language found in detected languages' , [ 'language' => $language , 'detected' => $detected , 'userlang' => $user_languages , 'body' => $body ]);
return true ;
}
}
2023-11-03 22:54:29 +00:00
foreach ( $languages as $language ) {
if ( in_array ( $language , $user_languages )) {
2023-11-05 19:18:10 +00:00
Logger :: debug ( 'Wanted language found in defined languages' , [ 'language' => $language , 'languages' => $languages , 'detected' => $detected , 'userlang' => $user_languages , 'body' => $body ]);
2023-11-03 22:54:29 +00:00
return true ;
}
2022-09-19 10:36:12 +00:00
}
2023-11-05 19:18:10 +00:00
Logger :: debug ( 'No wanted language found' , [ 'languages' => $languages , 'detected' => $detected , 'userlang' => $user_languages , 'body' => $body ]);
2023-11-03 22:54:29 +00:00
return false ;
2023-06-04 17:18:43 +00:00
} elseif ( DI :: config () -> get ( 'system' , 'relay_deny_undetected_language' )) {
Logger :: info ( 'Undetected language found' , [ 'body' => $body ]);
2022-09-19 10:36:12 +00:00
return false ;
}
2023-06-04 17:18:43 +00:00
return true ;
2020-09-30 17:37:46 +00:00
}
2020-11-15 23:28:05 +00:00
/**
* Update or insert a relay contact
*
* @ param array $gserver Global server record
* @ param array $fields Optional network specific fields
2022-06-22 03:44:57 +00:00
* @ return void
2020-11-15 23:28:05 +00:00
* @ throws \Exception
*/
public static function updateContact ( array $gserver , array $fields = [])
{
if ( in_array ( $gserver [ 'network' ], [ Protocol :: ACTIVITYPUB , Protocol :: DFRN ])) {
$system = APContact :: getByURL ( $gserver [ 'url' ] . '/friendica' );
if ( ! empty ( $system [ 'sharedinbox' ])) {
2023-03-22 04:08:33 +00:00
Logger :: info ( 'Successfully probed for relay contact' , [ 'server' => $gserver [ 'url' ]]);
2020-11-15 23:28:05 +00:00
$id = Contact :: updateFromProbeByURL ( $system [ 'url' ]);
Logger :: info ( 'Updated relay contact' , [ 'server' => $gserver [ 'url' ], 'id' => $id ]);
return ;
}
}
$condition = [ 'uid' => 0 , 'gsid' => $gserver [ 'id' ], 'contact-type' => Contact :: TYPE_RELAY ];
$old = DBA :: selectFirst ( 'contact' , [], $condition );
if ( ! DBA :: isResult ( $old )) {
$condition = [ 'uid' => 0 , 'nurl' => Strings :: normaliseLink ( $gserver [ 'url' ])];
$old = DBA :: selectFirst ( 'contact' , [], $condition );
if ( DBA :: isResult ( $old )) {
$fields [ 'gsid' ] = $gserver [ 'id' ];
$fields [ 'contact-type' ] = Contact :: TYPE_RELAY ;
Logger :: info ( 'Assigning missing data for relay contact' , [ 'server' => $gserver [ 'url' ], 'id' => $old [ 'id' ]]);
}
} elseif ( empty ( $fields )) {
Logger :: info ( 'No content to update, quitting' , [ 'server' => $gserver [ 'url' ]]);
return ;
}
2021-06-13 11:15:04 +00:00
if ( DBA :: isResult ( $old )) {
2020-11-15 23:28:05 +00:00
$fields [ 'updated' ] = DateTimeFormat :: utcNow ();
Logger :: info ( 'Update relay contact' , [ 'server' => $gserver [ 'url' ], 'id' => $old [ 'id' ], 'fields' => $fields ]);
2021-09-10 18:21:19 +00:00
Contact :: update ( $fields , [ 'id' => $old [ 'id' ]], $old );
2020-11-15 23:28:05 +00:00
} else {
$default = [ 'created' => DateTimeFormat :: utcNow (),
'name' => 'relay' , 'nick' => 'relay' , 'url' => $gserver [ 'url' ],
'nurl' => Strings :: normaliseLink ( $gserver [ 'url' ]),
'network' => Protocol :: DIASPORA , 'uid' => 0 ,
'batch' => $gserver [ 'url' ] . '/receive/public' ,
'rel' => Contact :: FOLLOWER , 'blocked' => false ,
'pending' => false , 'writable' => true ,
'gsid' => $gserver [ 'id' ],
'baseurl' => $gserver [ 'url' ], 'contact-type' => Contact :: TYPE_RELAY ];
$fields = array_merge ( $default , $fields );
Logger :: info ( 'Create relay contact' , [ 'server' => $gserver [ 'url' ], 'fields' => $fields ]);
Contact :: insert ( $fields );
}
}
/**
* Mark the relay contact of the given contact for archival
* This is called whenever there is a communication issue with the server .
* It avoids sending stuff to servers who don ' t exist anymore .
* The relay contact is a technical contact entry that exists once per server .
*
* @ param array $contact of the relay contact
2022-06-22 03:44:57 +00:00
* @ return void
2020-11-15 23:28:05 +00:00
*/
public static function markForArchival ( array $contact )
{
if ( ! empty ( $contact [ 'contact-type' ]) && ( $contact [ 'contact-type' ] == Contact :: TYPE_RELAY )) {
// This is already the relay contact, we don't need to fetch it
$relay_contact = $contact ;
} elseif ( empty ( $contact [ 'baseurl' ])) {
if ( ! empty ( $contact [ 'batch' ])) {
$condition = [ 'uid' => 0 , 'network' => Protocol :: FEDERATED , 'batch' => $contact [ 'batch' ], 'contact-type' => Contact :: TYPE_RELAY ];
$relay_contact = DBA :: selectFirst ( 'contact' , [], $condition );
} else {
return ;
}
} else {
$gserver = [ 'id' => $contact [ 'gsid' ] ? : GServer :: getID ( $contact [ 'baseurl' ], true ),
'url' => $contact [ 'baseurl' ], 'network' => $contact [ 'network' ]];
$relay_contact = self :: getContact ( $gserver , []);
}
if ( ! empty ( $relay_contact )) {
Logger :: info ( 'Relay contact will be marked for archival' , [ 'id' => $relay_contact [ 'id' ], 'url' => $relay_contact [ 'url' ]]);
Contact :: markForArchival ( $relay_contact );
}
}
/**
2021-06-13 11:15:04 +00:00
* Return a list of servers that we serve via the direct relay
2020-11-15 23:28:05 +00:00
*
* @ param integer $item_id id of the item that is sent
* @ param array $contacts Previously fetched contacts
2021-06-13 11:15:04 +00:00
* @ param array $networks Networks of the relay servers
2020-11-15 23:28:05 +00:00
* @ return array of relay servers
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
2022-06-22 03:44:57 +00:00
public static function getDirectRelayList ( int $item_id ) : array
2020-11-15 23:28:05 +00:00
{
$serverlist = [];
2022-06-22 03:44:57 +00:00
if ( ! DI :: config () -> get ( 'system' , 'relay_directly' , false )) {
2021-06-13 11:15:04 +00:00
return [];
}
2020-11-15 23:28:05 +00:00
2021-06-13 11:15:04 +00:00
// We distribute our stuff based on the parent to ensure that the thread will be complete
$parent = Post :: selectFirst ([ 'uri-id' ], [ 'id' => $item_id ]);
if ( ! DBA :: isResult ( $parent )) {
return [];
2020-11-15 23:28:05 +00:00
}
2021-06-13 11:15:04 +00:00
// Servers that want to get all content
$servers = DBA :: select ( 'gserver' , [ 'id' , 'url' , 'network' ], [ 'relay-subscribe' => true , 'relay-scope' => 'all' ]);
while ( $server = DBA :: fetch ( $servers )) {
$serverlist [ $server [ 'id' ]] = $server ;
}
DBA :: close ( $servers );
// All tags of the current post
$tags = DBA :: select ( 'tag-view' , [ 'name' ], [ 'uri-id' => $parent [ 'uri-id' ], 'type' => Tag :: HASHTAG ]);
$taglist = [];
while ( $tag = DBA :: fetch ( $tags )) {
$taglist [] = $tag [ 'name' ];
}
DBA :: close ( $tags );
// All servers who wants content with this tag
$tagserverlist = [];
if ( ! empty ( $taglist )) {
$tagserver = DBA :: select ( 'gserver-tag' , [ 'gserver-id' ], [ 'tag' => $taglist ]);
while ( $server = DBA :: fetch ( $tagserver )) {
$tagserverlist [] = $server [ 'gserver-id' ];
2020-11-15 23:28:05 +00:00
}
2021-06-13 11:15:04 +00:00
DBA :: close ( $tagserver );
}
2020-11-15 23:28:05 +00:00
2023-03-22 03:16:42 +00:00
// All addresses with the given id
2021-06-13 11:15:04 +00:00
if ( ! empty ( $tagserverlist )) {
$servers = DBA :: select ( 'gserver' , [ 'id' , 'url' , 'network' ], [ 'relay-subscribe' => true , 'relay-scope' => 'tags' , 'id' => $tagserverlist ]);
2020-11-15 23:28:05 +00:00
while ( $server = DBA :: fetch ( $servers )) {
$serverlist [ $server [ 'id' ]] = $server ;
}
DBA :: close ( $servers );
}
2021-06-13 11:15:04 +00:00
$contacts = [];
2020-11-15 23:28:05 +00:00
// Now we are collecting all relay contacts
foreach ( $serverlist as $gserver ) {
// We don't send messages to ourselves
if ( Strings :: compareLink ( $gserver [ 'url' ], DI :: baseUrl ())) {
continue ;
}
$contact = self :: getContact ( $gserver );
if ( empty ( $contact )) {
continue ;
}
}
return $contacts ;
}
2021-06-13 11:15:04 +00:00
/**
* Return a list of relay servers
*
* @ param array $fields Field list
2022-06-22 03:44:57 +00:00
* @ return array List of relay servers
2023-01-01 14:36:24 +00:00
* @ throws Exception
2021-06-13 11:15:04 +00:00
*/
2022-06-22 03:44:57 +00:00
public static function getList ( array $fields = []) : array
2021-06-13 11:15:04 +00:00
{
return DBA :: selectToArray ( 'apcontact' , $fields ,
2023-07-30 17:46:37 +00:00
[ " `type` IN (?, ?) AND `url` IN (SELECT `url` FROM `contact` WHERE `uid` = ? AND `rel` = ?) " , 'Application' , 'Service' , 0 , Contact :: FRIEND ]);
2021-06-13 11:15:04 +00:00
}
2020-11-15 23:28:05 +00:00
/**
* Return a contact for a given server address or creates a dummy entry
*
* @ param array $gserver Global server record
* @ param array $fields Fieldlist
2022-06-22 03:44:57 +00:00
* @ return array | bool Array with the contact or false on error
2020-11-15 23:28:05 +00:00
* @ throws \Exception
*/
private static function getContact ( array $gserver , array $fields = [ 'batch' , 'id' , 'url' , 'name' , 'network' , 'protocol' , 'archive' , 'blocked' ])
{
// Fetch the relay contact
$condition = [ 'uid' => 0 , 'gsid' => $gserver [ 'id' ], 'contact-type' => Contact :: TYPE_RELAY ];
$contact = DBA :: selectFirst ( 'contact' , $fields , $condition );
if ( DBA :: isResult ( $contact )) {
if ( $contact [ 'archive' ] || $contact [ 'blocked' ]) {
return false ;
}
return $contact ;
} else {
self :: updateContact ( $gserver );
$contact = DBA :: selectFirst ( 'contact' , $fields , $condition );
if ( DBA :: isResult ( $contact )) {
return $contact ;
}
}
// It should never happen that we arrive here
return [];
}
2022-05-24 07:02:42 +00:00
/**
* Resubscribe to all relay servers
2022-06-22 03:44:57 +00:00
*
* @ return void
2022-05-24 07:02:42 +00:00
*/
public static function reSubscribe ()
{
foreach ( self :: getList () as $server ) {
$success = ActivityPub\Transmitter :: sendRelayFollow ( $server [ 'url' ]);
Logger :: debug ( 'Resubscribed' , [ 'profile' => $server [ 'url' ], 'success' => $success ]);
2023-01-01 14:36:24 +00:00
}
2022-05-24 07:02:42 +00:00
}
2020-09-30 17:37:46 +00:00
}