Newer
Older
<?php

Yassine Doghri
committed
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\URI;
use Config\Database;
use Essence\Essence;

Yassine Doghri
committed
use Modules\Fediverse\Activities\AcceptActivity;
use Modules\Fediverse\ActivityRequest;
use Modules\Fediverse\Entities\Actor;
use Modules\Fediverse\Entities\PreviewCard;
if (! function_exists('get_webfinger_data')) {
/**
* Retrieve actor webfinger data from username and domain
*/

Yassine Doghri
committed
function get_webfinger_data(string $username, string $domain): ?object
{

Yassine Doghri
committed
$webfingerUri = new URI();
$webfingerUri->setScheme('https');
$webfingerUri->setHost($domain);
$webfingerUri->setPath('/.well-known/webfinger');
$webfingerUri->setQuery("resource=acct:{$username}@{$domain}");

Yassine Doghri
committed
$webfingerRequest = new ActivityRequest((string) $webfingerUri);
$webfingerResponse = $webfingerRequest->get();

Yassine Doghri
committed
return json_decode($webfingerResponse->getBody(), false, 512, JSON_THROW_ON_ERROR);
}
}
if (! function_exists('split_handle')) {
/**
* Splits handle into its parts (username, host and port)
*
* @return array<string, string>|false
*/
function split_handle(string $handle): array | false
{
if (
! preg_match('~^@?(?P<username>[\w\.\-]+)@(?P<domain>[\w\.\-]+)(?P<port>:[\d]+)?$~', $handle, $matches)
) {
return false;
}
return $matches;
}
}
if (! function_exists('accept_follow')) {
/**
* Sends an accept activity to the targetActor's inbox
*

Yassine Doghri
committed
* @param Actor $actor Actor which accepts the follow
* @param Actor $targetActor Actor which receives the accept follow
*/
function accept_follow(Actor $actor, Actor $targetActor, string $objectId): void
{
$acceptActivity = new AcceptActivity();
$acceptActivity->set('actor', $actor->uri)
->set('object', $objectId);
$db = db_connect();
$db->transStart();
$activityModel = model('ActivityModel');
$activityId = $activityModel->newActivity(
'Accept',
$actor->id,
$targetActor->id,
null,
$acceptActivity->toJSON(),
);

Yassine Doghri
committed
$acceptActivity->set('id', url_to('activity', $actor->username, $activityId));
$activityModel->update($activityId, [
'payload' => $acceptActivity->toJSON(),
]);
try {

Yassine Doghri
committed
$acceptRequest = new ActivityRequest($targetActor->inbox_url, $acceptActivity->toJSON());
$acceptRequest->sign($actor->public_key_id, $actor->private_key);
$acceptRequest->post();
} catch (Exception) {
$db->transRollback();
}
$db->transComplete();
}
}
if (! function_exists('send_activity_to_followers')) {
/**
* Sends an activity to all actor followers
*/
function send_activity_to_followers(Actor $actor, string $activityPayload): void
{
foreach ($actor->followers as $follower) {
try {

Yassine Doghri
committed
$acceptRequest = new ActivityRequest($follower->inbox_url, $activityPayload);
$acceptRequest->sign($actor->public_key_id, $actor->private_key);
$acceptRequest->post();
} catch (Exception $exception) {
// log error

Yassine Doghri
committed
log_message('critical', $exception->getMessage());
}
}
}
}
if (! function_exists('extract_urls_from_message')) {
/**
* Returns an array of all urls from a string
*
* @return string[]
*/

Yassine Doghri
committed
function extract_urls_from_message(string $message): array
{

Yassine Doghri
committed
preg_match_all('~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?<![\.,:])~i', $message, $match);
return $match[0];
}
}
if (! function_exists('create_preview_card_from_url')) {
/**
* Extract open graph metadata from given url and create preview card
*/

Yassine Doghri
committed
function create_preview_card_from_url(URI $url): ?PreviewCard
{

Yassine Doghri
committed
$essence = new Essence([
'filters' => [
'OEmbedProvider' => '//',
'OpenGraphProvider' => '//',
'TwitterCardsProvider' => '//',
],
]);
$media = $essence->extract((string) $url);
if ($media) {
$typeMapping = [
'photo' => 'image',
'video' => 'video',
'website' => 'link',
'rich' => 'rich',
];
// Check that, at least, the url and title are set
if ($media->url && $media->title) {

Yassine Doghri
committed
$newPreviewCard = new PreviewCard([
'url' => (string) $url,
'title' => $media->title,
'description' => $media->description,
'type' => isset($typeMapping[$media->type])
? $typeMapping[$media->type]
: 'link',
'author_name' => $media->authorName,
'author_url' => $media->authorUrl,
'provider_name' => $media->providerName,
'provider_url' => $media->providerUrl,
'image' => $media->thumbnailUrl,
'html' => $media->html,
]);
if (
! ($newPreviewCardId = model('PreviewCardModel')->insert($newPreviewCard, true))
) {
return null;
}

Yassine Doghri
committed
$newPreviewCard->id = $newPreviewCardId;
return $newPreviewCard;
}
}
return null;
}
}
if (! function_exists('get_or_create_preview_card_from_url')) {
/**
* Extract open graph metadata from given url and create preview card
*/

Yassine Doghri
committed
function get_or_create_preview_card_from_url(URI $url): ?PreviewCard
{
// check if preview card has already been generated
if (
$previewCard = model('PreviewCardModel')
->getPreviewCardFromUrl((string) $url)
) {
return $previewCard;
}
// create preview card
return create_preview_card_from_url($url);
}
}
if (! function_exists('get_or_create_actor_from_uri')) {
/**
* Retrieves actor from database using the actor uri If Actor is not present, it creates the record in the database
* and returns it.
*/

Yassine Doghri
committed
function get_or_create_actor_from_uri(string $actorUri): ?Actor
{
// check if actor exists in database already and return it
if ($actor = model('ActorModel')->getActorByUri($actorUri)) {
return $actor;
}
// if the actor doesn't exist, request actorUri to create it
return create_actor_from_uri($actorUri);
}
}
if (! function_exists('get_or_create_actor')) {
/**
* Retrieves actor from database using the actor username and domain If actor is not present, it creates the record
* in the database and returns it.
*/

Yassine Doghri
committed
function get_or_create_actor(string $username, string $domain): ?Actor
{
// check if actor exists in database already and return it
if (
$actor = model('ActorModel')
->getActorByUsername($username, $domain)
) {
return $actor;
}
// get actorUri with webfinger request
$webfingerData = get_webfinger_data($username, $domain);

Yassine Doghri
committed
$actorUriKey = array_search('self', array_column($webfingerData->links, 'rel'), true);
return create_actor_from_uri($webfingerData->links[$actorUriKey]->href);
}
}
if (! function_exists('create_actor_from_uri')) {
/**
* Creates actor record in database using the info gathered from the actorUri parameter
*/

Yassine Doghri
committed
function create_actor_from_uri(string $actorUri): ?Actor
{
$activityRequest = new ActivityRequest($actorUri);
$actorResponse = $activityRequest->get();

Yassine Doghri
committed
$actorPayload = json_decode($actorResponse->getBody(), false, 512, JSON_THROW_ON_ERROR);

Yassine Doghri
committed
$newActor = new Actor();
$newActor->uri = $actorUri;
$newActor->username = $actorPayload->preferredUsername;
$newActor->domain = $activityRequest->getDomain();
$newActor->public_key = $actorPayload->publicKey->publicKeyPem;
$newActor->private_key = null;
$newActor->display_name = $actorPayload->name;

Yassine Doghri
committed
$newActor->summary = property_exists($actorPayload, 'summary') ? $actorPayload->summary : null;
if (property_exists($actorPayload, 'icon')) {
$newActor->avatar_image_url = $actorPayload->icon->url;
$newActor->avatar_image_mimetype = $actorPayload->icon->mediaType;
}
if (property_exists($actorPayload, 'image')) {
$newActor->cover_image_url = $actorPayload->image->url;
$newActor->cover_image_mimetype = $actorPayload->image->mediaType;
}
$newActor->inbox_url = $actorPayload->inbox;

Yassine Doghri
committed
$newActor->outbox_url = property_exists($actorPayload, 'outbox') ? $actorPayload->outbox : null;
$newActor->followers_url = property_exists($actorPayload, 'followers') ? $actorPayload->followers : null;
if (! ($newActorId = model('ActorModel')->insert($newActor, true))) {
return null;
}
$newActor->id = $newActorId;
return $newActor;
}
}
if (! function_exists('get_current_domain')) {
/**
* Returns instance's domain name
*
* @throws HTTPException
*/

Yassine Doghri
committed
function get_current_domain(): string
{
$uri = current_url(true);
return $uri->getHost() . ($uri->getPort() ? ':' . $uri->getPort() : '');
}
}
if (! function_exists('extract_text_from_html')) {
/**
* Extracts the text from html content
*/
function extract_text_from_html(string $content): ?string
{

Yassine Doghri
committed
return preg_replace('~\s+~', ' ', strip_tags($content));
}
}

Yassine Doghri
committed
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
if (! function_exists('get_message_from_object')) {
/**
* Gets the message from content, if no content key is present, checks for content in contentMap
*
* TODO: store multiple languages, convert markdown
*
* @return string|false
*/
function get_message_from_object(stdClass $object): string | false
{
if (property_exists($object, 'content')) {
extract_text_from_html($object->content);
return $object->content;
}
$message = '';
if (property_exists($object, 'contentMap')) {
// TODO: update message to be json? (include all languages?)
if (property_exists($object->contentMap, 'en')) {
extract_text_from_html($object->contentMap->en);
$message = $object->contentMap->en;
} else {
$message = current($object->contentMap);
}
}
return $message;
}
}
if (! function_exists('linkify')) {
/**
* Turn all link elements in clickable links. Transforms urls and handles
*
* @param string[] $protocols http/https, twitter
*/
function linkify(string $text, array $protocols = ['http', 'handle']): string
{
$links = [];
// Extract text links for each protocol

Yassine Doghri
committed
foreach ($protocols as $protocol) {
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
$text = match ($protocol) {
'http', 'https' => preg_replace_callback(
'~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?<![\.,:])~i',
function (array $match) use ($protocol, &$links) {
if ($match[1]) {
$protocol = $match[1];
}
$link = $match[2] ?: $match[3];
helper('text');
$link = preg_replace('~^www\.(.+\.)~i', '$1', $link);
return '<' .
array_push(
$links,
anchor(
"{$protocol}://{$link}",
ellipsize(rtrim($link, '/'), 30),
[
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
),
) .
'>';
},
$text,
),
'handle' => preg_replace_callback(
'~(?<!\w)@(?<username>\w++)(?:@(?<domain>(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]))?~',
function ($match) use (&$links) {
// check if host is set and look for actor in database
if (isset($match['host'])) {
if (
$actor = model(
'ActorModel',
)->getActorByUsername($match['username'], $match['domain'])
) {
// TODO: check that host is local to remove target blank?
return '<' .
array_push(
$links,
anchor($actor->uri, $match[0], [
'target' => '_blank',
'rel' => 'noopener noreferrer',
]),
) .
'>';
}
try {
$actor = get_or_create_actor($match['username'], $match['domain']);
return '<' .
array_push(
$links,
anchor($actor->uri, $match[0], [
'target' => '_blank',
'rel' => 'noopener noreferrer',
]),
) .
'>';
} catch (HTTPException) {
// Couldn't retrieve actor, do not wrap the text in link
return '<' .
array_push($links, $match[0]) .
'>';
}
} else {
if (
$actor = model('ActorModel')
->getActorByUsername($match['username'])
) {
return '<' .
array_push($links, anchor($actor->uri, $match[0])) .
'>';
}
return '<' .
array_push($links, $match[0]) .
'>';
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
}
},
$text,
),
default => preg_replace_callback(
'~' .
preg_quote($protocol, '~') .
'://([^\s<]+?)(?<![\.,:])~i',
function (array $match) use ($protocol, &$links) {
return '<' .
array_push(
$links,
anchor(
"{$protocol}://{$match[1]}",
$match[1],
[
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
),
) .
'>';
},
$text,
),
};
}
// Insert all links
return preg_replace_callback(

Yassine Doghri
committed
'~<(\d+)>~',
function ($match) use (&$links) {
return $links[$match[1] - 1];
},
$text,
);
}
}