const FOLLOWER = 1;
const SHARING = 2;
const FRIEND = 3;
+ const SELF = 4;
/**
* @}
*/
* @param array $fields field array
* @param int $duplicate_mode Do an update on a duplicate entry
*
- * @return boolean was the insert successful?
+ * @return int id of the created contact
* @throws \Exception
*/
public static function insert(array $fields, int $duplicate_mode = Database::INSERT_DEFAULT)
$fields['created'] = DateTimeFormat::utcNow();
}
- $ret = DBA::insert('contact', $fields, $duplicate_mode);
- $contact = DBA::selectFirst('contact', ['nurl', 'uid'], ['id' => DBA::lastInsertId()]);
+ DBA::insert('contact', $fields, $duplicate_mode);
+ $contact = DBA::selectFirst('contact', [], ['id' => DBA::lastInsertId()]);
if (!DBA::isResult($contact)) {
// Shouldn't happen
- return $ret;
+ Logger::warning('Created contact could not be found', ['fields' => $fields]);
+ return 0;
}
+ Contact\User::insertForContactArray($contact);
+
// Search for duplicated contacts and get rid of them
- self::removeDuplicates($contact['nurl'], $contact['uid']);
+ if (!$contact['self']) {
+ self::removeDuplicates($contact['nurl'], $contact['uid']);
+ }
+
+ return $contact['id'];
+ }
+
+ /**
+ * Updates rows in the contact table
+ *
+ * @param array $fields contains the fields that are updated
+ * @param array $condition condition array with the key values
+ * @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate, false = don't update identical fields)
+ *
+ * @return boolean was the update successfull?
+ * @throws \Exception
+ */
+ public static function update(array $fields, array $condition, $old_fields = [])
+ {
+ $ret = DBA::update('contact', $fields, $condition, $old_fields);
+
+ // Apply changes to the "user-contact" table on dedicated fields
+ Contact\User::updateByContactUpdate($fields, $condition);
return $ret;
}
'nick' => $user['nickname'],
'pubkey' => $user['pubkey'],
'prvkey' => $user['prvkey'],
- 'photo' => User::getAvatarUrlForId($user['uid']),
- 'thumb' => User::getAvatarUrlForId($user['uid'], Proxy::SIZE_THUMB),
- 'micro' => User::getAvatarUrlForId($user['uid'], Proxy::SIZE_MICRO),
+ 'photo' => User::getAvatarUrl($user),
+ 'thumb' => User::getAvatarUrl($user, Proxy::SIZE_THUMB),
+ 'micro' => User::getAvatarUrl($user, Proxy::SIZE_MICRO),
'blocked' => 0,
'pending' => 0,
'url' => DI::baseUrl() . '/profile/' . $user['nickname'],
// Only create the entry if it doesn't exist yet
if (!DBA::exists('contact', ['uid' => $uid, 'self' => true])) {
- $return = DBA::insert('contact', $contact);
+ $return = (bool)self::insert($contact);
}
// Create the public contact
$contact['uid'] = 0;
$contact['prvkey'] = null;
- DBA::insert('contact', $contact, Database::INSERT_IGNORE);
+ self::insert($contact, Database::INSERT_IGNORE);
}
return $return;
return false;
}
- $fields = ['nickname', 'page-flags', 'account-type', 'prvkey', 'pubkey'];
+ $fields = ['uid', 'nickname', 'page-flags', 'account-type', 'prvkey', 'pubkey'];
$user = DBA::selectFirst('user', $fields, ['uid' => $uid, 'account_expired' => false]);
if (!DBA::isResult($user)) {
return false;
$fields['micro'] = self::getDefaultAvatar($fields, Proxy::SIZE_MICRO);
}
- $fields['avatar'] = User::getAvatarUrlForId($uid);
+ $fields['avatar'] = User::getAvatarUrl($user);
$fields['forum'] = $user['page-flags'] == User::PAGE_FLAGS_COMMUNITY;
$fields['prv'] = $user['page-flags'] == User::PAGE_FLAGS_PRVGROUP;
$fields['unsearchable'] = !$profile['net-publish'];
$fields['name-date'] = DateTimeFormat::utcNow();
}
$fields['updated'] = DateTimeFormat::utcNow();
- DBA::update('contact', $fields, ['id' => $self['id']]);
+ self::update($fields, ['id' => $self['id']]);
// Update the public contact as well
$fields['prvkey'] = null;
$fields['self'] = false;
- DBA::update('contact', $fields, ['uid' => 0, 'nurl' => $self['nurl']]);
+ self::update($fields, ['uid' => 0, 'nurl' => $self['nurl']]);
// Update the profile
$fields = [
- 'photo' => User::getAvatarUrlForId($uid),
- 'thumb' => User::getAvatarUrlForId($uid, Proxy::SIZE_THUMB)
+ 'photo' => User::getAvatarUrl($user),
+ 'thumb' => User::getAvatarUrl($user, Proxy::SIZE_THUMB)
];
DBA::update('profile', $fields, ['uid' => $uid]);
* Marks a contact for removal
*
* @param int $id contact id
- * @return null
* @throws HTTPException\InternalServerErrorException
*/
public static function remove($id)
}
// Archive the contact
- DBA::update('contact', ['archive' => true, 'network' => Protocol::PHANTOM, 'deleted' => true], ['id' => $id]);
+ self::update(['archive' => true, 'network' => Protocol::PHANTOM, 'deleted' => true], ['id' => $id]);
// Delete it in the background
Worker::add(PRIORITY_MEDIUM, 'RemoveContact', $id);
}
/**
- * Sends an unfriend message. Does not remove the contact
+ * Sends an unfriend message. Removes the contact for two-way unfriending or sharing only protocols (feed an mail)
+ *
+ * @param array $user User unfriending
+ * @param array $contact Contact (uid != 0) unfriended
+ * @param boolean $two_way Revoke eventual inbound follow as well
+ * @return bool|null true if successful, false if not, null if no action was performed
+ * @throws HTTPException\InternalServerErrorException
+ * @throws \ImagickException
+ */
+ public static function terminateFriendship(array $user, array $contact): bool
+ {
+ $result = Protocol::terminateFriendship($user, $contact);
+
+ if ($contact['rel'] == Contact::SHARING || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) {
+ self::remove($contact['id']);
+ } else {
+ self::update(['rel' => Contact::FOLLOWER], ['id' => $contact['id']]);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Revoke follow privileges of the remote user contact
*
- * @param array $user User unfriending
* @param array $contact Contact unfriended
- * @param boolean $dissolve Remove the contact on the remote side
- * @return void
+ * @return bool|null Whether the remote operation is successful or null if no remote operation was performed
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
- public static function terminateFriendship(array $user, array $contact, $dissolve = false)
+ public static function revokeFollow(array $contact): bool
{
if (empty($contact['network'])) {
- return;
+ throw new \InvalidArgumentException('Empty network in contact array');
}
- $protocol = $contact['network'];
- if (($protocol == Protocol::DFRN) && !empty($contact['protocol'])) {
- $protocol = $contact['protocol'];
+ if (empty($contact['uid'])) {
+ throw new \InvalidArgumentException('Unexpected public contact record');
}
- if (in_array($protocol, [Protocol::OSTATUS, Protocol::DFRN])) {
- // create an unfollow slap
- $item = [];
- $item['verb'] = Activity::O_UNFOLLOW;
- $item['gravity'] = GRAVITY_ACTIVITY;
- $item['follow'] = $contact["url"];
- $item['body'] = '';
- $item['title'] = '';
- $item['guid'] = '';
- $item['uri-id'] = 0;
- $slap = OStatus::salmon($item, $user);
-
- if (!empty($contact['notify'])) {
- Salmon::slapper($user, $contact['notify'], $slap);
- }
- } elseif ($protocol == Protocol::DIASPORA) {
- Diaspora::sendUnshare($user, $contact);
- } elseif ($protocol == Protocol::ACTIVITYPUB) {
- ActivityPub\Transmitter::sendContactUndo($contact['url'], $contact['id'], $user['uid']);
+ $result = Protocol::revokeFollow($contact);
- if ($dissolve) {
- ActivityPub\Transmitter::sendContactReject($contact['url'], $contact['hub-verify'], $user['uid']);
+ // A null value here means the remote network doesn't support explicit follow revocation, we can still
+ // break the locally recorded relationship
+ if ($result !== false) {
+ if ($contact['rel'] == self::FRIEND) {
+ self::update(['rel' => self::SHARING], ['id' => $contact['id']]);
+ } else {
+ self::remove($contact['id']);
}
}
+
+ return $result;
}
+
/**
* Marks a contact for archival after a communication issue delay
*
}
if ($contact['term-date'] <= DBA::NULL_DATETIME) {
- DBA::update('contact', ['term-date' => DateTimeFormat::utcNow()], ['id' => $contact['id']]);
- DBA::update('contact', ['term-date' => DateTimeFormat::utcNow()], ['`nurl` = ? AND `term-date` <= ? AND NOT `self`', Strings::normaliseLink($contact['url']), DBA::NULL_DATETIME]);
+ self::update(['term-date' => DateTimeFormat::utcNow()], ['id' => $contact['id']]);
+ self::update(['term-date' => DateTimeFormat::utcNow()], ['`nurl` = ? AND `term-date` <= ? AND NOT `self`', Strings::normaliseLink($contact['url']), DBA::NULL_DATETIME]);
} else {
/* @todo
* We really should send a notification to the owner after 2-3 weeks
* delete, though if the owner tries to unarchive them we'll start
* the whole process over again.
*/
- DBA::update('contact', ['archive' => true], ['id' => $contact['id']]);
- DBA::update('contact', ['archive' => true], ['nurl' => Strings::normaliseLink($contact['url']), 'self' => false]);
+ self::update(['archive' => true], ['id' => $contact['id']]);
+ self::update(['archive' => true], ['nurl' => Strings::normaliseLink($contact['url']), 'self' => false]);
}
}
}
$fields = ['failed' => false, 'term-date' => DBA::NULL_DATETIME, 'archive' => false];
$condition = ['uid' => 0, 'network' => Protocol::FEDERATED, 'batch' => $contact['batch'], 'contact-type' => self::TYPE_RELAY];
if (!DBA::exists('contact', array_merge($condition, $fields))) {
- DBA::update('contact', $fields, $condition);
+ self::update($fields, $condition);
}
}
// It's a miracle. Our dead contact has inexplicably come back to life.
$fields = ['failed' => false, 'term-date' => DBA::NULL_DATETIME, 'archive' => false];
- DBA::update('contact', $fields, ['id' => $contact['id']]);
- DBA::update('contact', $fields, ['nurl' => Strings::normaliseLink($contact['url']), 'self' => false]);
+ self::update($fields, ['id' => $contact['id']]);
+ self::update($fields, ['nurl' => Strings::normaliseLink($contact['url']), 'self' => false]);
}
/**
$pm_url = '';
$status_link = '';
$photos_link = '';
- $contact_drop_link = '';
$poke_link = '';
if ($uid == 0) {
$posts_link = DI::baseUrl() . '/contact/' . $contact['id'] . '/conversations';
- if (!$contact['self']) {
- $contact_drop_link = DI::baseUrl() . '/contact/' . $contact['id'] . '/drop?confirm=1';
- }
-
$follow_link = '';
$unfollow_link = '';
- if (!$contact['self'] && in_array($contact['network'], Protocol::NATIVE_SUPPORT)) {
+ if (!$contact['self'] && Protocol::supportsFollow($contact['network'])) {
if ($contact['uid'] && in_array($contact['rel'], [self::SHARING, self::FRIEND])) {
$unfollow_link = 'unfollow?url=' . urlencode($contact['url']) . '&auto=1';
} elseif(!$contact['pending']) {
}
}
- if (!empty($follow_link) || !empty($unfollow_link)) {
- $contact_drop_link = '';
- }
-
/**
* Menu array:
* "name" => [ "Label", "link", (bool)Should the link opened in a new tab? ]
'photos' => [DI::l10n()->t('View Photos') , $photos_link , true],
'network' => [DI::l10n()->t('Network Posts') , $posts_link , false],
'edit' => [DI::l10n()->t('View Contact') , $contact_url , false],
- 'drop' => [DI::l10n()->t('Drop Contact') , $contact_drop_link, false],
'pm' => [DI::l10n()->t('Send PM') , $pm_url , false],
'poke' => [DI::l10n()->t('Poke') , $poke_link , false],
'follow' => [DI::l10n()->t('Connect/Follow'), $follow_link , true],
$contact_id = $contact['id'];
Logger::notice('Contact had been created (shortly) before', ['id' => $contact_id, 'url' => $url, 'uid' => $uid]);
} else {
- DBA::insert('contact', $fields);
- $contact_id = DBA::lastInsertId();
+ $contact_id = self::insert($fields);
if ($contact_id) {
Logger::info('Contact inserted', ['id' => $contact_id, 'url' => $url, 'uid' => $uid]);
}
* @param bool $thread_mode
* @param int $update Update mode
* @param int $parent Item parent ID for the update mode
+ * @param bool $only_media Only display media content
* @return string posts in HTML
* @throws \Exception
*/
- public static function getPostsFromUrl($contact_url, $thread_mode = false, $update = 0, $parent = 0)
+ public static function getPostsFromUrl($contact_url, $thread_mode = false, $update = 0, $parent = 0, bool $only_media = false)
{
- return self::getPostsFromId(self::getIdForURL($contact_url), $thread_mode, $update, $parent);
+ return self::getPostsFromId(self::getIdForURL($contact_url), $thread_mode, $update, $parent, $only_media);
}
/**
* @param int $cid Contact ID
* @param bool $thread_mode
* @param int $update Update mode
- * @param int $parent Item parent ID for the update mode
+ * @param int $parent Item parent ID for the update mode
+ * @param bool $only_media Only display media content
* @return string posts in HTML
* @throws \Exception
*/
- public static function getPostsFromId($cid, $thread_mode = false, $update = 0, $parent = 0)
+ public static function getPostsFromId($cid, $thread_mode = false, $update = 0, $parent = 0, bool $only_media = false)
{
- $a = DI::app();
-
$contact = DBA::selectFirst('contact', ['contact-type', 'network'], ['id' => $cid]);
if (!DBA::isResult($contact)) {
return '';
}
}
+ if ($only_media) {
+ $condition = DBA::mergeConditions($condition, ["`uri-id` IN (SELECT `uri-id` FROM `post-media` WHERE `type` IN (?, ?, ?))",
+ Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO]);
+ }
+
if (DI::mode()->isMobile()) {
$itemsPerPage = DI::pConfig()->get(local_user(), 'system', 'itemspage_mobile_network',
DI::config()->get('system', 'itemspage_network_mobile'));
*/
public static function block($cid, $reason = null)
{
- $return = DBA::update('contact', ['blocked' => true, 'block_reason' => $reason], ['id' => $cid]);
+ $return = self::update(['blocked' => true, 'block_reason' => $reason], ['id' => $cid]);
return $return;
}
*/
public static function unblock($cid)
{
- $return = DBA::update('contact', ['blocked' => false, 'block_reason' => null], ['id' => $cid]);
+ $return = self::update(['blocked' => false, 'block_reason' => null], ['id' => $cid]);
return $return;
}
// Only update the cached photo links of public contacts when they already are cached
if (($uid == 0) && !$force && empty($contact['thumb']) && empty($contact['micro']) && !$create_cache) {
if ($contact['avatar'] != $avatar) {
- DBA::update('contact', ['avatar' => $avatar], ['id' => $cid]);
+ self::update(['avatar' => $avatar], ['id' => $cid]);
Logger::info('Only update the avatar', ['id' => $cid, 'avatar' => $avatar, 'contact' => $contact]);
}
return;
$cids[] = $cid;
$uids[] = $uid;
Logger::info('Updating cached contact avatars', ['cid' => $cids, 'uid' => $uids, 'fields' => $fields]);
- DBA::update('contact', $fields, ['id' => $cids]);
+ self::update($fields, ['id' => $cids]);
}
public static function deleteContactByUrl(string $url)
*/
private static function updateContact(int $id, int $uid, string $old_url, string $new_url, array $fields)
{
- if (!DBA::update('contact', $fields, ['id' => $id])) {
+ if (!self::update($fields, ['id' => $id])) {
Logger::info('Couldn\'t update contact.', ['id' => $id, 'fields' => $fields]);
return;
}
$condition = ['self' => false, 'nurl' => Strings::normaliseLink($old_url)];
$condition['network'] = [Protocol::DFRN, Protocol::DIASPORA, Protocol::ACTIVITYPUB];
- DBA::update('contact', $fields, $condition);
+ self::update($fields, $condition);
// We mustn't set the update fields for OStatus contacts since they are updated in OnePoll
$condition['network'] = Protocol::OSTATUS;
return;
}
- DBA::update('contact', $fields, $condition);
+ self::update($fields, $condition);
}
/**
*/
public static function removeDuplicates(string $nurl, int $uid)
{
- $condition = ['nurl' => $nurl, 'uid' => $uid, 'deleted' => false, 'network' => Protocol::FEDERATED];
+ $condition = ['nurl' => $nurl, 'uid' => $uid, 'self' => false, 'deleted' => false, 'network' => Protocol::FEDERATED];
$count = DBA::count('contact', $condition);
if ($count <= 1) {
return false;
}
}
if (!empty($fields)) {
- DBA::update('contact', $fields, ['id' => $id, 'self' => false]);
+ self::update($fields, ['id' => $id, 'self' => false]);
Logger::info('Updating local contact', ['id' => $id]);
}
}
$new_relation = (($contact['rel'] == self::FOLLOWER) ? self::FRIEND : self::SHARING);
$fields = ['rel' => $new_relation, 'subhub' => $subhub, 'readonly' => false];
- DBA::update('contact', $fields, ['id' => $contact['id']]);
+ self::update($fields, ['id' => $contact['id']]);
} else {
$new_relation = (in_array($protocol, [Protocol::MAIL]) ? self::FRIEND : self::SHARING);
$fields = ['url' => $contact['url'], 'request' => $contact['request'],
'notify' => $contact['notify'], 'poll' => $contact['poll'],
'confirm' => $contact['confirm'], 'poco' => $contact['poco']];
- DBA::update('contact', $fields, ['id' => $contact['id']]);
+ self::update($fields, ['id' => $contact['id']]);
}
return $contact;
if (($contact['rel'] == self::SHARING)
|| ($sharing && $contact['rel'] == self::FOLLOWER)) {
- DBA::update('contact', ['rel' => self::FRIEND, 'writable' => true, 'pending' => false],
+ self::update(['rel' => self::FRIEND, 'writable' => true, 'pending' => false],
['id' => $contact['id'], 'uid' => $importer['uid']]);
}
// Ensure to always have the correct network type, independent from the connection request method
self::updateFromProbe($contact['id']);
- Post\UserNotification::insertNotication($contact['id'], Verb::getID(Activity::FOLLOW), $importer['uid']);
+ Post\UserNotification::insertNotification($contact['id'], Activity::FOLLOW, $importer['uid']);
return true;
} else {
}
// create contact record
- DBA::insert('contact', [
+ $contact_id = self::insert([
'uid' => $importer['uid'],
'created' => DateTimeFormat::utcNow(),
'url' => $url,
'writable' => 1,
]);
- $contact_id = DBA::lastInsertId();
-
// Ensure to always have the correct network type, independent from the connection request method
self::updateFromProbe($contact_id);
self::updateAvatar($contact_id, $photo, true);
- Post\UserNotification::insertNotication($contact_id, Verb::getID(Activity::FOLLOW), $importer['uid']);
+ Post\UserNotification::insertNotification($contact_id, Activity::FOLLOW, $importer['uid']);
$contact_record = DBA::selectFirst('contact', ['id', 'network', 'name', 'url', 'photo'], ['id' => $contact_id]);
$fields['rel'] = self::FRIEND;
}
- DBA::update('contact', $fields, $condition);
+ self::update($fields, $condition);
return true;
}
public static function removeSharer($importer, $contact)
{
if (($contact['rel'] == self::FRIEND) || ($contact['rel'] == self::FOLLOWER)) {
- DBA::update('contact', ['rel' => self::FOLLOWER], ['id' => $contact['id']]);
+ self::update(['rel' => self::FOLLOWER], ['id' => $contact['id']]);
} else {
self::remove($contact['id']);
}