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 17 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']
]
];
176 changes: 176 additions & 0 deletions lib/Controller/SocialApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<?php
/**
* @copyright Copyright (c) 2020 Matthias Heinisch <contacts@matthiasheinisch.de>
*
* @author Matthias Heinisch <contacts@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\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;

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
* @NoCSRFRequired
*
* generate download url for a social entry (based on type of data requested)
*/
protected function getSocialConnector(array $socialentry, string $type) : ?string {
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
foreach ($socialentry as $network => $candidate) {
$connector = null;
$valid = false;

// get profile-id
switch (strtolower($network)) {
case "facebook":
$candidate = basename($candidate);
if (!ctype_digit($candidate)) {
// TODO: determine facebook profile id from username
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
break;
}
$valid = true;
break;
}
if ($valid) {
// build connector
switch (strtolower($network)) {
case "facebook":
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
switch ($type) {
case "avatar":
$connector = "https://graph.facebook.com/" . ($candidate) . "/picture?width=720";
break;
default:
break;
}
break;
}

// return first valid connector:
if ($connector) { return ($connector); }
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
}
}

// reached only if no valid connectors found
return null;
}

/**
* @NoAdminRequired
*
* Retrieves social profile data for a contact
*/
public function fetch(string $addressbookId, string $contactId, string $type) {

$url = null;
$response = 404;

try {
// get social parameters from contact
$contact = $this->manager->search($contactId, array('UID'))[0];
$socialprofile = $contact['X-SOCIALPROFILE'];
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved

// retrieve data
try {
$url = $this->getSocialConnector($socialprofile, $type);
}
catch (Exception $e) {
$response = 500;
throw new Exception($e->getMessage()); // TODO: implement better error handling
}

if (empty($url)) {
$response = 500;
throw new Exception('not implemented');
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
}

$host = parse_url($url);
if (!$host) {
$response = 404;
throw new Exception('Could not parse URL');
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
}
$opts = [
"http" => [
"method" => "GET",
"header" => "User-Agent: Nextcloud Contacts App"
]
];
$context = stream_context_create($opts);
$socialdata = file_get_contents($url, false, $context);
if (!$socialdata) {
$response = 404;
throw new Exception('Could not parse URL');
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
}

$response = 200;
switch ($type) {
case "avatar":
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
header("Content-type:image/png");
break;
skjnldsv marked this conversation as resolved.
Show resolved Hide resolved
default:
header("Content-type:application/json");
}

echo $socialdata;
}
catch (Exception $e) {
}

http_response_code($response);
exit;

call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
// TODO: instead of returning the image, modify the contact directly

}
}
49 changes: 48 additions & 1 deletion src/components/ContactDetails/ContactDetailsAvatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<div class="contact-header-avatar">
<div class="contact-header-avatar__wrapper">
<div class="contact-header-avatar__background" @click="toggleModal" />

<div v-if="contact.photo"
:style="{ 'backgroundImage': `url(${contact.photoUrl})` }"
class="contact-header-avatar__photo"
Expand Down Expand Up @@ -80,6 +81,9 @@
<ActionButton v-if="!isReadOnly" icon="icon-picture" @click="selectFilePicker">
{{ t('contacts', 'Choose from files') }}
</ActionButton>
<ActionButton v-if="!isReadOnly && hasSocialId" icon="icon-link" @click="selectSocialAvatar">
{{ t('contacts', 'Update from social media') }}
</ActionButton>
</Actions>
</div>
</div>
Expand All @@ -93,7 +97,7 @@ import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import Modal from '@nextcloud/vue/dist/Components/Modal'

import { getFilePickerBuilder } from '@nextcloud/dialogs'
import { generateRemoteUrl } from '@nextcloud/router'
import { generateUrl, generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import sanitizeSVG from '@mattkrick/sanitize-svg'

Expand Down Expand Up @@ -124,6 +128,7 @@ export default {
root: generateRemoteUrl(`dav/files/${getCurrentUser().uid}`),
width: 0,
height: 0,
facebookid: 0,
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
}
},
computed: {
Expand All @@ -133,6 +138,12 @@ export default {
}
return false
},
hasSocialId() {
const jCal = this.contact.jCal
const socialId = jCal[1].filter(props => props[0] === 'x-socialprofile')
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
if (socialId.length > 0) { return true }
return false
},
},
mounted() {
// update image size on window resize
Expand Down Expand Up @@ -330,6 +341,42 @@ export default {
}
},

/**
* WebImage handlers
*/
async selectSocialAvatar() {
const apiUrl = generateUrl('apps/contacts/api/v1/social/avatar/')
const addressbookId = this.contact.addressbook.id
const contactId = this.contact.uid
const imageUrl = apiUrl + addressbookId + '/' + contactId

console.debug('contact start')
console.debug(this.contact)
console.debug('contact end')

if (!this.loading) {

this.loading = true
try {
const { get } = await axios()
call-me-matt marked this conversation as resolved.
Show resolved Hide resolved
const response = await get(`${imageUrl}`, {
responseType: 'arraybuffer',
})
const type = response.headers['content-type']
if (response.status !== 200) throw new URIError('verify social profile id')
const data = Buffer.from(response.data, 'binary').toString('base64')
this.setPhoto(data, type)
} catch (error) {
OC.Notification.showTemporary(t('contacts', 'Error while processing the picture.'))
console.error(error)
this.loading = false
}

} else {
OC.Notification.showTemporary(t('contacts', 'Social avatar download failed'))
}
},

/**
* Menu handling
*/
Expand Down