Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

659 lines
23 KiB

11 months ago
11 months ago
  1. <?php
  2. /**
  3. * @copyright Copyright (C) 2020, Friendica
  4. *
  5. * @license GNU AGPL version 3 or any later version
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Affero General Public License as
  9. * published by the Free Software Foundation, either version 3 of the
  10. * License, or (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Affero General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public License
  18. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. *
  20. */
  21. namespace Friendica\Model\Contact;
  22. use Exception;
  23. use Friendica\Core\Logger;
  24. use Friendica\Core\Protocol;
  25. use Friendica\Database\Database;
  26. use Friendica\Database\DBA;
  27. use Friendica\DI;
  28. use Friendica\Model\APContact;
  29. use Friendica\Model\Contact;
  30. use Friendica\Model\Profile;
  31. use Friendica\Model\User;
  32. use Friendica\Protocol\ActivityPub;
  33. use Friendica\Util\DateTimeFormat;
  34. use Friendica\Util\Strings;
  35. /**
  36. * This class provides relationship information based on the `contact-relation` table.
  37. * This table is directional (cid = source, relation-cid = target), references public contacts (with uid=0) and records both
  38. * follows and the last interaction (likes/comments) on public posts.
  39. */
  40. class Relation
  41. {
  42. /**
  43. * No discovery of followers/followings
  44. */
  45. const DISCOVERY_NONE = 0;
  46. /**
  47. * Discover followers/followings of local contacts
  48. */
  49. const DISCOVERY_LOCAL = 1;
  50. /**
  51. * Discover followers/followings of local contacts and contacts that visibly interacted on the system
  52. */
  53. const DISCOVERY_INTERACTOR = 2;
  54. /**
  55. * Discover followers/followings of all contacts
  56. */
  57. const DISCOVERY_ALL = 3;
  58. public static function store(int $target, int $actor, string $interaction_date)
  59. {
  60. if ($actor == $target) {
  61. return;
  62. }
  63. DBA::insert('contact-relation', ['last-interaction' => $interaction_date, 'cid' => $target, 'relation-cid' => $actor], Database::INSERT_UPDATE);
  64. }
  65. /**
  66. * Fetches the followers of a given profile and adds them
  67. *
  68. * @param string $url URL of a profile
  69. * @return void
  70. */
  71. public static function discoverByUrl(string $url)
  72. {
  73. $contact = Contact::getByURL($url);
  74. if (empty($contact)) {
  75. return;
  76. }
  77. if (!self::isDiscoverable($url, $contact)) {
  78. return;
  79. }
  80. $uid = User::getIdForURL($url);
  81. if (!empty($uid)) {
  82. // Fetch the followers/followings locally
  83. $followers = self::getContacts($uid, [Contact::FOLLOWER, Contact::FRIEND]);
  84. $followings = self::getContacts($uid, [Contact::SHARING, Contact::FRIEND]);
  85. } else {
  86. $apcontact = APContact::getByURL($url, false);
  87. if (!empty($apcontact['followers']) && is_string($apcontact['followers'])) {
  88. $followers = ActivityPub::fetchItems($apcontact['followers']);
  89. } else {
  90. $followers = [];
  91. }
  92. if (!empty($apcontact['following']) && is_string($apcontact['following'])) {
  93. $followings = ActivityPub::fetchItems($apcontact['following']);
  94. } else {
  95. $followings = [];
  96. }
  97. }
  98. if (empty($followers) && empty($followings)) {
  99. DBA::update('contact', ['last-discovery' => DateTimeFormat::utcNow()], ['id' => $contact['id']]);
  100. Logger::info('The contact does not offer discoverable data', ['id' => $contact['id'], 'url' => $url, 'network' => $contact['network']]);
  101. return;
  102. }
  103. $target = $contact['id'];
  104. if (!empty($followers)) {
  105. // Clear the follower list, since it will be recreated in the next step
  106. DBA::update('contact-relation', ['follows' => false], ['cid' => $target]);
  107. }
  108. $contacts = [];
  109. foreach (array_merge($followers, $followings) as $contact) {
  110. if (is_string($contact)) {
  111. $contacts[] = $contact;
  112. } elseif (!empty($contact['url']) && is_string($contact['url'])) {
  113. $contacts[] = $contact['url'];
  114. }
  115. }
  116. $contacts = array_unique($contacts);
  117. $follower_counter = 0;
  118. $following_counter = 0;
  119. Logger::info('Discover contacts', ['id' => $target, 'url' => $url, 'contacts' => count($contacts)]);
  120. foreach ($contacts as $contact) {
  121. $actor = Contact::getIdForURL($contact);
  122. if (!empty($actor)) {
  123. if (in_array($contact, $followers)) {
  124. $condition = ['cid' => $target, 'relation-cid' => $actor];
  125. DBA::insert('contact-relation', $condition, Database::INSERT_IGNORE);
  126. DBA::update('contact-relation', ['follows' => true, 'follow-updated' => DateTimeFormat::utcNow()], $condition);
  127. $follower_counter++;
  128. }
  129. if (in_array($contact, $followings)) {
  130. $condition = ['cid' => $actor, 'relation-cid' => $target];
  131. DBA::insert('contact-relation', $condition, Database::INSERT_IGNORE);
  132. DBA::update('contact-relation', ['follows' => true, 'follow-updated' => DateTimeFormat::utcNow()], $condition);
  133. $following_counter++;
  134. }
  135. }
  136. }
  137. if (!empty($followers)) {
  138. // Delete all followers that aren't followers anymore (and aren't interacting)
  139. DBA::delete('contact-relation', ['cid' => $target, 'follows' => false, 'last-interaction' => DBA::NULL_DATETIME]);
  140. }
  141. DBA::update('contact', ['last-discovery' => DateTimeFormat::utcNow()], ['id' => $target]);
  142. Logger::info('Contacts discovery finished', ['id' => $target, 'url' => $url, 'follower' => $follower_counter, 'following' => $following_counter]);
  143. return;
  144. }
  145. /**
  146. * Fetch contact url list from the given local user
  147. *
  148. * @param integer $uid
  149. * @param array $rel
  150. * @return array contact list
  151. */
  152. private static function getContacts(int $uid, array $rel)
  153. {
  154. $list = [];
  155. $profile = Profile::getByUID($uid);
  156. if (!empty($profile['hide-friends'])) {
  157. return $list;
  158. }
  159. $condition = ['rel' => $rel, 'uid' => $uid, 'self' => false, 'deleted' => false,
  160. 'hidden' => false, 'archive' => false, 'pending' => false];
  161. $condition = DBA::mergeConditions($condition, ["`url` IN (SELECT `url` FROM `apcontact`)"]);
  162. $contacts = DBA::select('contact', ['url'], $condition);
  163. while ($contact = DBA::fetch($contacts)) {
  164. $list[] = $contact['url'];
  165. }
  166. DBA::close($contacts);
  167. return $list;
  168. }
  169. /**
  170. * Tests if a given contact url is discoverable
  171. *
  172. * @param string $url Contact url
  173. * @param array $contact Contact array
  174. * @return boolean True if contact is discoverable
  175. */
  176. public static function isDiscoverable(string $url, array $contact = [])
  177. {
  178. $contact_discovery = DI::config()->get('system', 'contact_discovery');
  179. if ($contact_discovery == self::DISCOVERY_NONE) {
  180. return false;
  181. }
  182. if (empty($contact)) {
  183. $contact = Contact::getByURL($url, false);
  184. }
  185. if (empty($contact)) {
  186. return false;
  187. }
  188. if ($contact['last-discovery'] > DateTimeFormat::utc('now - 1 month')) {
  189. Logger::info('No discovery - Last was less than a month ago.', ['id' => $contact['id'], 'url' => $url, 'discovery' => $contact['last-discovery']]);
  190. return false;
  191. }
  192. if ($contact_discovery != self::DISCOVERY_ALL) {
  193. $local = DBA::exists('contact', ["`nurl` = ? AND `uid` != ?", Strings::normaliseLink($url), 0]);
  194. if (($contact_discovery == self::DISCOVERY_LOCAL) && !$local) {
  195. Logger::info('No discovery - This contact is not followed/following locally.', ['id' => $contact['id'], 'url' => $url]);
  196. return false;
  197. }
  198. if ($contact_discovery == self::DISCOVERY_INTERACTOR) {
  199. $interactor = DBA::exists('contact-relation', ["`relation-cid` = ? AND `last-interaction` > ?", $contact['id'], DBA::NULL_DATETIME]);
  200. if (!$local && !$interactor) {
  201. Logger::info('No discovery - This contact is not interacting locally.', ['id' => $contact['id'], 'url' => $url]);
  202. return false;
  203. }
  204. }
  205. } elseif ($contact['created'] > DateTimeFormat::utc('now - 1 day')) {
  206. // Newly created contacts are not discovered to avoid DDoS attacks
  207. Logger::info('No discovery - Contact record is less than a day old.', ['id' => $contact['id'], 'url' => $url, 'discovery' => $contact['created']]);
  208. return false;
  209. }
  210. if (!in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS])) {
  211. $apcontact = APContact::getByURL($url, false);
  212. if (empty($apcontact)) {
  213. Logger::info('No discovery - The contact does not seem to speak ActivityPub.', ['id' => $contact['id'], 'url' => $url, 'network' => $contact['network']]);
  214. return false;
  215. }
  216. }
  217. return true;
  218. }
  219. /**
  220. * @param int $uid user
  221. * @param int $start optional, default 0
  222. * @param int $limit optional, default 80
  223. * @return array
  224. */
  225. static public function getSuggestions(int $uid, int $start = 0, int $limit = 80)
  226. {
  227. $cid = Contact::getPublicIdByUserId($uid);
  228. $totallimit = $start + $limit;
  229. $contacts = [];
  230. Logger::info('Collecting suggestions', ['uid' => $uid, 'cid' => $cid, 'start' => $start, 'limit' => $limit]);
  231. $diaspora = DI::config()->get('system', 'diaspora_enabled') ? Protocol::DIASPORA : Protocol::ACTIVITYPUB;
  232. $ostatus = !DI::config()->get('system', 'ostatus_disabled') ? Protocol::OSTATUS : Protocol::ACTIVITYPUB;
  233. // The query returns contacts where contacts interacted with whom the given user follows.
  234. // Contacts who already are in the user's contact table are ignored.
  235. $results = DBA::select('contact', [],
  236. ["`id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` IN
  237. (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ?)
  238. AND NOT `cid` IN (SELECT `id` FROM `contact` WHERE `uid` = ? AND `nurl` IN
  239. (SELECT `nurl` FROM `contact` WHERE `uid` = ? AND `rel` IN (?, ?))))
  240. AND NOT `hidden` AND `network` IN (?, ?, ?, ?)",
  241. $cid, 0, $uid, Contact::FRIEND, Contact::SHARING,
  242. Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $ostatus],
  243. ['order' => ['last-item' => true], 'limit' => $totallimit]
  244. );
  245. while ($contact = DBA::fetch($results)) {
  246. $contacts[$contact['id']] = $contact;
  247. }
  248. DBA::close($results);
  249. Logger::info('Contacts of contacts who are followed by the given user', ['uid' => $uid, 'cid' => $cid, 'count' => count($contacts)]);
  250. if (count($contacts) >= $totallimit) {
  251. return array_slice($contacts, $start, $limit);
  252. }
  253. // The query returns contacts where contacts interacted with whom also interacted with the given user.
  254. // Contacts who already are in the user's contact table are ignored.
  255. $results = DBA::select('contact', [],
  256. ["`id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` IN
  257. (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ?)
  258. AND NOT `cid` IN (SELECT `id` FROM `contact` WHERE `uid` = ? AND `nurl` IN
  259. (SELECT `nurl` FROM `contact` WHERE `uid` = ? AND `rel` IN (?, ?))))
  260. AND NOT `hidden` AND `network` IN (?, ?, ?, ?)",
  261. $cid, 0, $uid, Contact::FRIEND, Contact::SHARING,
  262. Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $ostatus],
  263. ['order' => ['last-item' => true], 'limit' => $totallimit]
  264. );
  265. while ($contact = DBA::fetch($results)) {
  266. $contacts[$contact['id']] = $contact;
  267. }
  268. DBA::close($results);
  269. Logger::info('Contacts of contacts who are following the given user', ['uid' => $uid, 'cid' => $cid, 'count' => count($contacts)]);
  270. if (count($contacts) >= $totallimit) {
  271. return array_slice($contacts, $start, $limit);
  272. }
  273. // The query returns contacts that follow the given user but aren't followed by that user.
  274. $results = DBA::select('contact', [],
  275. ["`nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` = ? AND `rel` = ?)
  276. AND NOT `hidden` AND `uid` = ? AND `network` IN (?, ?, ?, ?)",
  277. $uid, Contact::FOLLOWER, 0,
  278. Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $ostatus],
  279. ['order' => ['last-item' => true], 'limit' => $totallimit]
  280. );
  281. while ($contact = DBA::fetch($results)) {
  282. $contacts[$contact['id']] = $contact;
  283. }
  284. DBA::close($results);
  285. Logger::info('Followers that are not followed by the given user', ['uid' => $uid, 'cid' => $cid, 'count' => count($contacts)]);
  286. if (count($contacts) >= $totallimit) {
  287. return array_slice($contacts, $start, $limit);
  288. }
  289. // The query returns any contact that isn't followed by that user.
  290. $results = DBA::select('contact', [],
  291. ["NOT `nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` = ? AND `rel` IN (?, ?))
  292. AND NOT `hidden` AND `uid` = ? AND `network` IN (?, ?, ?, ?)",
  293. $uid, Contact::FRIEND, Contact::SHARING, 0,
  294. Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $ostatus],
  295. ['order' => ['last-item' => true], 'limit' => $totallimit]
  296. );
  297. while ($contact = DBA::fetch($results)) {
  298. $contacts[$contact['id']] = $contact;
  299. }
  300. DBA::close($results);
  301. Logger::info('Any contact', ['uid' => $uid, 'cid' => $cid, 'count' => count($contacts)]);
  302. return array_slice($contacts, $start, $limit);
  303. }
  304. /**
  305. * Counts all the known follows of the provided public contact
  306. *
  307. * @param int $cid Public contact id
  308. * @param array $condition Additional condition on the contact table
  309. * @return int
  310. * @throws Exception
  311. */
  312. public static function countFollows(int $cid, array $condition = [])
  313. {
  314. $condition = DBA::mergeConditions($condition,
  315. ['`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)',
  316. $cid]
  317. );
  318. return DI::dba()->count('contact', $condition);
  319. }
  320. /**
  321. * Returns a paginated list of contacts that are followed the provided public contact.
  322. *
  323. * @param int $cid Public contact id
  324. * @param array $condition Additional condition on the contact table
  325. * @param int $count
  326. * @param int $offset
  327. * @param bool $shuffle
  328. * @return array
  329. * @throws Exception
  330. */
  331. public static function listFollows(int $cid, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
  332. {
  333. $condition = DBA::mergeConditions($condition,
  334. ['`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)',
  335. $cid]
  336. );
  337. return DI::dba()->selectToArray('contact', [], $condition,
  338. ['limit' => [$offset, $count], 'order' => [$shuffle ? 'RAND()' : 'name']]
  339. );
  340. }
  341. /**
  342. * Counts all the known followers of the provided public contact
  343. *
  344. * @param int $cid Public contact id
  345. * @param array $condition Additional condition on the contact table
  346. * @return int
  347. * @throws Exception
  348. */
  349. public static function countFollowers(int $cid, array $condition = [])
  350. {
  351. $condition = DBA::mergeConditions($condition,
  352. ['`id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`)',
  353. $cid]
  354. );
  355. return DI::dba()->count('contact', $condition);
  356. }
  357. /**
  358. * Returns a paginated list of contacts that follow the provided public contact.
  359. *
  360. * @param int $cid Public contact id
  361. * @param array $condition Additional condition on the contact table
  362. * @param int $count
  363. * @param int $offset
  364. * @param bool $shuffle
  365. * @return array
  366. * @throws Exception
  367. */
  368. public static function listFollowers(int $cid, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
  369. {
  370. $condition = DBA::mergeConditions($condition,
  371. ['`id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`)', $cid]
  372. );
  373. return DI::dba()->selectToArray('contact', [], $condition,
  374. ['limit' => [$offset, $count], 'order' => [$shuffle ? 'RAND()' : 'name']]
  375. );
  376. }
  377. /**
  378. * Counts the number of contacts that are known mutuals with the provided public contact.
  379. *
  380. * @param int $cid Public contact id
  381. * @param array $condition Additional condition array on the contact table
  382. * @return int
  383. * @throws Exception
  384. */
  385. public static function countMutuals(int $cid, array $condition = [])
  386. {
  387. $condition = DBA::mergeConditions($condition,
  388. ['`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)
  389. AND `id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`)',
  390. $cid, $cid]
  391. );
  392. return DI::dba()->count('contact', $condition);
  393. }
  394. /**
  395. * Returns a paginated list of contacts that are known mutuals with the provided public contact.
  396. *
  397. * @param int $cid Public contact id
  398. * @param array $condition Additional condition on the contact table
  399. * @param int $count
  400. * @param int $offset
  401. * @param bool $shuffle
  402. * @return array
  403. * @throws Exception
  404. */
  405. public static function listMutuals(int $cid, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
  406. {
  407. $condition = DBA::mergeConditions($condition,
  408. ['`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)
  409. AND `id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`)',
  410. $cid, $cid]
  411. );
  412. return DI::dba()->selectToArray('contact', [], $condition,
  413. ['limit' => [$offset, $count], 'order' => [$shuffle ? 'RAND()' : 'name']]
  414. );
  415. }
  416. /**
  417. * Counts the number of contacts with any relationship with the provided public contact.
  418. *
  419. * @param int $cid Public contact id
  420. * @param array $condition Additional condition array on the contact table
  421. * @return int
  422. * @throws Exception
  423. */
  424. public static function countAll(int $cid, array $condition = [])
  425. {
  426. $condition = DBA::mergeConditions($condition,
  427. ['(`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)
  428. OR `id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`))',
  429. $cid, $cid]
  430. );
  431. return DI::dba()->count('contact', $condition);
  432. }
  433. /**
  434. * Returns a paginated list of contacts with any relationship with the provided public contact.
  435. *
  436. * @param int $cid Public contact id
  437. * @param array $condition Additional condition on the contact table
  438. * @param int $count
  439. * @param int $offset
  440. * @param bool $shuffle
  441. * @return array
  442. * @throws Exception
  443. */
  444. public static function listAll(int $cid, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
  445. {
  446. $condition = DBA::mergeConditions($condition,
  447. ['(`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)
  448. OR `id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`))',
  449. $cid, $cid]
  450. );
  451. return DI::dba()->selectToArray('contact', [], $condition,
  452. ['limit' => [$offset, $count], 'order' => [$shuffle ? 'RAND()' : 'name']]
  453. );
  454. }
  455. /**
  456. * Counts the number of contacts that both provided public contacts have interacted with at least once.
  457. * Interactions include follows and likes and comments on public posts.
  458. *
  459. * @param int $sourceId Public contact id
  460. * @param int $targetId Public contact id
  461. * @param array $condition Additional condition array on the contact table
  462. * @return int
  463. * @throws Exception
  464. */
  465. public static function countCommon(int $sourceId, int $targetId, array $condition = [])
  466. {
  467. $condition = DBA::mergeConditions($condition,
  468. ['`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ?)
  469. AND `id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ?)',
  470. $sourceId, $targetId]
  471. );
  472. return DI::dba()->count('contact', $condition);
  473. }
  474. /**
  475. * Returns a paginated list of contacts that both provided public contacts have interacted with at least once.
  476. * Interactions include follows and likes and comments on public posts.
  477. *
  478. * @param int $sourceId Public contact id
  479. * @param int $targetId Public contact id
  480. * @param array $condition Additional condition on the contact table
  481. * @param int $count
  482. * @param int $offset
  483. * @param bool $shuffle
  484. * @return array
  485. * @throws Exception
  486. */
  487. public static function listCommon(int $sourceId, int $targetId, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
  488. {
  489. $condition = DBA::mergeConditions($condition,
  490. ["`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ?)
  491. AND `id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ?)",
  492. $sourceId, $targetId]
  493. );
  494. return DI::dba()->selectToArray('contact', [], $condition,
  495. ['limit' => [$offset, $count], 'order' => [$shuffle ? 'RAND()' : 'name']]
  496. );
  497. }
  498. /**
  499. * Counts the number of contacts that are followed by both provided public contacts.
  500. *
  501. * @param int $sourceId Public contact id
  502. * @param int $targetId Public contact id
  503. * @param array $condition Additional condition array on the contact table
  504. * @return int
  505. * @throws Exception
  506. */
  507. public static function countCommonFollows(int $sourceId, int $targetId, array $condition = [])
  508. {
  509. $condition = DBA::mergeConditions($condition,
  510. ['`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)
  511. AND `id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)',
  512. $sourceId, $targetId]
  513. );
  514. return DI::dba()->count('contact', $condition);
  515. }
  516. /**
  517. * Returns a paginated list of contacts that are followed by both provided public contacts.
  518. *
  519. * @param int $sourceId Public contact id
  520. * @param int $targetId Public contact id
  521. * @param array $condition Additional condition array on the contact table
  522. * @param int $count
  523. * @param int $offset
  524. * @param bool $shuffle
  525. * @return array
  526. * @throws Exception
  527. */
  528. public static function listCommonFollows(int $sourceId, int $targetId, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
  529. {
  530. $condition = DBA::mergeConditions($condition,
  531. ["`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)
  532. AND `id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)",
  533. $sourceId, $targetId]
  534. );
  535. return DI::dba()->selectToArray('contact', [], $condition,
  536. ['limit' => [$offset, $count], 'order' => [$shuffle ? 'RAND()' : 'name']]
  537. );
  538. }
  539. /**
  540. * Counts the number of contacts that follow both provided public contacts.
  541. *
  542. * @param int $sourceId Public contact id
  543. * @param int $targetId Public contact id
  544. * @param array $condition Additional condition on the contact table
  545. * @return int
  546. * @throws Exception
  547. */
  548. public static function countCommonFollowers(int $sourceId, int $targetId, array $condition = [])
  549. {
  550. $condition = DBA::mergeConditions($condition,
  551. ["`id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`)
  552. AND `id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`)",
  553. $sourceId, $targetId]
  554. );
  555. return DI::dba()->count('contact', $condition);
  556. }
  557. /**
  558. * Returns a paginated list of contacts that follow both provided public contacts.
  559. *
  560. * @param int $sourceId Public contact id
  561. * @param int $targetId Public contact id
  562. * @param array $condition Additional condition on the contact table
  563. * @param int $count
  564. * @param int $offset
  565. * @param bool $shuffle
  566. * @return array
  567. * @throws Exception
  568. */
  569. public static function listCommonFollowers(int $sourceId, int $targetId, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
  570. {
  571. $condition = DBA::mergeConditions($condition,
  572. ["`id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`)
  573. AND `id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`)",
  574. $sourceId, $targetId]
  575. );
  576. return DI::dba()->selectToArray('contact', [], $condition,
  577. ['limit' => [$offset, $count], 'order' => [$shuffle ? 'RAND()' : 'name']]
  578. );
  579. }
  580. }