use Mastodon API instead of scraping

fixes #3151

Signed-off-by: call-me-matt <nextcloud@matthiasheinisch.de>
This commit is contained in:
call-me-matt 2023-01-01 15:18:40 +01:00
parent 753a66b309
commit 4d8449d971
2 changed files with 58 additions and 35 deletions

View File

@ -31,7 +31,7 @@ class MastodonProvider implements ISocialProvider {
private $httpClient; private $httpClient;
/** @var string */ /** @var string */
public $name = "mastodon"; public $name = 'mastodon';
public function __construct(IClientService $httpClient) { public function __construct(IClientService $httpClient) {
$this->httpClient = $httpClient->NewClient(); $this->httpClient = $httpClient->NewClient();
@ -45,7 +45,7 @@ class MastodonProvider implements ISocialProvider {
* @return bool * @return bool
*/ */
public function supportsContact(array $contact):bool { public function supportsContact(array $contact):bool {
if (!array_key_exists("X-SOCIALPROFILE",$contact)) { if (!array_key_exists('X-SOCIALPROFILE', $contact)) {
return false; return false;
} }
$profiles = $this->getProfileIds($contact); $profiles = $this->getProfileIds($contact);
@ -62,7 +62,6 @@ class MastodonProvider implements ISocialProvider {
public function getImageUrls(array $contact):array { public function getImageUrls(array $contact):array {
$profileIds = $this->getProfileIds($contact); $profileIds = $this->getProfileIds($contact);
$urls = []; $urls = [];
foreach ($profileIds as $profileId) { foreach ($profileIds as $profileId) {
$url = $this->getImageUrl($profileId); $url = $this->getImageUrl($profileId);
if (isset($url)) { if (isset($url)) {
@ -82,21 +81,15 @@ class MastodonProvider implements ISocialProvider {
public function getImageUrl(string $profileUrl):?string { public function getImageUrl(string $profileUrl):?string {
try { try {
$result = $this->httpClient->get($profileUrl); $result = $this->httpClient->get($profileUrl);
$jsonResult = json_decode($result->getBody());
$htmlResult = new \DOMDocument(); return $jsonResult->avatar;
$htmlResult->loadHTML($result->getBody());
$img = $htmlResult->getElementById('profile_page_avatar');
if (!is_null($img)) {
return $img->getAttribute("data-original");
}
return null;
} catch (\Exception $e) { } catch (\Exception $e) {
return null; return null;
} }
} }
/** /**
* Returns all possible profile ids for contact * Returns all possible profile URI for contact by searching the mastodon instance
* *
* @param {array} contact information * @param {array} contact information
* *
@ -108,9 +101,21 @@ class MastodonProvider implements ISocialProvider {
if (isset($socialprofiles)) { if (isset($socialprofiles)) {
foreach ($socialprofiles as $profile) { foreach ($socialprofiles as $profile) {
if (strtolower($profile['type']) == $this->name) { if (strtolower($profile['type']) == $this->name) {
$profileId = $this->cleanupId($profile['value']); $masto_user_server = $this->cleanupId($profile['value']);
if (isset($profileId)) { if (isset($masto_user_server)) {
$profileIds[] = $profileId; try {
[$masto_user, $masto_server] = $masto_user_server;
# search for user on Mastodon
$search = $masto_server . '/api/v2/search?q=' . $masto_user;
$result = $this->httpClient->get($search);
$jsonResult = json_decode($result->getBody());
# take first search result
$masto_id = $jsonResult->accounts[0]->id;
$profileId = $masto_server . "/api/v1/accounts/" . $masto_id;
$profileIds[] = $profileId;
} catch (\Exception $e) {
continue;
}
} }
} }
} }
@ -123,18 +128,25 @@ class MastodonProvider implements ISocialProvider {
* *
* @param {string} the value from the contact's x-socialprofile * @param {string} the value from the contact's x-socialprofile
* *
* @return string * @return array username and server instance
*/ */
protected function cleanupId(string $candidate):?string { protected function cleanupId(string $candidate):?array {
$candidate = preg_replace('/^' . preg_quote('x-apple:', '/') . '/', '', $candidate); $candidate = preg_replace('/^' . preg_quote('x-apple:', '/') . '/', '', $candidate);
try { try {
$user_server = explode('@', $candidate);
if (strpos($candidate, 'http') !== 0) { if (strpos($candidate, 'http') !== 0) {
$user_server = explode('@', $candidate); $masto_server = "https://" . array_pop($user_server);
$candidate = 'https://' . array_pop($user_server) . '/@' . array_pop($user_server); $masto_user = array_pop($user_server);
} else {
$masto_user = array_pop($user_server);
$masto_server = array_pop($user_server);
} }
if ((empty($masto_server)) || (empty($masto_user))) {
return null;
}
return [$masto_user, $masto_server];
} catch (\Exception $e) { } catch (\Exception $e) {
$candidate = null; return null;
} }
return $candidate;
} }
} }

View File

@ -90,20 +90,33 @@ class MastodonProviderTest extends TestCase {
$contactWithSocial = [ $contactWithSocial = [
'X-SOCIALPROFILE' => [ 'X-SOCIALPROFILE' => [
["value" => "user1@cloud1", "type" => "mastodon"], ["value" => "user1@cloud1", "type" => "mastodon"],
["value" => "user2@cloud2", "type" => "mastodon"] ["value" => "@user2@cloud2", "type" => "mastodon"],
["value" => "https://cloud3/@user3", "type" => "mastodon"],
["value" => "https://cloud/wrongSyntax", "type" => "mastodon"],
["value" => "@wrongSyntax", "type" => "mastodon"],
["value" => "wrongSyntax", "type" => "mastodon"]
] ]
]; ];
$contactWithSocialUrls = [ $contactWithSocialUrls = [
"https://cloud1/@user1", "https://cloud1/api/v2/search?q=user1",
"https://cloud2/@user2", "https://cloud2/api/v2/search?q=user2",
"https://cloud3//api/v2/search?q=user3",
"https://cloud1/api/v1/accounts/1",
"https://cloud2/api/v1/accounts/2",
"https://cloud3//api/v1/accounts/3"
]; ];
$contactWithSocialHtml = [ $contactWithSocialApi = [
'<html><profile id="profile_page_avatar" data-original="user1.jpg" /></html>', '{"accounts":[{"id":"1","username":"user1"}]}',
'<html><profile id="profile_page_avatar" data-original="user2.jpg" /></html>' '{"accounts":[{"id":"2","username":"user2"}]}',
'{"accounts":[{"id":"3","username":"user3"}]}',
'{"id":"1","avatar":"user1.jpg"}',
'{"id":"2","avatar":"user2.jpg"}',
'{"id":"3","avatar":"user3.jpg"}'
]; ];
$contactWithSocialImgs = [ $contactWithSocialImgs = [
"user1.jpg", "user1.jpg",
"user2.jpg" "user2.jpg",
"user3.jpg"
]; ];
$contactWithoutSocial = [ $contactWithoutSocial = [
@ -113,19 +126,19 @@ class MastodonProviderTest extends TestCase {
] ]
]; ];
$contactWithoutSocialUrls = []; $contactWithoutSocialUrls = [];
$contactWithoutSocialHtml = []; $contactWithoutSocialApi = [];
$contactWithoutSocialImgs = []; $contactWithoutSocialImgs = [];
return [ return [
'contact with mastodon fields' => [ 'contact with mastodon fields' => [
$contactWithSocial, $contactWithSocial,
$contactWithSocialHtml, $contactWithSocialApi,
$contactWithSocialUrls, $contactWithSocialUrls,
$contactWithSocialImgs $contactWithSocialImgs
], ],
'contact without mastodon fields' => [ 'contact without mastodon fields' => [
$contactWithoutSocial, $contactWithoutSocial,
$contactWithoutSocialHtml, $contactWithoutSocialApi,
$contactWithoutSocialUrls, $contactWithoutSocialUrls,
$contactWithoutSocialImgs $contactWithoutSocialImgs
] ]
@ -135,9 +148,9 @@ class MastodonProviderTest extends TestCase {
/** /**
* @dataProvider dataProviderGetImageUrls * @dataProvider dataProviderGetImageUrls
*/ */
public function testGetImageUrls($contact, $htmls, $urls, $imgs) { public function testGetImageUrls($contact, $api, $urls, $imgs) {
if (count($urls)) { if (count($urls)) {
$this->response->method("getBody")->willReturnOnConsecutiveCalls(...$htmls); $this->response->method("getBody")->willReturnOnConsecutiveCalls(...$api);
$this->client $this->client
->expects($this->exactly(count($urls))) ->expects($this->exactly(count($urls)))
->method("get") ->method("get")
@ -146,8 +159,6 @@ class MastodonProviderTest extends TestCase {
}, $urls)) }, $urls))
->willReturn($this->response); ->willReturn($this->response);
} }
$result = $this->provider->getImageUrls($contact); $result = $this->provider->getImageUrls($contact);
$this->assertEquals($imgs, $result); $this->assertEquals($imgs, $result);
} }