From 5d9ce800076f69b0de5c1d1d18b2e5a87de772ad Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Sat, 12 Nov 2022 16:50:28 -0500 Subject: [PATCH 1/3] [Database version 1489] Add new report database tables --- database.sql | 31 +++++++++++++++++++++++++++++- doc/database.md | 2 ++ doc/database/db_report-post.md | 30 +++++++++++++++++++++++++++++ doc/database/db_report.md | 35 ++++++++++++++++++++++++++++++++++ static/dbstructure.config.php | 29 +++++++++++++++++++++++++++- 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 doc/database/db_report-post.md create mode 100644 doc/database/db_report.md diff --git a/database.sql b/database.sql index 9f96b7bbf..71a5f0ae5 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2022.12-dev (Giant Rhubarb) --- DB_UPDATE_VERSION 1488 +-- DB_UPDATE_VERSION 1489 -- ------------------------------------------ @@ -1646,6 +1646,35 @@ CREATE TABLE IF NOT EXISTS `register` ( FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='registrations requiring admin approval'; +-- +-- TABLE report +-- +CREATE TABLE IF NOT EXISTS `report` ( + `id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID', + `uid` mediumint unsigned NOT NULL COMMENT 'Reporting user', + `cid` int unsigned NOT NULL COMMENT 'Reported contact', + `comment` text COMMENT 'Report', + `forward` boolean COMMENT 'Forward the report to the remote server', + `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', + PRIMARY KEY(`id`), + INDEX `uid` (`uid`), + INDEX `cid` (`cid`), + FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`cid`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE +) DEFAULT COLLATE utf8mb4_general_ci COMMENT=''; + +-- +-- TABLE report-post +-- +CREATE TABLE IF NOT EXISTS `report-post` ( + `rid` int unsigned NOT NULL COMMENT 'Report id', + `uri-id` int unsigned NOT NULL COMMENT 'Uri-id of the reported post', + PRIMARY KEY(`rid`,`uri-id`), + INDEX `uri-id` (`uri-id`), + FOREIGN KEY (`rid`) REFERENCES `report` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE +) DEFAULT COLLATE utf8mb4_general_ci COMMENT=''; + -- -- TABLE search -- diff --git a/doc/database.md b/doc/database.md index 1ffbf91f5..4259749d2 100644 --- a/doc/database.md +++ b/doc/database.md @@ -74,6 +74,8 @@ Database Tables | [profile_field](help/database/db_profile_field) | Custom profile fields | | [push_subscriber](help/database/db_push_subscriber) | Used for OStatus: Contains feed subscribers | | [register](help/database/db_register) | registrations requiring admin approval | +| [report](help/database/db_report) | | +| [report-post](help/database/db_report-post) | | | [search](help/database/db_search) | | | [session](help/database/db_session) | web session storage | | [storage](help/database/db_storage) | Data stored by Database storage backend | diff --git a/doc/database/db_report-post.md b/doc/database/db_report-post.md new file mode 100644 index 000000000..303aa3256 --- /dev/null +++ b/doc/database/db_report-post.md @@ -0,0 +1,30 @@ +Table report-post +=========== + + + +Fields +------ + +| Field | Description | Type | Null | Key | Default | Extra | +| ------ | --------------------------- | ------------ | ---- | --- | ------- | ----- | +| rid | Report id | int unsigned | NO | PRI | NULL | | +| uri-id | Uri-id of the reported post | int unsigned | NO | PRI | NULL | | + +Indexes +------------ + +| Name | Fields | +| ------- | ----------- | +| PRIMARY | rid, uri-id | +| uri-id | uri-id | + +Foreign Keys +------------ + +| Field | Target Table | Target Field | +|-------|--------------|--------------| +| rid | [report](help/database/db_report) | id | +| uri-id | [item-uri](help/database/db_item-uri) | id | + +Return to [database documentation](help/database) diff --git a/doc/database/db_report.md b/doc/database/db_report.md new file mode 100644 index 000000000..6e52636eb --- /dev/null +++ b/doc/database/db_report.md @@ -0,0 +1,35 @@ +Table report +=========== + + + +Fields +------ + +| Field | Description | Type | Null | Key | Default | Extra | +| ------- | --------------------------------------- | ------------------ | ---- | --- | ------------------- | -------------- | +| id | sequential ID | int unsigned | NO | PRI | NULL | auto_increment | +| uid | Reporting user | mediumint unsigned | NO | | NULL | | +| cid | Reported contact | int unsigned | NO | | NULL | | +| comment | Report | text | YES | | NULL | | +| forward | Forward the report to the remote server | boolean | YES | | NULL | | +| created | | datetime | NO | | 0001-01-01 00:00:00 | | + +Indexes +------------ + +| Name | Fields | +| ------- | ------ | +| PRIMARY | id | +| uid | uid | +| cid | cid | + +Foreign Keys +------------ + +| Field | Target Table | Target Field | +|-------|--------------|--------------| +| uid | [user](help/database/db_user) | uid | +| cid | [contact](help/database/db_contact) | id | + +Return to [database documentation](help/database) diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 343cf3020..17064fd95 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -55,7 +55,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1488); + define('DB_UPDATE_VERSION', 1489); } return [ @@ -1649,6 +1649,33 @@ return [ "uid" => ["uid"], ] ], + "report" => [ + "comment" => "", + "fields" => [ + "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"], + "uid" => ["type" => "mediumint unsigned", "not null" => "1", "foreign" => ["user" => "uid"], "comment" => "Reporting user"], + "cid" => ["type" => "int unsigned", "not null" => "1", "foreign" => ["contact" => "id"], "comment" => "Reported contact"], + "comment" => ["type" => "text", "comment" => "Report"], + "forward" => ["type" => "boolean", "comment" => "Forward the report to the remote server"], + "created" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""], + ], + "indexes" => [ + "PRIMARY" => ["id"], + "uid" => ["uid"], + "cid" => ["cid"], + ] + ], + "report-post" => [ + "comment" => "", + "fields" => [ + "rid" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["report" => "id"], "comment" => "Report id"], + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Uri-id of the reported post"], + ], + "indexes" => [ + "PRIMARY" => ["rid", "uri-id"], + "uri-id" => ["uri-id"], + ] + ], "search" => [ "comment" => "", "fields" => [ From 17a3a482107f2bb23f6fda5364d738a1568d56a5 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Sat, 12 Nov 2022 19:58:56 -0500 Subject: [PATCH 2/3] Add new Moderation\Report domain classes --- src/Moderation/Entity/Report.php | 60 ++++++++ src/Moderation/Factory/Report.php | 72 ++++++++++ src/Moderation/Repository/Report.php | 95 +++++++++++++ tests/src/Moderation/Factory/ReportTest.php | 149 ++++++++++++++++++++ 4 files changed, 376 insertions(+) create mode 100644 src/Moderation/Entity/Report.php create mode 100644 src/Moderation/Factory/Report.php create mode 100644 src/Moderation/Repository/Report.php create mode 100644 tests/src/Moderation/Factory/ReportTest.php diff --git a/src/Moderation/Entity/Report.php b/src/Moderation/Entity/Report.php new file mode 100644 index 000000000..93d42b94e --- /dev/null +++ b/src/Moderation/Entity/Report.php @@ -0,0 +1,60 @@ +. + * + */ + +namespace Friendica\Moderation\Entity; + +/** + * @property-read int $id + * @property-read int $uid + * @property-read int $cid + * @property-read string $comment + * @property-read bool $forward + * @property-read array $postUriIds + * @property-read \DateTime|null $created + */ +class Report extends \Friendica\BaseEntity +{ + /** @var int|null */ + protected $id; + /** @var int ID of the user making a moderation report*/ + protected $uid; + /** @var int ID of the contact being reported*/ + protected $cid; + /** @var string Optional comment */ + protected $comment; + /** @var bool Whether this report should be forwarded to the remote server */ + protected $forward; + /** @var \DateTime|null When the report was created */ + protected $created; + /** @var array Optional list of URI IDs of posts supporting the report*/ + protected $postUriIds; + + public function __construct(int $uid, int $cid, \DateTime $created, string $comment = '', bool $forward = false, array $postUriIds = [], int $id = null) + { + $this->uid = $uid; + $this->cid = $cid; + $this->created = $created; + $this->comment = $comment; + $this->forward = $forward; + $this->postUriIds = $postUriIds; + $this->id = $id; + } +} diff --git a/src/Moderation/Factory/Report.php b/src/Moderation/Factory/Report.php new file mode 100644 index 000000000..17203d307 --- /dev/null +++ b/src/Moderation/Factory/Report.php @@ -0,0 +1,72 @@ +. + * + */ + +namespace Friendica\Moderation\Factory; + +use Friendica\Capabilities\ICanCreateFromTableRow; +use Friendica\Moderation\Entity; + +class Report extends \Friendica\BaseFactory implements ICanCreateFromTableRow +{ + /** + * @param array $row `report` table row + * @param array $postUriIds List of post URI ids from the `report-post` table + * @return Entity\Report + * @throws \Exception + */ + public function createFromTableRow(array $row, array $postUriIds = []): Entity\Report + { + return new Entity\Report( + $row['uid'], + $row['cid'], + new \DateTime($row['created'] ?? 'now', new \DateTimeZone('UTC')), + $row['comment'], + $row['forward'], + $postUriIds, + $row['id'], + ); + } + + /** + * Creates a Report entity from a Mastodon API /reports request + * + * @see \Friendica\Module\Api\Mastodon\Reports::post() + * + * @param int $uid + * @param int $cid + * @param string $comment + * @param bool $forward + * @param array $postUriIds + * @return Entity\Report + * @throws \Exception + */ + public function createFromReportsRequest(int $uid, int $cid, string $comment = '', bool $forward = false, array $postUriIds = []): Entity\Report + { + return new Entity\Report( + $uid, + $cid, + new \DateTime('now', new \DateTimeZone('UTC')), + $comment, + $forward, + $postUriIds, + ); + } +} diff --git a/src/Moderation/Repository/Report.php b/src/Moderation/Repository/Report.php new file mode 100644 index 000000000..3ffc0de8a --- /dev/null +++ b/src/Moderation/Repository/Report.php @@ -0,0 +1,95 @@ +. + * + */ + +namespace Friendica\Moderation\Repository; + +use Friendica\BaseEntity; +use Friendica\Core\Logger; +use Friendica\Database\Database; +use Friendica\Model\Post; +use Friendica\Network\HTTPException\NotFoundException; +use Friendica\Util\DateTimeFormat; +use Psr\Log\LoggerInterface; + +class Report extends \Friendica\BaseRepository +{ + protected static $table_name = 'report'; + + /** + * @var \Friendica\Moderation\Factory\Report + */ + protected $factory; + + public function __construct(Database $database, LoggerInterface $logger, \Friendica\Moderation\Factory\Report $factory) + { + parent::__construct($database, $logger, $factory); + + $this->factory = $factory; + } + + public function selectOneById(int $lastInsertId): \Friendica\Moderation\Factory\Report + { + return $this->_selectOne(['id' => $lastInsertId]); + } + + public function save(\Friendica\Moderation\Entity\Report $Report) + { + $fields = [ + 'uid' => $Report->uid, + 'cid' => $Report->cid, + 'comment' => $Report->comment, + 'forward' => $Report->forward, + ]; + + if ($Report->id) { + $this->db->update(self::$table_name, $fields, ['id' => $Report->id]); + } else { + $fields['created'] = DateTimeFormat::utcNow(); + $this->db->insert(self::$table_name, $fields, Database::INSERT_IGNORE); + + $Report = $this->selectOneById($this->db->lastInsertId()); + } + + $this->db->delete('report-post', ['rid' => $Report->id]); + + foreach ($Report->postUriIds as $uriId) { + if (Post::exists(['uri-id' => $uriId])) { + $this->db->insert('report-post', ['rid' => $Report->id, 'uri-id' => $uriId]); + } else { + Logger::notice('Post does not exist', ['uri-id' => $uriId, 'report' => $Report]); + } + } + + return $Report; + } + + protected function _selectOne(array $condition, array $params = []): BaseEntity + { + $fields = $this->db->selectFirst(static::$table_name, [], $condition, $params); + if (!$this->db->isResult($fields)) { + throw new NotFoundException(); + } + + $postUriIds = array_column($this->db->selectToArray('report-post', ['uri-id'], ['rid' => $condition['id'] ?? 0]), 'uri-id'); + + return $this->factory->createFromTableRow($fields, $postUriIds); + } +} diff --git a/tests/src/Moderation/Factory/ReportTest.php b/tests/src/Moderation/Factory/ReportTest.php new file mode 100644 index 000000000..0f2970910 --- /dev/null +++ b/tests/src/Moderation/Factory/ReportTest.php @@ -0,0 +1,149 @@ +. + * + */ + +namespace Friendica\Test\src\Moderation\Factory; + +use Friendica\Moderation\Factory; +use Friendica\Moderation\Entity; +use Friendica\Test\MockedTest; +use Psr\Log\NullLogger; + +class ReportTest extends MockedTest +{ + public function dataCreateFromTableRow(): array + { + return [ + 'default' => [ + 'row' => [ + 'id' => 11, + 'uid' => 12, + 'cid' => 13, + 'comment' => '', + 'forward' => false, + 'created' => null + ], + 'postUriIds' => [], + 'assertion' => new Entity\Report( + 12, + 13, + new \DateTime('now', new \DateTimeZone('UTC')), + '', + false, + [], + 11, + ), + ], + 'full' => [ + 'row' => [ + 'id' => 11, + 'uid' => 12, + 'cid' => 13, + 'comment' => 'Report', + 'forward' => true, + 'created' => '2021-10-12 12:23:00' + ], + 'postUriIds' => [89, 90], + 'assertion' => new Entity\Report( + 12, + 13, + new \DateTime('2021-10-12 12:23:00', new \DateTimeZone('UTC')), + 'Report', + true, + [89, 90], + 11 + ), + ], + ]; + } + + public function assertReport(Entity\Report $assertion, Entity\Report $report) + { + self::assertEquals( + $assertion->id, + $report->id + ); + self::assertEquals($assertion->uid, $report->uid); + self::assertEquals($assertion->cid, $report->cid); + self::assertEquals($assertion->comment, $report->comment); + self::assertEquals($assertion->forward, $report->forward); + // No way to test "now" at the moment + //self::assertEquals($assertion->created, $report->created); + self::assertEquals($assertion->postUriIds, $report->postUriIds); + } + + /** + * @dataProvider dataCreateFromTableRow + */ + public function testCreateFromTableRow(array $row, array $postUriIds, Entity\Report $assertion) + { + $factory = new Factory\Report(new NullLogger()); + + $this->assertReport($factory->createFromTableRow($row, $postUriIds), $assertion); + } + + public function dataCreateFromReportsRequest(): array + { + return [ + 'default' => [ + 'uid' => 12, + 'cid' => 13, + 'comment' => '', + 'forward' => false, + 'postUriIds' => [], + 'assertion' => new Entity\Report( + 12, + 13, + new \DateTime('now', new \DateTimeZone('UTC')), + '', + false, + [], + null + ), + ], + 'full' => [ + 'uid' => 12, + 'cid' => 13, + 'comment' => 'Report', + 'forward' => true, + 'postUriIds' => [89, 90], + 'assertion' => new Entity\Report( + 12, + 13, + new \DateTime('now', new \DateTimeZone('UTC')), + 'Report', + true, + [89, 90], + null + ), + ], + ]; + } + + /** + * @dataProvider dataCreateFromReportsRequest + */ + public function testCreateFromReportsRequest(int $uid, int $cid, string $comment, bool $forward, array $postUriIds, Entity\Report $assertion) + { + $factory = new Factory\Report(new NullLogger()); + + $this->assertReport($factory->createFromReportsRequest($uid, $cid, $comment, $forward, $postUriIds), $assertion); + } +} From 63fc315ea01e8bcaad2067b7f8ef8b9566c2c071 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Sat, 12 Nov 2022 20:00:02 -0500 Subject: [PATCH 3/3] Add support for Mastodon /reports API call --- src/Module/Api/Mastodon/Reports.php | 74 +++++++++++++++++++++++++++++ static/routes.config.php | 2 +- 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/Module/Api/Mastodon/Reports.php diff --git a/src/Module/Api/Mastodon/Reports.php b/src/Module/Api/Mastodon/Reports.php new file mode 100644 index 000000000..6ae54eb6f --- /dev/null +++ b/src/Module/Api/Mastodon/Reports.php @@ -0,0 +1,74 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon; + +use Friendica\App; +use Friendica\Core\L10n; +use Friendica\Core\System; +use Friendica\Model\Contact; +use Friendica\Module\Api\ApiResponse; +use Friendica\Module\BaseApi; +use Friendica\Network\HTTPException; +use Friendica\Util\Profiler; +use Psr\Log\LoggerInterface; + +/** + * @see https://docs.joinmastodon.org/methods/accounts/reports/ + */ +class Reports extends BaseApi +{ + /** @var \Friendica\Moderation\Factory\Report */ + private $reportFactory; + /** @var \Friendica\Moderation\Repository\Report */ + private $reportRepo; + + public function __construct(\Friendica\Moderation\Repository\Report $reportRepo, \Friendica\Moderation\Factory\Report $reportFactory, App $app, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, ApiResponse $response, array $server, array $parameters = []) + { + parent::__construct($app, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->reportFactory = $reportFactory; + $this->reportRepo = $reportRepo; + } + + public function post(array $request = []) + { + self::checkAllowedScope(self::SCOPE_WRITE); + + $request = $this->getRequest([ + 'account_id' => '', // ID of the account to report + 'status_ids' => [], // Array of Statuses to attach to the report, for context + 'comment' => '', // Reason for the report (default max 1000 characters) + 'forward' => false, // If the account is remote, should the report be forwarded to the remote admin? + ], $request); + + $contact = Contact::getById($request['account_id'], ['id']); + if (empty($contact)) { + throw new HTTPException\NotFoundException('Account ' . $request['account_id'] . ' not found'); + } + + $report = $this->reportFactory->createFromReportsRequest(self::getCurrentUserID(), $request['account_id'], $request['comment'], $request['forward'], $request['status_ids']); + + $this->reportRepo->save($report); + + System::jsonExit([]); + } +} diff --git a/static/routes.config.php b/static/routes.config.php index 0958ee877..43fafb167 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -256,7 +256,7 @@ return [ '/polls/{id:\d+}/votes' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // not supported '/preferences' => [Module\Api\Mastodon\Preferences::class, [R::GET ]], '/push/subscription' => [Module\Api\Mastodon\PushSubscription::class, [R::GET, R::POST, R::PUT, R::DELETE]], - '/reports' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // not supported + '/reports' => [Module\Api\Mastodon\Reports::class, [ R::POST]], '/scheduled_statuses' => [Module\Api\Mastodon\ScheduledStatuses::class, [R::GET ]], '/scheduled_statuses/{id:\d+}' => [Module\Api\Mastodon\ScheduledStatuses::class, [R::GET, R::PUT, R::DELETE]], '/statuses' => [Module\Api\Mastodon\Statuses::class, [ R::POST]],