Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get contact pictures from social networks #1580

Closed
wants to merge 78 commits into from
Closed
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
2fc64dd
first steps with vue, editing the menues
Mar 30, 2020
18ed4e5
added setup instructions
Mar 30, 2020
7d23da2
hide menu when no facebook id present
Mar 31, 2020
057c6ce
higher resolution pictures
Apr 7, 2020
acad8ff
moved all profile pic stuff into ContactDetailsAvatar
Apr 8, 2020
93ce55c
moved profile loader to page controller
Apr 12, 2020
23672ef
add demo image
Apr 12, 2020
9fcf232
handle profile ids also if uri
Apr 13, 2020
4e10add
improved readability
Apr 13, 2020
f86aba8
preparation for avatar settings page to update complete addressbook
Apr 13, 2020
f82fff0
creation of empty settings page
Apr 13, 2020
56e7a97
separated avatar logic
Apr 14, 2020
95f1eb6
considering recommendation from Codacy/PR Quality Review
call-me-matt Apr 14, 2020
09df2f4
moved code from frontend to backend
Apr 15, 2020
b536fea
Merge remote-tracking branch 'upstream/master'
call-me-matt Apr 15, 2020
21927f9
renaming the route into API
call-me-matt Apr 16, 2020
12db28b
implemented contact access in SocialApiController
call-me-matt Apr 16, 2020
19a59bd
ported code to php, fixed responses
call-me-matt Apr 17, 2020
62a98e3
axios token to prevent CSRF exception
call-me-matt Apr 18, 2020
00fccc4
retrieve updated contact
call-me-matt Apr 18, 2020
9cb71e7
Merge remote-tracking branch 'upstream/master'
call-me-matt Apr 18, 2020
8a6a00c
testing only - refresh view after backend updates
call-me-matt Apr 18, 2020
28177a0
fixup! testing only - refresh view after backend updates
skjnldsv Apr 18, 2020
a345eb5
removed test dummy
call-me-matt Apr 18, 2020
c6361ec
first steps towards unit testing
call-me-matt Apr 18, 2020
c260ec2
first steps with unit tests
call-me-matt Apr 19, 2020
c5e5b9c
improving code as reviewed by skjnldsv
call-me-matt Apr 19, 2020
f74139c
accept only supported social networks
call-me-matt Apr 19, 2020
a184e98
retrieve list of supported networks from socialAPI
call-me-matt Apr 19, 2020
51771f1
added support for avatar.io/twitter
call-me-matt Apr 19, 2020
4d2a96a
notify if avatar unchanged
call-me-matt Apr 20, 2020
d990d64
using initial-state to load supported networks
call-me-matt Apr 20, 2020
3eba54f
constants for network connectors, added support for tumblr, removed a…
call-me-matt Apr 21, 2020
55ccf25
added vcard support for v3 and higher
call-me-matt Apr 21, 2020
ff69634
removing number check for fb to allow for company avatars
call-me-matt Apr 21, 2020
e58ddb4
Merge remote-tracking branch 'upstream/master'
call-me-matt Apr 21, 2020
25d6af7
creating stubs for testing
call-me-matt Apr 22, 2020
037444b
allow regex cleanups, data provider for unittests
call-me-matt Apr 22, 2020
eccae0c
icon change from globe to sync
call-me-matt Apr 22, 2020
9a34d07
improved code style after review
call-me-matt Apr 23, 2020
2cc83bf
added lock file for npm lint
call-me-matt Apr 23, 2020
164b585
allowing for selection of social network
call-me-matt Apr 23, 2020
8d7de9b
adapted error return codes
call-me-matt Apr 25, 2020
d8e2937
code cleanup, removed type for less complexity
call-me-matt Apr 25, 2020
b6e1731
removed doubled test case
call-me-matt Apr 25, 2020
87f5f75
improved as recommended by codacy
call-me-matt Apr 25, 2020
63de675
separated vcard image tag creation into own function
call-me-matt Apr 25, 2020
3b3dfdd
reducing complexity
call-me-matt Apr 25, 2020
db096f0
Merge remote-tracking branch 'upstream/master'
call-me-matt Apr 28, 2020
6a813cf
force re-fetch of contacts after update
call-me-matt Apr 28, 2020
cb30db5
prevent error logs for contacts without social profiles/avatars
call-me-matt May 1, 2020
63f05e2
renaming & compatibility with nextcloud v19
call-me-matt May 4, 2020
ba3c38b
using dependency injection
call-me-matt May 4, 2020
2bc3d03
added instagram
call-me-matt May 6, 2020
07426a5
split controller into service
call-me-matt May 7, 2020
4d9e45d
added admin setting to (de)activate social media integration
call-me-matt May 7, 2020
f1aac0b
enable social sync by default
call-me-matt May 7, 2020
81e55f2
changing for vuejs (trying)
call-me-matt May 8, 2020
2f0979e
admin settings & not case sensitive network names
May 9, 2020
133e9b9
reorder menu items according to priority
May 10, 2020
51ee1da
moved all logic from controller to service
call-me-matt May 11, 2020
e5efde0
individual icons per social network (dummies)
call-me-matt May 11, 2020
637fb80
split service into providers
call-me-matt May 15, 2020
0d69e4f
allowing for nextcloud v20
call-me-matt May 15, 2020
2f4becf
code improvements according to codacy
call-me-matt May 15, 2020
c10dc11
added twitter
call-me-matt May 16, 2020
7eb403e
added Mastodon
call-me-matt May 17, 2020
7857b91
Merge remote-tracking branch 'upstream/master'
call-me-matt May 30, 2020
638495a
delete previous photos during update for vCard version 3.0
call-me-matt Jun 18, 2020
04e4a24
cleanup (variables)
call-me-matt Jun 18, 2020
b67c85d
changed icon names to lower case
call-me-matt Jul 9, 2020
ae14dbe
fixed identation
call-me-matt Jul 9, 2020
c131de8
formatting and styling issues
call-me-matt Jul 9, 2020
30eb73f
never trust your inputs
call-me-matt Jul 9, 2020
6ae6abd
using app name from config
call-me-matt Jul 10, 2020
f6eff74
changed method for avatar change to PUT
call-me-matt Jul 10, 2020
381e7b8
use Nextcloud's HTTP service
call-me-matt Jul 10, 2020
09ef3ee
add social icons
call-me-matt Jul 10, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'page#index', 'url' => '/{group}', 'verb' => 'GET', 'postfix' => 'group'],
['name' => 'page#index', 'url' => '/{group}/{contact}', 'verb' => 'GET', 'postfix' => 'group.contact']
['name' => 'page#index', 'url' => '/{group}/{contact}', 'verb' => 'GET', 'postfix' => 'group.contact'],
['name' => 'social_api#fetch', 'url' => '/api/v1/social/{type}/{addressbookId}/{contactId}', 'verb' => 'GET']
]
];
4 changes: 4 additions & 0 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
use OCP\L10N\IFactory;
use OCP\IRequest;

use OCA\Contacts\Controller\SocialApiController;

class PageController extends Controller {

protected $appName;
Expand Down Expand Up @@ -65,9 +67,11 @@ public function __construct(string $appName,
public function index(): TemplateResponse {
$locales = $this->languageFactory->findAvailableLocales();
$defaultProfile = $this->config->getAppValue($this->appName, 'defaultProfile', 'HOME');
$supportedNetworks = SocialApiController::getSupportedNetworks('all');

$this->initialStateService->provideInitialState($this->appName, 'locales', $locales);
$this->initialStateService->provideInitialState($this->appName, 'defaultProfile', $defaultProfile);
$this->initialStateService->provideInitialState($this->appName, 'supportedNetworks', $supportedNetworks);
return new TemplateResponse($this->appName, 'main');
}
}
276 changes: 276 additions & 0 deletions lib/Controller/SocialApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
<?php
/**
* @copyright Copyright (c) 2020 Matthias Heinisch <nextcloud@matthiasheinisch.de>
*
* @author Matthias Heinisch <nextcloud@matthiasheinisch.de>
*
* @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\Contacts\Controller;

use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\TemplateResponse;
// use OCP\IInitialStateService;
use OCP\IConfig;
use OCP\Contacts\IManager;
use OCP\L10N\IFactory;
use OCP\IRequest;


class SocialApiController extends ApiController {

protected $appName;
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved

//** @var IInitialStateService */
// private $initialStateService;

/** @var IFactory */
private $languageFactory;
/** @var IManager */
private $manager;
/** @var IConfig */
private $config;

/**
* This constant stores the supported social networks
* It is an ordered list, so that first listed items will be checked first
* Each item stores the avatar-url-formula as recipe, a cleanup parameter to
* extract the profile-id from the users entry, and possible filters to check
* validity
*
* @const {array} SOCIAL_CONNECTORS dictionary of supported social networks
*/
const SOCIAL_CONNECTORS = [
'facebook' => [
'recipe' => 'https://graph.facebook.com/{socialId}/picture?width=720',
'cleanups' => ['basename'],
'checks' => ['number'],
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
],
'tumblr' => [
'recipe' => 'https://api.tumblr.com/v2/blog/{socialId}/avatar/512',
'cleanups' => ['basename'],
'checks' => [],
],
/* do we trust avatars.io?
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
'instagram' => [
'recipe' => 'http://avatars.io/instagram/{socialId}',
'cleanups' => ['basename'],
'checks' => []
],
'twitter' => [
'recipe' => 'http://avatars.io/twitter/{socialId}',
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
'cleanups' => ['basename'],
'checks' => []
],
*/
];

public function __construct(string $AppName,
IRequest $request,
IManager $manager,
IConfig $config,
// IInitialStateService $initialStateService,
IFactory $languageFactory) {
parent::__construct($AppName, $request);

$this->appName = $AppName;
// $this->initialStateService = $initialStateService;
$this->languageFactory = $languageFactory;
$this->manager = $manager;
$this->config = $config;

}


/**
* @NoAdminRequired
*
* returns an array of supported social networks
*
* @param {String} type the kind of information interested in
* @returns {array} an array of supported social networks
*/
public function getSupportedNetworks(string $type) : ?array {

$supported = array();
$supported['avatar'] = array();

foreach(self::SOCIAL_CONNECTORS as $network => $social) {
array_push($supported['avatar'], $network);
}

if (strcmp($type, 'all') === 0) {
// return array of arrays
return $supported;
}
if (array_key_exists($type, $supported)) {
return $supported[$type];
}
// unknown type
return array();
}

/**
* @NoAdminRequired
*
* generate download url for a social entry (based on type of data requested)
*
* @param {array} socialentry the network and id from the social profile
* @returns {String} the url to the requested information or null in case of errors
*/
protected function getSocialConnector(array $socialentry) : ?string {

$connector = null;

// check supported networks in order
foreach(self::SOCIAL_CONNECTORS as $network => $social) {

// search for this network in user's profile
foreach ($socialentry as $networkentry => $profileId) {
if ($network === strtolower($networkentry)) {
// cleanups
if (in_array('basename', $social['cleanups'])) {
$profileId = basename($profileId);
}
// checks
if (in_array('number', $social['checks'])) {
if (!ctype_digit($profileId)) {
break;
}
}
$connector = str_replace("{socialId}", $profileId, $social['recipe']);
break;
}
}
if ($connector) {
break;
}
}
return ($connector);
}


/**
* @NoAdminRequired
*
* Retrieves social profile data for a contact
*
* @param {String} addressbookId the addressbook identifier
* @param {String} contactId the contact identifier
* @param {String} type the kind of information to retrieve -- provision
*
* @returns {JSONResponse} an empty JSONResponse with respective http status code
*/
public function fetch(string $addressbookId, string $contactId, string $type) : JSONResponse {
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved

$url = null;

try {
// get corresponding addressbook
$addressBooks = $this->manager->getUserAddressBooks();
$addressBook = null;
foreach($addressBooks as $ab) {
if ($ab->getUri() === $addressbookId) {
$addressBook = $ab;
}
}
if (is_null($addressBook)) {
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
}

// search contact in that addressbook
$contact = $addressBook->search($contactId, ['UID'], [])[0];
if (is_null($contact)) {
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
}

// get social data
$socialprofile = $contact['X-SOCIALPROFILE'];
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
if (is_null($socialprofile)) {
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
}

// retrieve data
try {
$url = $this->getSocialConnector($socialprofile);
}
catch (Exception $e) {
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
}

if (empty($url)) {
return new JSONResponse([], Http::STATUS_NOT_IMPLEMENTED);
}

$host = parse_url($url);
if (!$host) {
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
}
$opts = [
"http" => [
"method" => "GET",
"header" => "User-Agent: Nextcloud Contacts App"
]
];
$context = stream_context_create($opts);
$socialdata = file_get_contents($url, false, $context);

$image_type = null;
foreach ($http_response_header as $value) {
if (preg_match('/^Content-Type:/i', $value)) {
if (stripos($value, "image") !== false) {
$image_type = substr($value, stripos($value, "image"));
}
}
}

if ((!$socialdata) || ($image_type === null)) {
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}

// update contact
switch ($type) {
case 'avatar':
if (!empty($contact['PHOTO'])) {
// overwriting without notice!
}
$changes = array();
$changes['URI']=$contact['URI'];
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
$changes['PHOTO'] = "data:" . $image_type . ";base64," . base64_encode($socialdata);
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved

if ($changes['PHOTO'] === $contact['PHOTO']) {
return new JSONResponse([], Http::STATUS_NOT_MODIFIED);
}

$addressBook->createOrUpdate($changes, $addressbookId);
break;
default:
return new JSONResponse([], Http::STATUS_NOT_IMPLEMENTED);
}

}
catch (Exception $e) {
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
}

return new JSONResponse([], Http::STATUS_OK);
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@nextcloud/paths": "^1.1.1",
"@nextcloud/router": "^1.0.2",
"@nextcloud/vue": "1.4.1",
"@nextcloud/axios": "^1.3.2",
"axios": "^0.19.2",
"cdav-library": "git+https://github.com/nextcloud/cdav-library.git",
"core-js": "^3.6.5",
Expand Down
6 changes: 5 additions & 1 deletion src/components/ContactDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@
<!-- contact header -->
<header :style="{ 'backgroundColor': colorAvatar }">
<!-- avatar and upload photo -->
<ContactAvatar :contact="contact" />
<ContactAvatar
:contact="contact"
@updateLocalContact="updateLocalContact"
@refreshContact="refreshContact" />
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
<!-- QUESTION: is it better to pass contact as a prop or get it from the store inside
contact-avatar ? :avatar="contact.photo"-->

Expand All @@ -56,6 +59,7 @@
autocorrect="off"
spellcheck="false"
name="fullname"
@updateLocalContact="updateLocalContact"
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
@input="debounceUpdateContact"
@click="selectInput">
</h2>
Expand Down
Loading