Merge pull request #8074 from MrPetovan/task/domain-driven

Domain-driven-design implementation 4th iteration
This commit is contained in:
Philipp 2020-01-08 18:00:50 +01:00 committed by GitHub
commit 2ea1c95e3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1178 additions and 394 deletions

View File

@ -0,0 +1,239 @@
Domain-Driven-Design
==============
* [Home](help)
* [Developer Intro](help/Developers-Intro)
Friendica uses class structures inspired by Domain-Driven-Design programming patterns.
This page is meant to explain what it means in practical terms for Friendica development.
## Inspiration
- https://designpatternsphp.readthedocs.io/en/latest/Structural/DependencyInjection/README.html
- https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
- https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html
- https://designpatternsphp.readthedocs.io/en/latest/Creational/FactoryMethod/README.html
- https://designpatternsphp.readthedocs.io/en/latest/Creational/Prototype/README.html
## Core concepts
### Models and Collections
Instead of anonymous arrays of arrays of database field values, we have Models and collections to take full advantage of PHP type hints.
Before:
```php
function doSomething(array $intros)
{
foreach ($intros as $intro) {
$introId = $intro['id'];
}
}
$intros = \Friendica\Database\DBA::selectToArray('intros', [], ['uid' => local_user()]);
doSomething($intros);
```
After:
```php
function doSomething(\Friendica\Collection\Introductions $intros)
{
foreach ($intros as $intro) {
/** @var $intro \Friendica\Model\Introduction */
$introId = $intro->id;
}
}
/** @var $intros \Friendica\Collection\Introductions */
$intros = \Friendica\DI::intro()->select(['uid' => local_user()]);
doSomething($intros);
```
### Dependency Injection
Under this concept, we want class objects to carry with them the dependencies they will use.
Instead of calling global/static function/methods, objects use their own class members.
Before:
```php
class Model
{
public $id;
function save()
{
return \Friendica\Database\DBA::update('table', get_object_vars($this), ['id' => $this->id]);
}
}
```
After:
```php
class Model
{
/**
* @var \Friendica\Database\Database
*/
protected $dba;
public $id;
function __construct(\Friendica\Database\Database $dba)
{
$this->dba = $dba;
}
function save()
{
return $this->dba->update('table', get_object_vars($this), ['id' => $this->id]);
}
}
```
The main advantage is testability.
Another one is avoiding dependency circles and avoid implicit initializing.
In the first example the method `save()` has to be tested with the `DBA::update()` method, which may or may not have dependencies itself.
In the second example we can mock `\Friendica\Database\Database`, e.g. overload the class by replacing its methods by placeholders, which allows us to test only `Model::save()` and nothing else implicitly.
The main drawback is lengthy constructors for dependency-heavy classes.
To alleviate this issue we are using [DiCe](https://r.je/dice) to simplify the instantiation of the higher level objects Friendica uses.
We also added a convenience factory named `\Friendica\DI` that creates some of the most common objects used in modules.
### Factories
Since we added a bunch of parameters to class constructors, instantiating objects has become cumbersome.
To keep it simple, we are using Factories.
Factories are classes used to generate other objects, centralizing the dependencies required in their constructor.
Factories encapsulate more or less complex creation of objects and create them redundancy free.
Before:
```php
$model = new Model(\Friendica\DI::dba());
$model->id = 1;
$model->key = 'value';
$model->save();
```
After:
```php
class Factory
{
/**
* @var \Friendica\Database\Database
*/
protected $dba;
function __construct(\Friendica\Database\Database $dba)
{
$this->dba;
}
public function create()
{
return new Model($this->dba);
}
}
$model = \Friendica\DI::factory()->create();
$model->id = 1;
$model->key = 'value';
$model->save();
```
Here, `DI::factory()` returns an instance of `Factory` that can then be used to create a `Model` object without having to care about its dependencies.
### Repositories
Last building block of our code architecture, repositories are meant as the interface between models and how they are stored.
In Friendica they are stored in a relational database but repositories allow models not to have to care about it.
Repositories also act as factories for the Model they are managing.
Before:
```php
class Model
{
/**
* @var \Friendica\Database\Database
*/
protected $dba;
public $id;
function __construct(\Friendica\Database\Database $dba)
{
$this->dba = $dba;
}
function save()
{
return $this->dba->update('table', get_object_vars($this), ['id' => $this->id]);
}
}
class Factory
{
/**
* @var \Friendica\Database\Database
*/
protected $dba;
function __construct(\Friendica\Database\Database $dba)
{
$this->dba;
}
public function create()
{
return new Model($this->dba);
}
}
$model = \Friendica\DI::factory()->create();
$model->id = 1;
$model->key = 'value';
$model->save();
```
After:
```php
class Model {
public $id;
}
class Repository extends Factory
{
/**
* @var \Friendica\Database\Database
*/
protected $dba;
function __construct(\Friendica\Database\Database $dba)
{
$this->dba;
}
public function create()
{
return new Model($this->dba);
}
public function save(Model $model)
{
return $this->dba->update('table', get_object_vars($model), ['id' => $model->id]);
}
}
$model = \Friendica\DI::repository()->create();
$model->id = 1;
$model->key = 'value';
\Friendica\DI::repository()->save($model);
```

View File

@ -41,6 +41,8 @@ If you have seen Friendica you probably have ideas to improve it, haven't you?
## Programming
Friendica uses an implementation of [Domain-Driven-Design](help/Developer-Domain-Driven-Design), please make sure to check out the provided links for hints at the expected code architecture.
### Composer
Friendica uses [Composer](https://getcomposer.org) to manage dependencies libraries and the class autoloader both for libraries and namespaced Friendica classes.

View File

@ -45,6 +45,7 @@ Friendica Documentation and Resources
* [Help on Vagrant](help/Vagrant)
* [Bugs and Issues](help/Bugs-and-Issues)
* Code structure
* [Domain-Driven-Design](help/Developer-Domain-Driven-Design)
* [Addon Development](help/Addons)
* [Theme Development](help/themes)
* [Smarty 3 Templates](help/smarty3-templates)

View File

@ -31,14 +31,14 @@ function notifications_post(App $a)
}
if ($request_id) {
$Intro = DI::intro()->fetch(['id' => $request_id, 'uid' => local_user()]);
$intro = DI::intro()->selectFirst(['id' => $request_id, 'uid' => local_user()]);
switch ($_POST['submit']) {
case L10n::t('Discard'):
$Intro->discard();
$intro->discard();
break;
case L10n::t('Ignore'):
$Intro->ignore();
$intro->ignore();
break;
}

18
src/Api/BaseEntity.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace Friendica\Api;
/**
* The API entity classes are meant as data transfer objects. As such, their member should be protected.
* Then the JsonSerializable interface ensures the protected members will be included in a JSON encode situation.
*
* Constructors are supposed to take as arguments the Friendica dependencies/model/collection/data it needs to
* populate the class members.
*/
abstract class BaseEntity implements \JsonSerializable
{
public function jsonSerialize()
{
return get_object_vars($this);
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace Friendica\Api\Entity\Mastodon;
use Friendica\Api\BaseEntity;
use Friendica\App\BaseURL;
use Friendica\Content\Text\BBCode;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Util\DateTimeFormat;
/**
* Class Account
*
* @see https://docs.joinmastodon.org/entities/account
*/
class Account extends BaseEntity
{
/** @var string */
protected $id;
/** @var string */
protected $username;
/** @var string */
protected $acct;
/** @var string */
protected $display_name;
/** @var bool */
protected $locked;
/** @var string (Datetime) */
protected $created_at;
/** @var int */
protected $followers_count;
/** @var int */
protected $following_count;
/** @var int */
protected $statuses_count;
/** @var string */
protected $note;
/** @var string (URL)*/
protected $url;
/** @var string (URL) */
protected $avatar;
/** @var string (URL) */
protected $avatar_static;
/** @var string (URL) */
protected $header;
/** @var string (URL) */
protected $header_static;
/** @var Emoji[] */
protected $emojis;
/** @var Account|null */
protected $moved = null;
/** @var Field[]|null */
protected $fields = null;
/** @var bool|null */
protected $bot = null;
/** @var bool */
protected $group;
/** @var bool */
protected $discoverable;
/** @var string|null (Datetime) */
protected $last_status_at = null;
/**
* Creates an account record from a public contact record. Expects all contact table fields to be set.
*
* @param BaseURL $baseUrl
* @param array $publicContact Full contact table record with uid = 0
* @param array $apcontact Optional full apcontact table record
* @param array $userContact Optional full contact table record with uid != 0
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function __construct(BaseURL $baseUrl, array $publicContact, array $apcontact = [], array $userContact = [])
{
$this->id = $publicContact['id'];
$this->username = $publicContact['nick'];
$this->acct =
strpos($publicContact['url'], $baseUrl->get() . '/') === 0 ?
$publicContact['nick'] :
$publicContact['addr'];
$this->display_name = $publicContact['name'];
$this->locked = !empty($apcontact['manually-approve']);
$this->created_at = DateTimeFormat::utc($publicContact['created'], DateTimeFormat::ATOM);
$this->followers_count = $apcontact['followers_count'] ?? 0;
$this->following_count = $apcontact['following_count'] ?? 0;
$this->statuses_count = $apcontact['statuses_count'] ?? 0;
$this->note = BBCode::convert($publicContact['about'], false);
$this->url = $publicContact['url'];
$this->avatar = $userContact['avatar'] ?? $publicContact['avatar'];
$this->avatar_static = $userContact['avatar'] ?? $publicContact['avatar'];
// No header picture in Friendica
$this->header = '';
$this->header_static = '';
// No custom emojis per account in Friendica
$this->emojis = [];
// No metadata fields in Friendica
$this->fields = [];
$this->bot = ($publicContact['contact-type'] == Contact::TYPE_NEWS);
$this->group = ($publicContact['contact-type'] == Contact::TYPE_COMMUNITY);
$this->discoverable = !$publicContact['unsearchable'];
$publicContactLastItem = $publicContact['last-item'] ?: DBA::NULL_DATETIME;
$userContactLastItem = $userContact['last-item'] ?? DBA::NULL_DATETIME;
$lastItem = $userContactLastItem > $publicContactLastItem ? $userContactLastItem : $publicContactLastItem;
$this->last_status_at = $lastItem != DBA::NULL_DATETIME ? DateTimeFormat::utc($lastItem, DateTimeFormat::ATOM) : null;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Friendica\Api\Entity\Mastodon;
use Friendica\Api\BaseEntity;
/**
* Class Emoji
*
* @see https://docs.joinmastodon.org/api/entities/#emoji
*/
class Emoji extends BaseEntity
{
/** @var string */
protected $shortcode;
/** @var string (URL)*/
protected $static_url;
/** @var string (URL)*/
protected $url;
/** @var bool */
protected $visible_in_picker;
}

View File

@ -1,18 +1,20 @@
<?php
namespace Friendica\Api\Mastodon;
namespace Friendica\Api\Entity\Mastodon;
use Friendica\Api\BaseEntity;
/**
* Class Field
*
* @see https://docs.joinmastodon.org/api/entities/#field
*/
class Field
class Field extends BaseEntity
{
/** @var string */
var $name;
protected $name;
/** @var string (HTML) */
var $value;
protected $value;
/** @var string (Datetime)*/
var $verified_at;
protected $verified_at;
}

View File

@ -0,0 +1,32 @@
<?php
namespace Friendica\Api\Entity\Mastodon;
use Friendica\App\BaseURL;
use Friendica\Model\Introduction;
/**
* Virtual entity to separate Accounts from Follow Requests.
* In the Mastodon API they are one and the same.
*/
class FollowRequest extends Account
{
/**
* Creates a follow request entity from an introduction record.
*
* The account ID is set to the Introduction ID to allow for later interaction with follow requests.
*
* @param BaseURL $baseUrl
* @param int $introduction_id Introduction record id
* @param array $publicContact Full contact table record with uid = 0
* @param array $apcontact Optional full apcontact table record
* @param array $userContact Optional full contact table record with uid != 0
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function __construct(BaseURL $baseUrl, int $introduction_id, array $publicContact, array $apcontact = [], array $userContact = [])
{
parent::__construct($baseUrl, $publicContact, $apcontact, $userContact);
$this->id = $introduction_id;
}
}

View File

@ -1,12 +1,11 @@
<?php
namespace Friendica\Api\Mastodon;
namespace Friendica\Api\Entity\Mastodon;
use Friendica\App;
use Friendica\Api\BaseEntity;
use Friendica\Core\Config;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\APContact;
use Friendica\Model\User;
use Friendica\Module\Register;
@ -15,42 +14,41 @@ use Friendica\Module\Register;
*
* @see https://docs.joinmastodon.org/api/entities/#instance
*/
class Instance
class Instance extends BaseEntity
{
/** @var string (URL) */
var $uri;
protected $uri;
/** @var string */
var $title;
protected $title;
/** @var string */
var $description;
protected $description;
/** @var string */
var $email;
protected $email;
/** @var string */
var $version;
protected $version;
/** @var array */
var $urls;
protected $urls;
/** @var Stats */
var $stats;
/** @var string */
var $thumbnail;
protected $stats;
/** @var string|null */
protected $thumbnail = null;
/** @var array */
var $languages;
protected $languages;
/** @var int */
var $max_toot_chars;
protected $max_toot_chars;
/** @var bool */
var $registrations;
protected $registrations;
/** @var bool */
var $approval_required;
protected $approval_required;
/** @var Account|null */
var $contact_account;
protected $contact_account = null;
/**
* Creates an instance record
*
* @param App $app
*
* @return Instance
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function get()
{
@ -77,9 +75,8 @@ class Instance
$adminList = explode(',', str_replace(' ', '', Config::get('config', 'admin_email')));
$administrator = User::getByEmail($adminList[0], ['nickname']);
if (!empty($administrator)) {
$adminContact = DBA::selectFirst('contact', [], ['nick' => $administrator['nickname'], 'self' => true]);
$apcontact = APContact::getByURL($adminContact['url'], false);
$instance->contact_account = Account::create($baseUrl, $adminContact, $apcontact);
$adminContact = DBA::selectFirst('contact', ['id'], ['nick' => $administrator['nickname'], 'self' => true]);
$instance->contact_account = DI::mstdnAccount()->createFromContactId($adminContact['id']);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Friendica\Api\Entity\Mastodon;
use Friendica\Api\BaseEntity;
use Friendica\Model\Contact;
use Friendica\Util\Network;
/**
* Class Relationship
*
* @see https://docs.joinmastodon.org/api/entities/#relationship
*/
class Relationship extends BaseEntity
{
/** @var int */
protected $id;
/** @var bool */
protected $following = false;
/** @var bool */
protected $followed_by = false;
/** @var bool */
protected $blocking = false;
/** @var bool */
protected $muting = false;
/** @var bool */
protected $muting_notifications = false;
/** @var bool */
protected $requested = false;
/** @var bool */
protected $domain_blocking = false;
/**
* Unsupported
* @var bool
*/
protected $showing_reblogs = true;
/**
* Unsupported
* @var bool
*/
protected $endorsed = false;
/**
* @param int $userContactId Contact row Id with uid != 0
* @param array $userContact Full Contact table record with uid != 0
*/
public function __construct(int $userContactId, array $userContact = [])
{
$this->id = $userContactId;
$this->following = in_array($userContact['rel'] ?? 0, [Contact::SHARING, Contact::FRIEND]);
$this->followed_by = in_array($userContact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND]);
$this->blocking = (bool)$userContact['blocked'] ?? false;
$this->muting = (bool)$userContact['readonly'] ?? false;
$this->muting_notifications = (bool)$userContact['readonly'] ?? false;
$this->requested = (bool)$userContact['pending'] ?? false;
$this->domain_blocking = Network::isUrlBlocked($userContact['url'] ?? '');
return $this;
}
}

View File

@ -1,7 +1,8 @@
<?php
namespace Friendica\Api\Mastodon;
namespace Friendica\Api\Entity\Mastodon;
use Friendica\Api\BaseEntity;
use Friendica\Core\Config;
use Friendica\Core\Protocol;
use Friendica\Database\DBA;
@ -11,14 +12,14 @@ use Friendica\Database\DBA;
*
* @see https://docs.joinmastodon.org/api/entities/#stats
*/
class Stats
class Stats extends BaseEntity
{
/** @var int */
var $user_count;
protected $user_count = 0;
/** @var int */
var $status_count;
protected $status_count = 0;
/** @var int */
var $domain_count;
protected $domain_count = 0;
/**
* Creates a stats record

View File

@ -1,111 +0,0 @@
<?php
namespace Friendica\Api\Mastodon;
use Friendica\App\BaseURL;
use Friendica\Content\Text\BBCode;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Util\DateTimeFormat;
/**
* Class Account
*
* @see https://docs.joinmastodon.org/entities/account
*/
class Account
{
/** @var string */
var $id;
/** @var string */
var $username;
/** @var string */
var $acct;
/** @var string */
var $display_name;
/** @var bool */
var $locked;
/** @var string (Datetime) */
var $created_at;
/** @var int */
var $followers_count;
/** @var int */
var $following_count;
/** @var int */
var $statuses_count;
/** @var string */
var $note;
/** @var string (URL)*/
var $url;
/** @var string (URL) */
var $avatar;
/** @var string (URL) */
var $avatar_static;
/** @var string (URL) */
var $header;
/** @var string (URL) */
var $header_static;
/** @var Emoji[] */
var $emojis;
/** @var Account|null */
var $moved = null;
/** @var Field[]|null */
var $fields = null;
/** @var bool|null */
var $bot = null;
/** @var bool */
var $group;
/** @var bool */
var $discoverable;
/** @var string|null (Datetime) */
var $last_status_at = null;
/**
* Creates an account record from a public contact record. Expects all contact table fields to be set.
*
* @param BaseURL $baseUrl
* @param array $publicContact Full contact table record with uid = 0
* @param array $apcontact Optional full apcontact table record
* @param array $userContact Optional full contact table record with uid = local_user()
* @return Account
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function create(BaseURL $baseUrl, array $publicContact, array $apcontact = [], array $userContact = [])
{
$account = new Account();
$account->id = $publicContact['id'];
$account->username = $publicContact['nick'];
$account->acct =
strpos($publicContact['url'], $baseUrl->get() . '/') === 0 ?
$publicContact['nick'] :
$publicContact['addr'];
$account->display_name = $publicContact['name'];
$account->locked = !empty($apcontact['manually-approve']);
$account->created_at = DateTimeFormat::utc($publicContact['created'], DateTimeFormat::ATOM);
$account->followers_count = $apcontact['followers_count'] ?? 0;
$account->following_count = $apcontact['following_count'] ?? 0;
$account->statuses_count = $apcontact['statuses_count'] ?? 0;
$account->note = BBCode::convert($publicContact['about'], false);
$account->url = $publicContact['url'];
$account->avatar = $userContact['avatar'] ?? $publicContact['avatar'];
$account->avatar_static = $userContact['avatar'] ?? $publicContact['avatar'];
// No header picture in Friendica
$account->header = '';
$account->header_static = '';
// No custom emojis per account in Friendica
$account->emojis = [];
// No metadata fields in Friendica
$account->fields = [];
$account->bot = ($publicContact['contact-type'] == Contact::TYPE_NEWS);
$account->group = ($publicContact['contact-type'] == Contact::TYPE_COMMUNITY);
$account->discoverable = !$publicContact['unsearchable'];
$publicContactLastItem = $publicContact['last-item'] ?: DBA::NULL_DATETIME;
$userContactLastItem = $userContact['last-item'] ?? DBA::NULL_DATETIME;
$lastItem = $userContactLastItem > $publicContactLastItem ? $userContactLastItem : $publicContactLastItem;
$account->last_status_at = $lastItem != DBA::NULL_DATETIME ? DateTimeFormat::utc($lastItem, DateTimeFormat::ATOM) : null;
return $account;
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace Friendica\Api\Mastodon;
/**
* Class Emoji
*
* @see https://docs.joinmastodon.org/api/entities/#emoji
*/
class Emoji
{
/** @var string */
var $shortcode;
/** @var string (URL)*/
var $static_url;
/** @var string (URL)*/
var $url;
/** @var bool */
var $visible_in_picker;
}

View File

@ -1,59 +0,0 @@
<?php
namespace Friendica\Api\Mastodon;
use Friendica\Model\Contact;
use Friendica\Util\Network;
/**
* Class Relationship
*
* @see https://docs.joinmastodon.org/api/entities/#relationship
*/
class Relationship
{
/** @var int */
var $id;
/** @var bool */
var $following = false;
/** @var bool */
var $followed_by = false;
/** @var bool */
var $blocking = false;
/** @var bool */
var $muting = false;
/** @var bool */
var $muting_notifications = false;
/** @var bool */
var $requested = false;
/** @var bool */
var $domain_blocking = false;
/** @var bool */
var $showing_reblogs = false;
/** @var bool */
var $endorsed = false;
/**
* @param array $contact Full Contact table record
* @return Relationship
*/
public static function createFromContact(array $contact)
{
$relationship = new self();
$relationship->id = $contact['id'];
$relationship->following = in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND]);
$relationship->followed_by = in_array($contact['rel'], [Contact::FOLLOWER, Contact::FRIEND]);
$relationship->blocking = (bool)$contact['blocked'];
$relationship->muting = (bool)$contact['readonly'];
$relationship->muting_notifications = (bool)$contact['readonly'];
$relationship->requested = (bool)$contact['pending'];
$relationship->domain_blocking = Network::isUrlBlocked($contact['url']);
// Unsupported
$relationship->showing_reblogs = true;
// Unsupported
$relationship->endorsed = false;
return $relationship;
}
}

38
src/BaseCollection.php Normal file
View File

@ -0,0 +1,38 @@
<?php
namespace Friendica;
/**
* The Collection classes inheriting from this abstract class are meant to represent a list of database record.
* The associated model class has to be provided in the child classes.
*
* Collections can be used with foreach(), accessed like an array and counted.
*/
abstract class BaseCollection extends \ArrayIterator
{
/**
* This property is used with paginated results to hold the total number of items satisfying the paginated request.
* @var int
*/
protected $totalCount = 0;
/**
* @param BaseModel[] $models
* @param int|null $totalCount
*/
public function __construct(array $models = [], int $totalCount = null)
{
parent::__construct($models);
$this->models = $models;
$this->totalCount = $totalCount ?? count($models);
}
/**
* @return int
*/
public function getTotalCount()
{
return $this->totalCount;
}
}

22
src/BaseFactory.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace Friendica;
use Psr\Log\LoggerInterface;
/**
* Factories act as an intermediary to avoid direct Entitiy instanciation.
*
* @see BaseModel
* @see BaseCollection
*/
abstract class BaseFactory
{
/** @var LoggerInterface */
protected $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
}

View File

@ -7,8 +7,6 @@ use Friendica\Network\HTTPException;
use Psr\Log\LoggerInterface;
/**
* Class BaseModel
*
* The Model classes inheriting from this abstract class are meant to represent a single database record.
* The associated table name has to be provided in the child class, and the table is expected to have a unique `id` field.
*
@ -16,8 +14,6 @@ use Psr\Log\LoggerInterface;
*/
abstract class BaseModel
{
protected static $table_name;
/** @var Database */
protected $dba;
/** @var LoggerInterface */
@ -32,13 +28,33 @@ abstract class BaseModel
*/
private $data = [];
public function __construct(Database $dba, LoggerInterface $logger, $data = [])
/**
* @param Database $dba
* @param LoggerInterface $logger
* @param array $data Table row attributes
*/
public function __construct(Database $dba, LoggerInterface $logger, array $data = [])
{
$this->dba = $dba;
$this->logger = $logger;
$this->data = $data;
}
/**
* Performance-improved model creation in a loop
*
* @param BaseModel $prototype
* @param array $data
* @return BaseModel
*/
public static function createFromPrototype(BaseModel $prototype, array $data)
{
$model = clone $prototype;
$model->data = $data;
return $model;
}
/**
* Magic getter. This allows to retrieve model fields with the following syntax:
* - $model->field (outside of class)
@ -62,33 +78,16 @@ abstract class BaseModel
}
/**
* Fetches a single model record. The condition array is expected to contain a unique index (primary or otherwise).
*
* Chainable.
*
* @param array $condition
* @return BaseModel
* @throws HTTPException\NotFoundException
* @param string $name
* @param mixed $value
*/
public function fetch(array $condition)
public function __set($name, $value)
{
$data = $this->dba->selectFirst(static::$table_name, [], $condition);
if (!$data) {
throw new HTTPException\NotFoundException(static::class . ' record not found.');
}
return new static($this->dba, $this->logger, $data);
$this->data[$name] = $value;
}
/**
* Deletes the model record from the database.
* Prevents further methods from being called by wiping the internal model data.
*/
public function delete()
public function toArray()
{
if ($this->dba->delete(static::$table_name, ['id' => $this->id])) {
$this->data = [];
}
return $this->data;
}
}

200
src/BaseRepository.php Normal file
View File

@ -0,0 +1,200 @@
<?php
namespace Friendica;
use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\Network\HTTPException;
use Psr\Log\LoggerInterface;
/**
* Repositories are Factories linked to one or more database tables.
*
* @see BaseModel
* @see BaseCollection
*/
abstract class BaseRepository extends BaseFactory
{
const LIMIT = 30;
/** @var Database */
protected $dba;
/** @var string */
protected static $table_name;
/** @var BaseModel */
protected static $model_class;
/** @var BaseCollection */
protected static $collection_class;
public function __construct(Database $dba, LoggerInterface $logger)
{
parent::__construct($logger);
$this->dba = $dba;
$this->logger = $logger;
}
/**
* Fetches a single model record. The condition array is expected to contain a unique index (primary or otherwise).
*
* Chainable.
*
* @param array $condition
* @return BaseModel
* @throws HTTPException\NotFoundException
*/
public function selectFirst(array $condition)
{
$data = $this->dba->selectFirst(static::$table_name, [], $condition);
if (!$data) {
throw new HTTPException\NotFoundException(static::class . ' record not found.');
}
return $this->create($data);
}
/**
* Populates a Collection according to the condition.
*
* Chainable.
*
* @param array $condition
* @param array $params
* @return BaseCollection
* @throws \Exception
*/
public function select(array $condition = [], array $params = [])
{
$models = $this->selectModels($condition, $params);
return new static::$collection_class($models);
}
/**
* Populates the collection according to the condition. Retrieves a limited subset of models depending on the boundaries
* and the limit. The total count of rows matching the condition is stored in the collection.
*
* Chainable.
*
* @param array $condition
* @param array $params
* @param int? $max_id
* @param int? $since_id
* @param int $limit
* @return BaseCollection
* @throws \Exception
*/
public function selectByBoundaries(array $condition = [], array $params = [], int $max_id = null, int $since_id = null, int $limit = self::LIMIT)
{
$condition = DBA::collapseCondition($condition);
$boundCondition = $condition;
if (isset($max_id)) {
$boundCondition[0] .= " AND `id` < ?";
$boundCondition[] = $max_id;
}
if (isset($since_id)) {
$boundCondition[0] .= " AND `id` > ?";
$boundCondition[] = $since_id;
}
$params['limit'] = $limit;
$models = $this->selectModels($boundCondition, $params);
$totalCount = DBA::count(static::$table_name, $condition);
return new static::$collection_class($models, $totalCount);
}
/**
* This method updates the database row from the model.
*
* @param BaseModel $model
* @return bool
* @throws \Exception
*/
public function update(BaseModel $model)
{
return $this->dba->update(static::$table_name, $model->toArray(), ['id' => $model->id], true);
}
/**
* This method creates a new database row and returns a model if it was successful.
*
* @param array $fields
* @return BaseModel|bool
* @throws \Exception
*/
public function insert(array $fields)
{
$return = $this->dba->insert(static::$table_name, $fields);
if ($return) {
$fields['id'] = $this->dba->lastInsertId();
$return = $this->create($fields);
}
return $return;
}
/**
* Deletes the model record from the database.
*
* @param BaseModel $model
* @return bool
* @throws \Exception
*/
public function delete(BaseModel &$model)
{
if ($success = $this->dba->delete(static::$table_name, ['id' => $model->id])) {
$model = null;
}
return $success;
}
/**
* Base instantiation method, can be overriden to add specific dependencies
*
* @param array $data
* @return BaseModel
*/
protected function create(array $data)
{
return new static::$model_class($this->dba, $this->logger, $data);
}
/**
* @param array $condition Query condition
* @param array $params Additional query parameters
* @return BaseModel[]
* @throws \Exception
*/
protected function selectModels(array $condition, array $params = [])
{
$result = $this->dba->select(static::$table_name, [], $condition, $params);
/** @var BaseModel $prototype */
$prototype = null;
$models = [];
while ($record = $this->dba->fetch($result)) {
if ($prototype === null) {
$prototype = $this->create($record);
$models[] = $prototype;
} else {
$models[] = static::$model_class::createFromPrototype($prototype, $record);
}
}
return $models;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Friendica\Collection;
use Friendica\BaseCollection;
use Friendica\Model\Introduction;
/**
* @property Introduction[] $models
*/
class Introductions extends BaseCollection
{
}

View File

@ -28,9 +28,12 @@ use Psr\Log\LoggerInterface;
* @method static Core\Process process()
* @method static Core\Session\ISession session()
* @method static Database\Database dba()
* @method static Factory\Mastodon\Account mstdnAccount()
* @method static Factory\Mastodon\FollowRequest mstdnFollowRequest()
* @method static Factory\Mastodon\Relationship mstdnRelationship()
* @method static Model\User\Cookie cookie()
* @method static Model\Notify notify()
* @method static Model\Introduction intro()
* @method static Repository\Introduction intro()
* @method static Protocol\Activity activity()
* @method static Util\ACLFormatter aclFormatter()
* @method static Util\DateTimeFormat dtFormat()
@ -62,9 +65,12 @@ abstract class DI
'process' => Core\Process::class,
'session' => Core\Session\ISession::class,
'dba' => Database\Database::class,
'mstdnAccount' => Factory\Mastodon\Account::class,
'mstdnFollowRequest' => Factory\Mastodon\FollowRequest::class,
'mstdnRelationship' => Factory\Mastodon\Relationship::class,
'cookie' => Model\User\Cookie::class,
'notify' => Model\Notify::class,
'intro' => Model\Introduction::class,
'intro' => Repository\Introduction::class,
'activity' => Protocol\Activity::class,
'aclFormatter' => Util\ACLFormatter::class,
'dtFormat' => Util\DateTimeFormat::class,

View File

@ -529,67 +529,96 @@ class DBA
*/
public static function buildCondition(array &$condition = [])
{
$condition = self::collapseCondition($condition);
$condition_string = '';
if (count($condition) > 0) {
reset($condition);
$first_key = key($condition);
if (is_int($first_key)) {
$condition_string = " WHERE (" . array_shift($condition) . ")";
} else {
$new_values = [];
$condition_string = "";
foreach ($condition as $field => $value) {
if ($condition_string != "") {
$condition_string .= " AND ";
}
if (is_array($value)) {
if (count($value)) {
/* Workaround for MySQL Bug #64791.
* Never mix data types inside any IN() condition.
* In case of mixed types, cast all as string.
* Logic needs to be consistent with DBA::p() data types.
*/
$is_int = false;
$is_alpha = false;
foreach ($value as $single_value) {
if (is_int($single_value)) {
$is_int = true;
} else {
$is_alpha = true;
}
}
if ($is_int && $is_alpha) {
foreach ($value as &$ref) {
if (is_int($ref)) {
$ref = (string)$ref;
}
}
unset($ref); //Prevent accidental re-use.
}
$new_values = array_merge($new_values, array_values($value));
$placeholders = substr(str_repeat("?, ", count($value)), 0, -2);
$condition_string .= self::quoteIdentifier($field) . " IN (" . $placeholders . ")";
} else {
// Empty value array isn't supported by IN and is logically equivalent to no match
$condition_string .= "FALSE";
}
} elseif (is_null($value)) {
$condition_string .= self::quoteIdentifier($field) . " IS NULL";
} else {
$new_values[$field] = $value;
$condition_string .= self::quoteIdentifier($field) . " = ?";
}
}
$condition_string = " WHERE (" . $condition_string . ")";
$condition = $new_values;
}
$condition_string = " WHERE (" . array_shift($condition) . ")";
}
return $condition_string;
}
/**
* Collapse an associative array condition into a SQL string + parameters condition array.
*
* ['uid' => 1, 'network' => ['dspr', 'apub']]
*
* gets transformed into
*
* ["`uid` = ? AND `network` IN (?, ?)", 1, 'dspr', 'apub']
*
* @param array $condition
* @return array
*/
public static function collapseCondition(array $condition)
{
// Ensures an always true condition is returned
if (count($condition) < 1) {
return ['1'];
}
reset($condition);
$first_key = key($condition);
if (is_int($first_key)) {
// Already collapsed
return $condition;
}
$values = [];
$condition_string = "";
foreach ($condition as $field => $value) {
if ($condition_string != "") {
$condition_string .= " AND ";
}
if (is_array($value)) {
if (count($value)) {
/* Workaround for MySQL Bug #64791.
* Never mix data types inside any IN() condition.
* In case of mixed types, cast all as string.
* Logic needs to be consistent with DBA::p() data types.
*/
$is_int = false;
$is_alpha = false;
foreach ($value as $single_value) {
if (is_int($single_value)) {
$is_int = true;
} else {
$is_alpha = true;
}
}
if ($is_int && $is_alpha) {
foreach ($value as &$ref) {
if (is_int($ref)) {
$ref = (string)$ref;
}
}
unset($ref); //Prevent accidental re-use.
}
$values = array_merge($values, array_values($value));
$placeholders = substr(str_repeat("?, ", count($value)), 0, -2);
$condition_string .= self::quoteIdentifier($field) . " IN (" . $placeholders . ")";
} else {
// Empty value array isn't supported by IN and is logically equivalent to no match
$condition_string .= "FALSE";
}
} elseif (is_null($value)) {
$condition_string .= self::quoteIdentifier($field) . " IS NULL";
} else {
$values[$field] = $value;
$condition_string .= self::quoteIdentifier($field) . " = ?";
}
}
$condition = array_merge([$condition_string], array_values($values));
return $condition;
}
/**
* @brief Returns the SQL parameter string built from the provided parameter array
*

View File

@ -1327,10 +1327,6 @@ class Database
return false;
}
$table_string = DBA::buildTableString($table);
$condition_string = DBA::buildCondition($condition);
if (is_bool($old_fields)) {
$do_insert = $old_fields;
@ -1361,13 +1357,16 @@ class Database
return true;
}
$table_string = DBA::buildTableString($table);
$condition_string = DBA::buildCondition($condition);
$sql = "UPDATE " . $table_string . " SET "
. implode(" = ?, ", array_map([DBA::class, 'quoteIdentifier'], array_keys($fields))) . " = ?"
. $condition_string;
$params1 = array_values($fields);
$params2 = array_values($condition);
$params = array_merge_recursive($params1, $params2);
// Combines the updated fields parameter values with the condition parameter values
$params = array_merge(array_values($fields), $condition);
return $this->e($sql, $params);
}

View File

@ -0,0 +1,46 @@
<?php
namespace Friendica\Factory\Mastodon;
use Friendica\App\BaseURL;
use Friendica\Model\APContact;
use Friendica\Model\Contact;
use Friendica\Network\HTTPException;
use Friendica\BaseFactory;
use Psr\Log\LoggerInterface;
class Account extends BaseFactory
{
/** @var BaseURL */
protected $baseUrl;
public function __construct(LoggerInterface $logger, BaseURL $baseURL)
{
parent::__construct($logger);
$this->baseUrl = $baseURL;
}
/**
* @param int $contactId
* @param int $uid User Id
* @return \Friendica\Api\Entity\Mastodon\Account
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public function createFromContactId(int $contactId, $uid = 0)
{
$cdata = Contact::getPublicAndUserContacID($contactId, $uid);
if (!empty($cdata)) {
$publicContact = Contact::getById($cdata['public']);
$userContact = Contact::getById($cdata['user']);
} else {
$publicContact = Contact::getById($contactId);
$userContact = [];
}
$apcontact = APContact::getByURL($publicContact['url'], false);
return new \Friendica\Api\Entity\Mastodon\Account($this->baseUrl, $publicContact, $apcontact, $userContact);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Friendica\Factory\Mastodon;
use Friendica\App\BaseURL;
use Friendica\Model\APContact;
use Friendica\Model\Contact;
use Friendica\Model\Introduction;
use Friendica\Network\HTTPException;
use Friendica\BaseFactory;
use Psr\Log\LoggerInterface;
class FollowRequest extends BaseFactory
{
/** @var BaseURL */
protected $baseUrl;
public function __construct(LoggerInterface $logger, BaseURL $baseURL)
{
parent::__construct($logger);
$this->baseUrl = $baseURL;
}
/**
* @param Introduction $introduction
* @return \Friendica\Api\Entity\Mastodon\FollowRequest
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public function createFromIntroduction(Introduction $introduction)
{
$cdata = Contact::getPublicAndUserContacID($introduction->{'contact-id'}, $introduction->uid);
if (empty($cdata)) {
$this->logger->warning('Wrong introduction data', ['Introduction' => $introduction]);
throw new HTTPException\InternalServerErrorException('Wrong introduction data');
}
$publicContact = Contact::getById($cdata['public']);
$userContact = Contact::getById($cdata['user']);
$apcontact = APContact::getByURL($publicContact['url'], false);
return new \Friendica\Api\Entity\Mastodon\FollowRequest($this->baseUrl, $introduction->id, $publicContact, $apcontact, $userContact);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Friendica\Factory\Mastodon;
use Friendica\Api\Entity\Mastodon\Relationship as RelationshipEntity;
use Friendica\BaseFactory;
use Friendica\Model\Contact;
class Relationship extends BaseFactory
{
/**
* @param int $userContactId Contact row id with uid != 0
* @return RelationshipEntity
* @throws \Exception
*/
public function createFromContactId(int $userContactId)
{
return $this->createFromContact(Contact::getById($userContactId));
}
/**
* @param array $userContact Full contact row record with uid != 0
* @return RelationshipEntity
*/
public function createFromContact(array $userContact)
{
return new RelationshipEntity($userContact['id'], $userContact);
}
/**
* @param int $userContactId Contact row id with uid != 0
* @return RelationshipEntity
*/
public function createDefaultFromContactId(int $userContactId)
{
return new RelationshipEntity($userContactId);
}
}

View File

@ -4,10 +4,13 @@ namespace Friendica\Model;
use Friendica\BaseModel;
use Friendica\Core\Protocol;
use Friendica\Database\Database;
use Friendica\Network\HTTPException;
use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\Diaspora;
use Friendica\Repository;
use Friendica\Util\DateTimeFormat;
use Psr\Log\LoggerInterface;
/**
* @property int uid
@ -20,33 +23,40 @@ use Friendica\Util\DateTimeFormat;
* @property string datetime
* @property bool blocked
* @property bool ignored
*
* @package Friendica\Model
*/
final class Introduction extends BaseModel
{
static $table_name = 'intro';
/** @var Repository\Introduction */
protected $intro;
public function __construct(Database $dba, LoggerInterface $logger, Repository\Introduction $intro, array $data = [])
{
parent::__construct($dba, $logger, $data);
$this->intro = $intro;
}
/**
* Confirms a follow request and sends a notic to the remote contact.
* Confirms a follow request and sends a notice to the remote contact.
*
* @param bool $duplex Is it a follow back?
* @param bool|null $hidden Should this contact be hidden? null = no change
* @param bool $duplex Is it a follow back?
* @param bool|null $hidden Should this contact be hidden? null = no change
* @return bool
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
* @throws HTTPException\NotFoundException
* @throws \ImagickException
*/
public function confirm(bool $duplex = false, bool $hidden = null)
{
$this->logger->info('Confirming follower', ['cid' => $this->{'contact-id'}]);
$contact = Contact::selectFirst([], ['id' => $this->{'contact-id'}, 'uid' => $this->uid]);
$contact = Model\Contact::selectFirst([], ['id' => $this->{'contact-id'}, 'uid' => $this->uid]);
if (!$contact) {
throw new HTTPException\NotFoundException('Contact record not found.');
}
$new_relation = $contact['rel'];
$newRelation = $contact['rel'];
$writable = $contact['writable'];
if (!empty($contact['protocol'])) {
@ -61,12 +71,12 @@ final class Introduction extends BaseModel
if (in_array($protocol, [Protocol::DIASPORA, Protocol::ACTIVITYPUB])) {
if ($duplex) {
$new_relation = Contact::FRIEND;
$newRelation = Model\Contact::FRIEND;
} else {
$new_relation = Contact::FOLLOWER;
$newRelation = Model\Contact::FOLLOWER;
}
if ($new_relation != Contact::FOLLOWER) {
if ($newRelation != Model\Contact::FOLLOWER) {
$writable = 1;
}
}
@ -79,43 +89,42 @@ final class Introduction extends BaseModel
'protocol' => $protocol,
'writable' => $writable,
'hidden' => $hidden ?? $contact['hidden'],
'rel' => $new_relation,
'rel' => $newRelation,
];
$this->dba->update('contact', $fields, ['id' => $contact['id']]);
array_merge($contact, $fields);
if ($new_relation == Contact::FRIEND) {
if ($newRelation == Model\Contact::FRIEND) {
if ($protocol == Protocol::DIASPORA) {
$ret = Diaspora::sendShare(User::getById($contact['uid']), $contact);
$ret = Diaspora::sendShare(Model\Contact::getById($contact['uid']), $contact);
$this->logger->info('share returns', ['return' => $ret]);
} elseif ($protocol == Protocol::ACTIVITYPUB) {
ActivityPub\Transmitter::sendActivity('Follow', $contact['url'], $contact['uid']);
}
}
$this->delete();
return $this->intro->delete($this);
}
/**
* Silently ignores the introduction, hides it from notifications and prevents the remote contact from submitting
* additional follow requests.
*
* Chainable
*
* @return Introduction
* @return bool
* @throws \Exception
*/
public function ignore()
{
$this->dba->update('intro', ['ignore' => true], ['id' => $this->id]);
$this->ignored = true;
return $this;
return $this->intro->update($this);
}
/**
* Discards the introduction and sends a rejection message to AP contacts.
*
* @return bool
* @throws HTTPException\InternalServerErrorException
* @throws HTTPException\NotFoundException
* @throws \ImagickException
@ -127,15 +136,15 @@ final class Introduction extends BaseModel
if (!$this->fid) {
// When the contact entry had been created just for that intro, we want to get rid of it now
$condition = ['id' => $this->{'contact-id'}, 'uid' => $this->uid,
'self' => false, 'pending' => true, 'rel' => [0, Contact::FOLLOWER]];
'self' => false, 'pending' => true, 'rel' => [0, Model\Contact::FOLLOWER]];
if ($this->dba->exists('contact', $condition)) {
Contact::remove($this->{'contact-id'});
Model\Contact::remove($this->{'contact-id'});
} else {
$this->dba->update('contact', ['pending' => false], ['id' => $this->{'contact-id'}]);
}
}
$contact = Contact::selectFirst([], ['id' => $this->{'contact-id'}, 'uid' => $this->uid]);
$contact = Model\Contact::selectFirst([], ['id' => $this->{'contact-id'}, 'uid' => $this->uid]);
if (!$contact) {
throw new HTTPException\NotFoundException('Contact record not found.');
@ -151,6 +160,6 @@ final class Introduction extends BaseModel
ActivityPub\Transmitter::sendContactReject($contact['url'], $contact['hub-verify'], $contact['uid']);
}
$this->delete();
return $this->intro->delete($this);
}
}

View File

@ -2,18 +2,16 @@
namespace Friendica\Module\Api\Mastodon;
use Friendica\Api\Mastodon;
use Friendica\Api\Entity\Mastodon;
use Friendica\Api\Entity\Mastodon\Relationship;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Model\APContact;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Introduction;
use Friendica\Module\Base\Api;
use Friendica\Network\HTTPException;
/**
* @see https://docs.joinmastodon.org/api/rest/follow-requests/
* @see https://docs.joinmastodon.org/methods/accounts/follow_requests
*/
class FollowRequests extends Api
{
@ -30,8 +28,11 @@ class FollowRequests extends Api
* @param array $parameters
* @throws HTTPException\BadRequestException
* @throws HTTPException\ForbiddenException
* @throws HTTPException\InternalServerErrorException
* @throws HTTPException\NotFoundException
* @throws HTTPException\UnauthorizedException
* @throws \ImagickException
*
* @see https://docs.joinmastodon.org/methods/accounts/follow_requests#accept-follow
* @see https://docs.joinmastodon.org/methods/accounts/follow_requests#reject-follow
*/
@ -39,23 +40,25 @@ class FollowRequests extends Api
{
parent::post($parameters);
$Intro = DI::intro()->fetch(['id' => $parameters['id'], 'uid' => self::$current_user_id]);
$introduction = DI::intro()->selectFirst(['id' => $parameters['id'], 'uid' => self::$current_user_id]);
$contactId = $Intro->{'contact-id'};
$relationship = new Mastodon\Relationship();
$relationship->id = $contactId;
$contactId = $introduction->{'contact-id'};
switch ($parameters['action']) {
case 'authorize':
$Intro->confirm();
$relationship = Mastodon\Relationship::createFromContact(Contact::getById($contactId));
$introduction->confirm();
$relationship = DI::mstdnRelationship()->createFromContactId($contactId);
break;
case 'ignore':
$Intro->ignore();
$introduction->ignore();
$relationship = DI::mstdnRelationship()->createDefaultFromContactId($contactId);
break;
case 'reject':
$Intro->discard();
$introduction->discard();
$relationship = DI::mstdnRelationship()->createDefaultFromContactId($contactId);
break;
default:
throw new HTTPException\BadRequestException('Unexpected action parameter, expecting "authorize", "ignore" or "reject"');
@ -78,41 +81,23 @@ class FollowRequests extends Api
$baseUrl = DI::baseUrl();
if (isset($since_id) && isset($max_id)) {
$condition = ['`uid` = ? AND NOT `ignore` AND `id` > ? AND `id` < ?', self::$current_user_id, $since_id, $max_id];
} elseif (isset($since_id)) {
$condition = ['`uid` = ? AND NOT `ignore` AND `id` > ?', self::$current_user_id, $since_id];
} elseif (isset($max_id)) {
$condition = ['`uid` = ? AND NOT `ignore` AND `id` < ?', self::$current_user_id, $max_id];
} else {
$condition = ['`uid` = ? AND NOT `ignore`', self::$current_user_id];
}
$count = DBA::count('intro', $condition);
$intros = DBA::selectToArray(
'intro',
[],
$condition,
['order' => ['id' => 'DESC'], 'limit' => $limit]
$introductions = DI::intro()->selectByBoundaries(
['`uid` = ? AND NOT `ignore`', self::$current_user_id],
['order' => ['id' => 'DESC']],
$since_id,
$max_id,
$limit
);
$return = [];
foreach ($intros as $intro) {
$cdata = Contact::getPublicAndUserContacID($intro['contact-id'], $intro['uid']);
if (empty($cdata['public'])) {
continue;
foreach ($introductions as $key => $introduction) {
try {
$return[] = DI::mstdnFollowRequest()->createFromIntroduction($introduction);
} catch (HTTPException\InternalServerErrorException $exception) {
DI::intro()->delete($introduction);
unset($introductions[$key]);
}
$publicContact = Contact::getById($cdata['public']);
$userContact = Contact::getById($cdata['user']);
$apcontact = APContact::getByURL($publicContact['url'], false);
$account = Mastodon\Account::create($baseUrl, $publicContact, $apcontact, $userContact);
// Not ideal, the same "account" can have multiple ids depending on the context
$account->id = $intro['id'];
$return[] = $account;
}
$base_query = [];
@ -121,10 +106,10 @@ class FollowRequests extends Api
}
$links = [];
if ($count > $limit) {
$links[] = '<' . $baseUrl->get() . '/api/v1/follow_requests?' . http_build_query($base_query + ['max_id' => $intros[count($intros) - 1]['id']]) . '>; rel="next"';
if ($introductions->getTotalCount() > $limit) {
$links[] = '<' . $baseUrl->get() . '/api/v1/follow_requests?' . http_build_query($base_query + ['max_id' => $introductions[count($introductions) - 1]->id]) . '>; rel="next"';
}
$links[] = '<' . $baseUrl->get() . '/api/v1/follow_requests?' . http_build_query($base_query + ['since_id' => $intros[0]['id']]) . '>; rel="prev"';
$links[] = '<' . $baseUrl->get() . '/api/v1/follow_requests?' . http_build_query($base_query + ['since_id' => $introductions[0]->id]) . '>; rel="prev"';
header('Link: ' . implode(', ', $links));

View File

@ -2,7 +2,7 @@
namespace Friendica\Module\Api\Mastodon;
use Friendica\Api\Mastodon\Instance as InstanceEntity;
use Friendica\Api\Entity\Mastodon\Instance as InstanceEntity;
use Friendica\Core\System;
use Friendica\Module\Base\Api;

View File

@ -23,11 +23,11 @@ class FollowConfirm extends BaseModule
$duplex = intval($_POST['duplex'] ?? 0);
$hidden = intval($_POST['hidden'] ?? 0);
$Intro = DI::intro()->fetch(['id' => $intro_id, 'uid' => local_user()]);
$intro = DI::intro()->selectFirst(['id' => $intro_id, 'uid' => local_user()]);
$cid = $Intro->{'contact-id'};
$cid = $intro->{'contact-id'};
$Intro->confirm($duplex, $hidden);
$intro->confirm($duplex, $hidden);
DI::baseUrl()->redirect('contact/' . intval($cid));
}

View File

@ -0,0 +1,60 @@
<?php
namespace Friendica\Repository;
use Friendica\BaseRepository;
use Friendica\Collection;
use Friendica\Model;
class Introduction extends BaseRepository
{
protected static $table_name = 'intro';
protected static $model_class = Model\Introduction::class;
protected static $collection_class = Collection\Introductions::class;
/**
* @param array $data
* @return Model\Introduction
*/
protected function create(array $data)
{
return new Model\Introduction($this->dba, $this->logger, $this, $data);
}
/**
* @param array $condition
* @return Model\Introduction
* @throws \Friendica\Network\HTTPException\NotFoundException
*/
public function selectFirst(array $condition)
{
return parent::selectFirst($condition);
}
/**
* @param array $condition
* @param array $params
* @return Collection\Introductions
* @throws \Exception
*/
public function select(array $condition = [], array $params = [])
{
return parent::select($condition, $params);
}
/**
* @param array $condition
* @param array $params
* @param int|null $max_id
* @param int|null $since_id
* @param int $limit
* @return Collection\Introductions
* @throws \Exception
*/
public function selectByBoundaries(array $condition = [], array $params = [], int $max_id = null, int $since_id = null, int $limit = self::LIMIT)
{
return parent::selectByBoundaries($condition, $params, $max_id, $since_id, $limit);
}
}