summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--appinfo/info.xml1
-rw-r--r--lib/Command/CheckInstall.php44
-rw-r--r--lib/Command/ExtendedBase.php134
-rw-r--r--lib/Command/NoteBoost.php14
-rw-r--r--lib/Command/NoteLike.php14
-rw-r--r--lib/Command/StreamDetails.php145
-rw-r--r--lib/Command/Timeline.php103
-rw-r--r--lib/Db/CoreRequestBuilder.php58
-rw-r--r--lib/Db/FollowsRequest.php22
-rw-r--r--lib/Db/FollowsRequestBuilder.php8
-rw-r--r--lib/Interfaces/Object/NoteInterface.php11
-rw-r--r--lib/Model/ActivityPub/ACore.php24
-rw-r--r--lib/Model/ActivityPub/Actor/Person.php1
-rw-r--r--lib/Model/ActivityPub/Item.php11
-rw-r--r--lib/Model/StreamDetails.php208
-rw-r--r--lib/Service/BoostService.php18
-rw-r--r--lib/Service/ConfigService.php1
-rw-r--r--lib/Service/DetailsService.php131
-rw-r--r--lib/Service/FollowService.php19
-rw-r--r--lib/Service/MiscService.php11
-rw-r--r--lib/Service/PushService.php127
-rw-r--r--package.json2
22 files changed, 978 insertions, 129 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 44f40255..a713e873 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -56,6 +56,7 @@
<command>OCA\Social\Command\NoteCreate</command>
<command>OCA\Social\Command\NoteBoost</command>
<command>OCA\Social\Command\Reset</command>
+ <command>OCA\Social\Command\StreamDetails</command>
<command>OCA\Social\Command\Timeline</command>
<command>OCA\Social\Command\QueueStatus</command>
<command>OCA\Social\Command\QueueProcess</command>
diff --git a/lib/Command/CheckInstall.php b/lib/Command/CheckInstall.php
index fcd06aa0..d761cf42 100644
--- a/lib/Command/CheckInstall.php
+++ b/lib/Command/CheckInstall.php
@@ -35,13 +35,20 @@ use Exception;
use OC\Core\Command\Base;
use OCA\Social\Service\CheckService;
use OCA\Social\Service\MiscService;
+use OCA\Social\Service\PushService;
+use OCP\IUserManager;
+use OCP\Stratos\Exceptions\StratosInstallException;
use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class CheckInstall extends Base {
+ /** @var IUserManager */
+ private $userManager;
+
/** @var CheckService */
private $checkService;
@@ -52,16 +59,25 @@ class CheckInstall extends Base {
/**
* CacheUpdate constructor.
*
+ * @param IUserManager $userManager
* @param CheckService $checkService
* @param MiscService $miscService
+ * @param PushService $pushService
*/
- public function __construct(CheckService $checkService, MiscService $miscService) {
+ public function __construct(
+ IUserManager $userManager, CheckService $checkService, MiscService $miscService,
+ PushService $pushService
+ ) {
parent::__construct();
+ $this->userManager = $userManager;
$this->checkService = $checkService;
$this->miscService = $miscService;
+ $this->pushService = $pushService;
}
+ /** @var PushService */
+ private $pushService;
/**
*
@@ -69,6 +85,10 @@ class CheckInstall extends Base {
protected function configure() {
parent::configure();
$this->setName('social:check:install')
+ ->addOption(
+ 'stratos', '', InputOption::VALUE_REQUIRED, 'a local account used to test Stratos',
+ ''
+ )
->setDescription('Check the integrity of the installation');
}
@@ -81,8 +101,30 @@ class CheckInstall extends Base {
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$this->checkService->checkInstallationStatus();
+
+ $this->checkStratos($input, $output);
}
+ /**
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ *
+ * @throws Exception
+ */
+ private function checkStratos(InputInterface $input, OutputInterface $output) {
+ $userId = $input->getOption('stratos');
+ if ($userId !== '') {
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ throw new Exception('unknown user');
+ }
+
+ $wrapper = $this->pushService->testOnAccount($userId);
+
+ $output->writeln(json_encode($wrapper, JSON_PRETTY_PRINT));
+ }
+ }
+
}
diff --git a/lib/Command/ExtendedBase.php b/lib/Command/ExtendedBase.php
new file mode 100644
index 00000000..743c6fb5
--- /dev/null
+++ b/lib/Command/ExtendedBase.php
@@ -0,0 +1,134 @@
+<?php
+declare(strict_types=1);
+
+
+/**
+ * Nextcloud - Social Support
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Maxence Lange <maxence@artificial-owl.com>
+ * @copyright 2018, Maxence Lange <maxence@artificial-owl.com>
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Social\Command;
+
+
+use daita\MySmallPhpTools\Exceptions\CacheItemNotFoundException;
+use OC\Core\Command\Base;
+use OCA\Social\AP;
+use OCA\Social\Exceptions\ItemUnknownException;
+use OCA\Social\Exceptions\RedundancyLimitException;
+use OCA\Social\Exceptions\SocialAppConfigException;
+use OCA\Social\Model\ActivityPub\Actor\Person;
+use OCA\Social\Model\ActivityPub\Stream;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Output\OutputInterface;
+
+
+class ExtendedBase extends Base {
+
+ /** @var OutputInterface */
+ protected $output;
+
+ /** @var bool */
+ protected $asJson = false;
+
+
+ /**
+ * @param Person $actor
+ */
+ protected function outputActor(Person $actor) {
+ if ($this->asJson) {
+ $this->output->writeln(json_encode($actor, JSON_PRETTY_PRINT));
+ }
+
+ $this->output->writeln('<info>Account</info>: ' . $actor->getAccount());
+ $this->output->writeln('<info>Id</info>: ' . $actor->getId());
+ $this->output->writeln('');
+
+ }
+
+
+ /**
+ * @param Stream[] $streams
+ */
+ protected function outputStreams(array $streams) {
+ if ($this->asJson) {
+ $this->output->writeln(json_encode($streams, JSON_PRETTY_PRINT));
+ }
+
+ $table = new Table($this->output);
+ $table->setHeaders(['Id', 'Source', 'Type', 'Author', 'Content']);
+ $table->render();
+ $this->output->writeln('');
+
+ foreach ($streams as $item) {
+ $objectId = $item->getObjectId();
+ $cache = $item->getCache();
+ $content = '';
+ $author = '';
+ if ($objectId !== '' && $cache->hasItem($objectId)) {
+ try {
+ $cachedObject = $cache->getItem($objectId)
+ ->getObject();
+
+ /** @var Stream $cachedItem */
+ $cachedItem = AP::$activityPub->getItemFromData($cachedObject);
+ $content = $cachedItem->getContent();
+ $author = $cachedItem->getActor()
+ ->getAccount();
+ } catch (CacheItemNotFoundException $e) {
+ } catch (ItemUnknownException $e) {
+ } catch (RedundancyLimitException $e) {
+ } catch (SocialAppConfigException $e) {
+ }
+ } else {
+ $content = $item->getContent();
+ $author = $item->getActor()
+ ->getAccount();
+ }
+
+ $table->appendRow(
+ [
+ '<comment>' . $item->getId() . '</comment>',
+ '<info>' . $item->getActor()
+ ->getAccount() . '</info>',
+ $item->getType(),
+ '<info>' . $author . '</info>',
+ $content,
+ ]
+ );
+ }
+ }
+
+
+ /**
+ * @param Stream $stream
+ */
+ protected function outputStream(Stream $stream) {
+ $actor = $stream->getActor();
+ $this->output->writeln('id: <comment>' . $stream->getId() . '</comment>');
+ $this->output->writeln(
+ 'author: <comment>' . $actor->getAccount() . '</comment>'
+ );
+ $this->output->writeln('type: <info>' . $stream->getType() . '</info>');
+ }
+
+}
diff --git a/lib/Command/NoteBoost.php b/lib/Command/NoteBoost.php
index 3df7a515..499672ce 100644
--- a/lib/Command/NoteBoost.php
+++ b/lib/Command/NoteBoost.php
@@ -36,7 +36,7 @@ use OC\Core\Command\Base;
use OCA\Social\Service\AccountService;
use OCA\Social\Service\BoostService;
use OCA\Social\Service\MiscService;
-use OCA\Social\Service\NoteService;
+use OCA\Social\Service\StreamService;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -50,8 +50,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class NoteBoost extends Base {
- /** @var NoteService */
- private $noteService;
+ /** @var StreamService */
+ private $streamService;
/** @var AccountService */
private $accountService;
@@ -67,17 +67,17 @@ class NoteBoost extends Base {
* NoteBoost constructor.
*
* @param AccountService $accountService
- * @param NoteService $noteService
+ * @param StreamService $streamService
* @param BoostService $boostService
* @param MiscService $miscService
*/
public function __construct(
- AccountService $accountService, NoteService $noteService, BoostService $boostService,
+ AccountService $accountService, StreamService $streamService, BoostService $boostService,
MiscService $miscService
) {
parent::__construct();
- $this->noteService = $noteService;
+ $this->streamService = $streamService;
$this->boostService = $boostService;
$this->accountService = $accountService;
$this->miscService = $miscService;
@@ -108,7 +108,7 @@ class NoteBoost extends Base {
$noteId = $input->getArgument('note_id');
$actor = $this->accountService->getActorFromUserId($userId);
- $this->noteService->setViewer($actor);
+ $this->streamService->setViewer($actor);
if (!$input->getOption('unboost')) {
$activity = $this->boostService->create($actor, $noteId, $token);
diff --git a/lib/Command/NoteLike.php b/lib/Command/NoteLike.php
index d2618c0d..6c45bb95 100644
--- a/lib/Command/NoteLike.php
+++ b/lib/Command/NoteLike.php
@@ -36,7 +36,7 @@ use OC\Core\Command\Base;
use OCA\Social\Service\AccountService;
use OCA\Social\Service\LikeService;
use OCA\Social\Service\MiscService;
-use OCA\Social\Service\NoteService;
+use OCA\Social\Service\StreamService;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -51,8 +51,8 @@ use Symfony\Component\Console\Output\OutputInterface;
class NoteLike extends Base {
- /** @var NoteService */
- private $noteService;
+ /** @var StreamService */
+ private $streamService;
/** @var AccountService */
private $accountService;
@@ -68,17 +68,17 @@ class NoteLike extends Base {
* NoteBoost constructor.
*
* @param AccountService $accountService
- * @param NoteService $noteService
+ * @param StreamService $streamService
* @param LikeService $likeService
* @param MiscService $miscService
*/
public function __construct(
- AccountService $accountService, NoteService $noteService, LikeService $likeService,
+ AccountService $accountService, StreamService $streamService, LikeService $likeService,
MiscService $miscService
) {
parent::__construct();
- $this->noteService = $noteService;
+ $this->streamService = $streamService;
$this->likeService = $likeService;
$this->accountService = $accountService;
$this->miscService = $miscService;
@@ -109,7 +109,7 @@ class NoteLike extends Base {
$noteId = $input->getArgument('note_id');
$actor = $this->accountService->getActorFromUserId($userId);
- $this->noteService->setViewer($actor);
+ $this->streamService->setViewer($actor);
if (!$input->getOption('unlike')) {
$activity = $this->likeService->create($actor, $noteId, $token);
diff --git a/lib/Command/StreamDetails.php b/lib/Command/StreamDetails.php
new file mode 100644
index 00000000..f6e952b5
--- /dev/null
+++ b/lib/Command/StreamDetails.php
@@ -0,0 +1,145 @@
+<?php
+declare(strict_types=1);
+
+
+/**
+ * Nextcloud - Social Support
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Maxence Lange <maxence@artificial-owl.com>
+ * @copyright 2018, Maxence Lange <maxence@artificial-owl.com>
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+namespace OCA\Social\Command;
+
+
+use Exception;
+use OCA\Social\Exceptions\StreamNotFoundException;
+use OCA\Social\Model\ActivityPub\Actor\Person;
+use OCA\Social\Service\DetailsService;
+use OCA\Social\Service\MiscService;
+use OCA\Social\Service\StreamService;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\ConsoleOutput;
+use Symfony\Component\Console\Output\OutputInterface;
+
+
+/**
+ * Class StreamDetails
+ *
+ * @package OCA\Social\Command
+ */
+class StreamDetails extends ExtendedBase {
+
+
+ /** @var StreamService */
+ private $streamService;
+
+ /** @var DetailsService */
+ private $detailsService;
+
+ /** @var MiscService */
+ private $miscService;
+
+
+ /**
+ * StreamDetails constructor.
+ *
+ * @param StreamService $streamService
+ * @param DetailsService $detailsService
+ * @param MiscService $miscService
+ */
+ public function __construct(
+ StreamService $streamService, DetailsService $detailsService, MiscService $miscService
+ ) {
+ parent::__construct();
+
+ $this->streamService = $streamService;
+ $this->detailsService = $detailsService;
+ $this->miscService = $miscService;
+ }
+
+
+ /**
+ *
+ */
+ protected function configure() {
+ parent::configure();
+ $this->setName('social:details')
+ ->addArgument('streamId', InputArgument::REQUIRED, 'Id of the Stream item')
+ ->addOption('json', '', InputOption::VALUE_NONE, 'return JSON format')
+ ->setDescription('Get details about a Stream item');
+ }
+
+
+ /**
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ *
+ * @throws Exception
+ */
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $output = new ConsoleOutput();
+ $this->output = $output->section();
+
+ $this->asJson = $input->getOption('json');
+ $streamId = $input->getArgument('streamId');
+
+ try {
+ $stream = $this->streamService->getStreamById($streamId);
+ } catch (StreamNotFoundException $e) {
+ throw new Exception('Unknown item');
+ }
+
+ $details = $this->detailsService->generateDetailsFromStream($stream);
+
+ if ($this->asJson) {
+ $this->output->writeln(json_encode($details, JSON_PRETTY_PRINT));
+
+ return;
+ }
+
+ $this->outputStream($stream);
+ $this->output->writeln('');
+
+ $this->output->writeln('<comment>Affected Timelines</comment>:');
+ $home = array_map(
+ function(Person $item): string {
+ return $item->getUserId();
+ }, $details->getHomeViewers()
+ );
+
+ $this->output->writeln('* <info>Home</info>: ' . json_encode($home, JSON_PRETTY_PRINT));
+ $direct = array_map(
+ function(Person $item): string {
+ return $item->getUserId();
+ }, $details->getDirectViewers()
+ );
+
+ $this->output->writeln('* <info>Direct</info>: ' . json_encode($direct, JSON_PRETTY_PRINT));
+ $this->output->writeln('* <info>Public</info>: ' . ($details->isPublic() ? 'true' : 'false'));
+ $this->output->writeln('* <info>Federated</info>: ' . ($details->isFederated() ? 'true' : 'true'));
+ }
+
+}
+
diff --git a/lib/Command/Timeline.php b/lib/Command/Timeline.php
index d2120ead..2379cb93 100644
--- a/lib/Command/Timeline.php
+++ b/lib/Command/Timeline.php
@@ -31,22 +31,13 @@ declare(strict_types=1);
namespace OCA\Social\Command;
-use daita\MySmallPhpTools\Exceptions\CacheItemNotFoundException;
use Exception;
-use OC\Core\Command\Base;
-use OCA\Social\AP;
use OCA\Social\Db\StreamRequest;
-use OCA\Social\Exceptions\ItemUnknownException;
-use OCA\Social\Exceptions\RedundancyLimitException;
-use OCA\Social\Exceptions\SocialAppConfigException;
use OCA\Social\Model\ActivityPub\Actor\Person;
-use OCA\Social\Model\ActivityPub\Stream;
use OCA\Social\Service\AccountService;
-use OCA\Social\Service\CacheActorService;
use OCA\Social\Service\ConfigService;
use OCA\Social\Service\MiscService;
use OCP\IUserManager;
-use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -59,7 +50,7 @@ use Symfony\Component\Console\Output\OutputInterface;
*
* @package OCA\Social\Command
*/
-class Timeline extends Base {
+class Timeline extends ExtendedBase {
/** @var IUserManager */
private $userManager;
@@ -77,18 +68,12 @@ class Timeline extends Base {
private $miscService;
- /** @var OutputInterface */
- private $output;
-
- /** @var bool */
- private $asJson;
-
/** @var int */
private $count;
/**
- * Stream constructor.
+ * Timeline constructor.
*
* @param IUserManager $userManager
* @param StreamRequest $streamRequest
@@ -147,25 +132,13 @@ class Timeline extends Base {
$actor = $this->accountService->getActor($userId);
- $this->outputActor($actor);
+ if (!$this->asJson) {
+ $this->outputActor($actor);
+ }
$this->displayStream($actor, $timeline);
}
- /**
- * @param Person $actor
- */
- private function outputActor(Person $actor) {
- if ($this->asJson) {
- return;
- }
-
- $this->output->writeln('<info>Account</info>: ' . $actor->getAccount());
- $this->output->writeln('<info>Id</info>: ' . $actor->getId());
- $this->output->writeln('');
-
- }
-
/**
* @param Person $actor
@@ -177,27 +150,27 @@ class Timeline extends Base {
switch ($timeline) {
case 'home':
$stream = $this->streamRequest->getTimelineHome($actor, 0, $this->count);
- $this->outputStream($stream);
+ $this->outputStreams($stream);
break;
case 'direct':
$stream = $this->streamRequest->getTimelineDirect($actor, 0, $this->count);
- $this->outputStream($stream);
+ $this->outputStreams($stream);
break;
case 'notifications':
$stream = $this->streamRequest->getTimelineNotifications($actor, 0, $this->count);
- $this->outputStream($stream);
+ $this->outputStreams($stream);
break;
case 'local':
$stream = $this->streamRequest->getTimelineGlobal(0, $this->count, true);
- $this->outputStream($stream);
+ $this->outputStreams($stream);
break;
case 'global':
$stream = $this->streamRequest->getTimelineGlobal(0, $this->count, false);
- $this->outputStream($stream);
+ $this->outputStreams($stream);
break;
default:
@@ -207,61 +180,5 @@ class Timeline extends Base {
}
}
-
- /**
- * @param Stream[] $stream
- */
- private function outputStream(array $stream) {
- if ($this->asJson) {
- $this->output->writeln(json_encode($stream, JSON_PRETTY_PRINT));
- }
-
- $table = new Table($this->output);
- $table->setHeaders(['Id', 'Source', 'Type', 'Author', 'Content']);
- $table->render();
- $this->output->writeln('');
-
- foreach ($stream as $item) {
- $objectId = $item->getObjectId();
- $cache = $item->getCache();
- $content = '';
- $author = '';
- if ($objectId !== '' && $cache->hasItem($objectId)) {
- try {
- $cachedObject = $cache->getItem($objectId)
- ->getObject();
-
- /** @var Stream $cachedItem */
- $cachedItem = AP::$activityPub->getItemFromData($cachedObject);
- $content = $cachedItem->getContent();
- $author = $cachedItem->getActor()
- ->getAccount();
- } catch (CacheItemNotFoundException $e) {
- } catch (ItemUnknownException $e) {
- } catch (RedundancyLimitException $e) {
- } catch (SocialAppConfigException $e) {
- }
- } else {
- $content = $item->getContent();
- $author = $item->getActor()
- ->getAccount();
- }
-
- $table->appendRow(
- [
- '<comment>' . $item->getId() . '</comment>',
- '<info>' . $item->getActor()
- ->getAccount() . '</info>',
- $item->getType(),
- '<info>' . $author . '</info>',
- $content,
- ]
- );
- }
-
-
- }
-
-
}
diff --git a/lib/Db/CoreRequestBuilder.php b/lib/Db/CoreRequestBuilder.php
index 7210e19c..040c1736 100644
--- a/lib/Db/CoreRequestBuilder.php
+++ b/lib/Db/CoreRequestBuilder.php
@@ -767,7 +767,40 @@ class CoreRequestBuilder {
$qb->leftJoin(
$this->defaultSelectAlias, CoreRequestBuilder::TABLE_CACHE_ACTORS, 'ca', $orX
);
+ }
+
+
+ /**
+ * @param IQueryBuilder $qb
+ * @param string $fieldActorId
+ * @param string $alias
+ */
+ protected function leftJoinAccounts(IQueryBuilder &$qb, string $fieldActorId, string $alias = ''
+ ) {
+ if ($qb->getType() !== QueryBuilder::SELECT) {
+ return;
+ }
+
+ $expr = $qb->expr();
+ $func = $qb->func();
+
+ $pf = ($alias === '') ? $this->defaultSelectAlias : $alias;
+ $qb->selectAlias('lja.id', 'accounts_id')
+ ->selectAlias('lja.user_id', 'accounts_user_id')
+ ->selectAlias('lja.preferred_username', 'accounts_preferred_username')
+ ->selectAlias('lja.name', 'accounts_name')
+ ->selectAlias('lja.summary', 'accounts_summary')
+ ->selectAlias('lja.public_key', 'accounts_public_key');
+
+ $on = $expr->eq(
+ $func->lower($pf . '.' . $fieldActorId),
+ $func->lower('lja.id')
+ );
+
+ $qb->leftJoin(
+ $this->defaultSelectAlias, CoreRequestBuilder::TABLE_ACTORS, 'lja', $on
+ );
}
@@ -798,6 +831,31 @@ class CoreRequestBuilder {
/**
+ * @param array $data
+ *
+ * @return Person
+ * @throws InvalidResourceException
+ */
+ protected function parseAccountsLeftJoin(array $data): Person {
+ $new = [];
+ foreach ($data as $k => $v) {
+ if (substr($k, 0, 9) === 'accounts_') {
+ $new[substr($k, 9)] = $v;
+ }
+ }
+
+ $actor = new Person();
+ $actor->importFromDatabase($new);
+
+ if (!$actor->getUserId()) {
+ throw new InvalidResourceException();
+ }
+
+ return $actor;
+ }
+
+
+ /**
* @param IQueryBuilder $qb
*/
protected function leftJoinStreamAction(IQueryBuilder &$qb) {
diff --git a/lib/Db/FollowsRequest.php b/lib/Db/FollowsRequest.php
index 8e478caf..77ab6894 100644
--- a/lib/Db/FollowsRequest.php
+++ b/lib/Db/FollowsRequest.php
@@ -236,6 +236,28 @@ class FollowsRequest extends FollowsRequestBuilder {
/**
+ * @param string $followId
+ *
+ * @return Follow[]
+ */
+ public function getFollowersByFollowId(string $followId): array {
+ $qb = $this->getFollowsSelectSql();
+ $this->limitToFollowId($qb, $followId);
+ $this->limitToAccepted($qb, true);
+ $this->leftJoinAccounts($qb, 'actor_id');
+
+ $follows = [];
+ $cursor = $qb->execute();
+ while ($data = $cursor->fetch()) {
+ $follows[] = $this->parseFollowsSelectSql($data);
+ }
+ $cursor->closeCursor();
+
+ return $follows;
+ }
+
+
+ /**
* @param Follow $follow
*/
public function delete(Follow $follow) {
diff --git a/lib/Db/FollowsRequestBuilder.php b/lib/Db/FollowsRequestBuilder.php
index 9d08dfbf..a41e7114 100644
--- a/