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.
 
 
 
 
 
 

6300 lines
182 KiB

  1. <?php
  2. /**
  3. * Friendica implementation of statusnet/twitter API
  4. *
  5. * @file include/api.php
  6. * @todo Automatically detect if incoming data is HTML or BBCode
  7. */
  8. use Friendica\App;
  9. use Friendica\Content\ContactSelector;
  10. use Friendica\Content\Feature;
  11. use Friendica\Content\Text\BBCode;
  12. use Friendica\Content\Text\HTML;
  13. use Friendica\Core\Config;
  14. use Friendica\Core\Hook;
  15. use Friendica\Core\L10n;
  16. use Friendica\Core\Logger;
  17. use Friendica\Core\Protocol;
  18. use Friendica\Core\Session;
  19. use Friendica\Core\System;
  20. use Friendica\Core\Worker;
  21. use Friendica\Database\DBA;
  22. use Friendica\DI;
  23. use Friendica\Model\Contact;
  24. use Friendica\Model\Group;
  25. use Friendica\Model\Item;
  26. use Friendica\Model\Mail;
  27. use Friendica\Model\Photo;
  28. use Friendica\Model\Profile;
  29. use Friendica\Model\User;
  30. use Friendica\Model\UserItem;
  31. use Friendica\Network\FKOAuth1;
  32. use Friendica\Network\HTTPException;
  33. use Friendica\Network\HTTPException\BadRequestException;
  34. use Friendica\Network\HTTPException\ExpectationFailedException;
  35. use Friendica\Network\HTTPException\ForbiddenException;
  36. use Friendica\Network\HTTPException\InternalServerErrorException;
  37. use Friendica\Network\HTTPException\MethodNotAllowedException;
  38. use Friendica\Network\HTTPException\NotFoundException;
  39. use Friendica\Network\HTTPException\NotImplementedException;
  40. use Friendica\Network\HTTPException\TooManyRequestsException;
  41. use Friendica\Network\HTTPException\UnauthorizedException;
  42. use Friendica\Object\Image;
  43. use Friendica\Protocol\Activity;
  44. use Friendica\Protocol\Diaspora;
  45. use Friendica\Util\DateTimeFormat;
  46. use Friendica\Util\Images;
  47. use Friendica\Util\Network;
  48. use Friendica\Util\Proxy as ProxyUtils;
  49. use Friendica\Util\Strings;
  50. use Friendica\Util\XML;
  51. require_once __DIR__ . '/../mod/share.php';
  52. require_once __DIR__ . '/../mod/item.php';
  53. require_once __DIR__ . '/../mod/wall_upload.php';
  54. define('API_METHOD_ANY', '*');
  55. define('API_METHOD_GET', 'GET');
  56. define('API_METHOD_POST', 'POST,PUT');
  57. define('API_METHOD_DELETE', 'POST,DELETE');
  58. define('API_LOG_PREFIX', 'API {action} - ');
  59. $API = [];
  60. $called_api = [];
  61. /**
  62. * It is not sufficient to use local_user() to check whether someone is allowed to use the API,
  63. * because this will open CSRF holes (just embed an image with src=friendicasite.com/api/statuses/update?status=CSRF
  64. * into a page, and visitors will post something without noticing it).
  65. *
  66. * @brief Auth API user
  67. */
  68. function api_user()
  69. {
  70. if (!empty($_SESSION['allow_api'])) {
  71. return local_user();
  72. }
  73. return false;
  74. }
  75. /**
  76. * Clients can send 'source' parameter to be show in post metadata
  77. * as "sent via <source>".
  78. * Some clients doesn't send a source param, we support ones we know
  79. * (only Twidere, atm)
  80. *
  81. * @brief Get source name from API client
  82. *
  83. * @return string
  84. * Client source name, default to "api" if unset/unknown
  85. * @throws Exception
  86. */
  87. function api_source()
  88. {
  89. if (requestdata('source')) {
  90. return requestdata('source');
  91. }
  92. // Support for known clients that doesn't send a source name
  93. if (!empty($_SERVER['HTTP_USER_AGENT'])) {
  94. if(strpos($_SERVER['HTTP_USER_AGENT'], "Twidere") !== false) {
  95. return "Twidere";
  96. }
  97. Logger::info(API_LOG_PREFIX . 'Unrecognized user-agent', ['module' => 'api', 'action' => 'source', 'http_user_agent' => $_SERVER['HTTP_USER_AGENT']]);
  98. } else {
  99. Logger::info(API_LOG_PREFIX . 'Empty user-agent', ['module' => 'api', 'action' => 'source']);
  100. }
  101. return "api";
  102. }
  103. /**
  104. * @brief Format date for API
  105. *
  106. * @param string $str Source date, as UTC
  107. * @return string Date in UTC formatted as "D M d H:i:s +0000 Y"
  108. * @throws Exception
  109. */
  110. function api_date($str)
  111. {
  112. // Wed May 23 06:01:13 +0000 2007
  113. return DateTimeFormat::utc($str, "D M d H:i:s +0000 Y");
  114. }
  115. /**
  116. * Register a function to be the endpoint for defined API path.
  117. *
  118. * @brief Register API endpoint
  119. *
  120. * @param string $path API URL path, relative to DI::baseUrl()
  121. * @param string $func Function name to call on path request
  122. * @param bool $auth API need logged user
  123. * @param string $method HTTP method reqiured to call this endpoint.
  124. * One of API_METHOD_ANY, API_METHOD_GET, API_METHOD_POST.
  125. * Default to API_METHOD_ANY
  126. */
  127. function api_register_func($path, $func, $auth = false, $method = API_METHOD_ANY)
  128. {
  129. global $API;
  130. $API[$path] = [
  131. 'func' => $func,
  132. 'auth' => $auth,
  133. 'method' => $method,
  134. ];
  135. // Workaround for hotot
  136. $path = str_replace("api/", "api/1.1/", $path);
  137. $API[$path] = [
  138. 'func' => $func,
  139. 'auth' => $auth,
  140. 'method' => $method,
  141. ];
  142. }
  143. /**
  144. * Log in user via OAuth1 or Simple HTTP Auth.
  145. * Simple Auth allow username in form of <pre>user@server</pre>, ignoring server part
  146. *
  147. * @brief Login API user
  148. *
  149. * @param App $a App
  150. * @throws ForbiddenException
  151. * @throws InternalServerErrorException
  152. * @throws UnauthorizedException
  153. * @hook 'authenticate'
  154. * array $addon_auth
  155. * 'username' => username from login form
  156. * 'password' => password from login form
  157. * 'authenticated' => return status,
  158. * 'user_record' => return authenticated user record
  159. */
  160. function api_login(App $a)
  161. {
  162. $oauth1 = new FKOAuth1();
  163. // login with oauth
  164. try {
  165. $request = OAuthRequest::from_request();
  166. list($consumer, $token) = $oauth1->verify_request($request);
  167. if (!is_null($token)) {
  168. $oauth1->loginUser($token->uid);
  169. Session::set('allow_api', true);
  170. return;
  171. }
  172. echo __FILE__.__LINE__.__FUNCTION__ . "<pre>";
  173. var_dump($consumer, $token);
  174. die();
  175. } catch (Exception $e) {
  176. Logger::warning(API_LOG_PREFIX . 'error', ['module' => 'api', 'action' => 'login', 'exception' => $e->getMessage()]);
  177. }
  178. // workaround for HTTP-auth in CGI mode
  179. if (!empty($_SERVER['REDIRECT_REMOTE_USER'])) {
  180. $userpass = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"], 6));
  181. if (strlen($userpass)) {
  182. list($name, $password) = explode(':', $userpass);
  183. $_SERVER['PHP_AUTH_USER'] = $name;
  184. $_SERVER['PHP_AUTH_PW'] = $password;
  185. }
  186. }
  187. if (empty($_SERVER['PHP_AUTH_USER'])) {
  188. Logger::debug(API_LOG_PREFIX . 'failed', ['module' => 'api', 'action' => 'login', 'parameters' => $_SERVER]);
  189. header('WWW-Authenticate: Basic realm="Friendica"');
  190. throw new UnauthorizedException("This API requires login");
  191. }
  192. $user = $_SERVER['PHP_AUTH_USER'] ?? '';
  193. $password = $_SERVER['PHP_AUTH_PW'] ?? '';
  194. // allow "user@server" login (but ignore 'server' part)
  195. $at = strstr($user, "@", true);
  196. if ($at) {
  197. $user = $at;
  198. }
  199. // next code from mod/auth.php. needs better solution
  200. $record = null;
  201. $addon_auth = [
  202. 'username' => trim($user),
  203. 'password' => trim($password),
  204. 'authenticated' => 0,
  205. 'user_record' => null,
  206. ];
  207. /*
  208. * An addon indicates successful login by setting 'authenticated' to non-zero value and returning a user record
  209. * Addons should never set 'authenticated' except to indicate success - as hooks may be chained
  210. * and later addons should not interfere with an earlier one that succeeded.
  211. */
  212. Hook::callAll('authenticate', $addon_auth);
  213. if ($addon_auth['authenticated'] && count($addon_auth['user_record'])) {
  214. $record = $addon_auth['user_record'];
  215. } else {
  216. $user_id = User::authenticate(trim($user), trim($password), true);
  217. if ($user_id !== false) {
  218. $record = DBA::selectFirst('user', [], ['uid' => $user_id]);
  219. }
  220. }
  221. if (!DBA::isResult($record)) {
  222. Logger::debug(API_LOG_PREFIX . 'failed', ['module' => 'api', 'action' => 'login', 'parameters' => $_SERVER]);
  223. header('WWW-Authenticate: Basic realm="Friendica"');
  224. //header('HTTP/1.0 401 Unauthorized');
  225. //die('This api requires login');
  226. throw new UnauthorizedException("This API requires login");
  227. }
  228. DI::auth()->setForUser($a, $record);
  229. $_SESSION["allow_api"] = true;
  230. Hook::callAll('logged_in', $a->user);
  231. }
  232. /**
  233. * API endpoints can define which HTTP method to accept when called.
  234. * This function check the current HTTP method agains endpoint
  235. * registered method.
  236. *
  237. * @brief Check HTTP method of called API
  238. *
  239. * @param string $method Required methods, uppercase, separated by comma
  240. * @return bool
  241. */
  242. function api_check_method($method)
  243. {
  244. if ($method == "*") {
  245. return true;
  246. }
  247. return (stripos($method, $_SERVER['REQUEST_METHOD'] ?? 'GET') !== false);
  248. }
  249. /**
  250. * Authenticate user, call registered API function, set HTTP headers
  251. *
  252. * @brief Main API entry point
  253. *
  254. * @param App $a App
  255. * @param App\Arguments $args The app arguments (optional, will retrieved by the DI-Container in case of missing)
  256. * @return string|array API call result
  257. * @throws Exception
  258. */
  259. function api_call(App $a, App\Arguments $args = null)
  260. {
  261. global $API, $called_api;
  262. if ($args == null) {
  263. $args = DI::args();
  264. }
  265. $type = "json";
  266. if (strpos($args->getQueryString(), ".xml") > 0) {
  267. $type = "xml";
  268. }
  269. if (strpos($args->getQueryString(), ".json") > 0) {
  270. $type = "json";
  271. }
  272. if (strpos($args->getQueryString(), ".rss") > 0) {
  273. $type = "rss";
  274. }
  275. if (strpos($args->getQueryString(), ".atom") > 0) {
  276. $type = "atom";
  277. }
  278. try {
  279. foreach ($API as $p => $info) {
  280. if (strpos($args->getQueryString(), $p) === 0) {
  281. if (!api_check_method($info['method'])) {
  282. throw new MethodNotAllowedException();
  283. }
  284. $called_api = explode("/", $p);
  285. //unset($_SERVER['PHP_AUTH_USER']);
  286. /// @TODO should be "true ==[=] $info['auth']", if you miss only one = character, you assign a variable (only with ==). Let's make all this even.
  287. if (!empty($info['auth']) && api_user() === false) {
  288. api_login($a);
  289. }
  290. Logger::info(API_LOG_PREFIX . 'username {username}', ['module' => 'api', 'action' => 'call', 'username' => $a->user['username']]);
  291. Logger::debug(API_LOG_PREFIX . 'parameters', ['module' => 'api', 'action' => 'call', 'parameters' => $_REQUEST]);
  292. $stamp = microtime(true);
  293. $return = call_user_func($info['func'], $type);
  294. $duration = floatval(microtime(true) - $stamp);
  295. Logger::info(API_LOG_PREFIX . 'username {username}', ['module' => 'api', 'action' => 'call', 'username' => $a->user['username'], 'duration' => round($duration, 2)]);
  296. DI::profiler()->saveLog(DI::logger(), API_LOG_PREFIX . 'performance');
  297. if (false === $return) {
  298. /*
  299. * api function returned false withour throw an
  300. * exception. This should not happend, throw a 500
  301. */
  302. throw new InternalServerErrorException();
  303. }
  304. switch ($type) {
  305. case "xml":
  306. header("Content-Type: text/xml");
  307. break;
  308. case "json":
  309. header("Content-Type: application/json");
  310. if (!empty($return)) {
  311. $json = json_encode(end($return));
  312. if (!empty($_GET['callback'])) {
  313. $json = $_GET['callback'] . "(" . $json . ")";
  314. }
  315. $return = $json;
  316. }
  317. break;
  318. case "rss":
  319. header("Content-Type: application/rss+xml");
  320. $return = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $return;
  321. break;
  322. case "atom":
  323. header("Content-Type: application/atom+xml");
  324. $return = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $return;
  325. break;
  326. }
  327. return $return;
  328. }
  329. }
  330. Logger::warning(API_LOG_PREFIX . 'not implemented', ['module' => 'api', 'action' => 'call', 'query' => DI::args()->getQueryString()]);
  331. throw new NotImplementedException();
  332. } catch (HTTPException $e) {
  333. header("HTTP/1.1 {$e->getCode()} {$e->httpdesc}");
  334. return api_error($type, $e, $args);
  335. }
  336. }
  337. /**
  338. * @brief Format API error string
  339. *
  340. * @param string $type Return type (xml, json, rss, as)
  341. * @param object $e HTTPException Error object
  342. * @param App\Arguments $args The App arguments
  343. * @return string|array error message formatted as $type
  344. */
  345. function api_error($type, $e, App\Arguments $args)
  346. {
  347. $error = ($e->getMessage() !== "" ? $e->getMessage() : $e->httpdesc);
  348. /// @TODO: https://dev.twitter.com/overview/api/response-codes
  349. $error = ["error" => $error,
  350. "code" => $e->getCode() . " " . $e->httpdesc,
  351. "request" => $args->getQueryString()];
  352. $return = api_format_data('status', $type, ['status' => $error]);
  353. switch ($type) {
  354. case "xml":
  355. header("Content-Type: text/xml");
  356. break;
  357. case "json":
  358. header("Content-Type: application/json");
  359. $return = json_encode($return);
  360. break;
  361. case "rss":
  362. header("Content-Type: application/rss+xml");
  363. break;
  364. case "atom":
  365. header("Content-Type: application/atom+xml");
  366. break;
  367. }
  368. return $return;
  369. }
  370. /**
  371. * @brief Set values for RSS template
  372. *
  373. * @param App $a
  374. * @param array $arr Array to be passed to template
  375. * @param array $user_info User info
  376. * @return array
  377. * @throws BadRequestException
  378. * @throws ImagickException
  379. * @throws InternalServerErrorException
  380. * @throws UnauthorizedException
  381. * @todo find proper type-hints
  382. */
  383. function api_rss_extra(App $a, $arr, $user_info)
  384. {
  385. if (is_null($user_info)) {
  386. $user_info = api_get_user($a);
  387. }
  388. $arr['$user'] = $user_info;
  389. $arr['$rss'] = [
  390. 'alternate' => $user_info['url'],
  391. 'self' => DI::baseUrl() . "/" . DI::args()->getQueryString(),
  392. 'base' => DI::baseUrl(),
  393. 'updated' => api_date(null),
  394. 'atom_updated' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
  395. 'language' => $user_info['lang'],
  396. 'logo' => DI::baseUrl() . "/images/friendica-32.png",
  397. ];
  398. return $arr;
  399. }
  400. /**
  401. * @brief Unique contact to contact url.
  402. *
  403. * @param int $id Contact id
  404. * @return bool|string
  405. * Contact url or False if contact id is unknown
  406. * @throws Exception
  407. */
  408. function api_unique_id_to_nurl($id)
  409. {
  410. $r = DBA::selectFirst('contact', ['nurl'], ['id' => $id]);
  411. if (DBA::isResult($r)) {
  412. return $r["nurl"];
  413. } else {
  414. return false;
  415. }
  416. }
  417. /**
  418. * @brief Get user info array.
  419. *
  420. * @param App $a App
  421. * @param int|string $contact_id Contact ID or URL
  422. * @return array|bool
  423. * @throws BadRequestException
  424. * @throws ImagickException
  425. * @throws InternalServerErrorException
  426. * @throws UnauthorizedException
  427. */
  428. function api_get_user(App $a, $contact_id = null)
  429. {
  430. global $called_api;
  431. $user = null;
  432. $extra_query = "";
  433. $url = "";
  434. Logger::info(API_LOG_PREFIX . 'Fetching data for user {user}', ['module' => 'api', 'action' => 'get_user', 'user' => $contact_id]);
  435. // Searching for contact URL
  436. if (!is_null($contact_id) && (intval($contact_id) == 0)) {
  437. $user = DBA::escape(Strings::normaliseLink($contact_id));
  438. $url = $user;
  439. $extra_query = "AND `contact`.`nurl` = '%s' ";
  440. if (api_user() !== false) {
  441. $extra_query .= "AND `contact`.`uid`=" . intval(api_user());
  442. }
  443. }
  444. // Searching for contact id with uid = 0
  445. if (!is_null($contact_id) && (intval($contact_id) != 0)) {
  446. $user = DBA::escape(api_unique_id_to_nurl(intval($contact_id)));
  447. if ($user == "") {
  448. throw new BadRequestException("User ID ".$contact_id." not found.");
  449. }
  450. $url = $user;
  451. $extra_query = "AND `contact`.`nurl` = '%s' ";
  452. if (api_user() !== false) {
  453. $extra_query .= "AND `contact`.`uid`=" . intval(api_user());
  454. }
  455. }
  456. if (is_null($user) && !empty($_GET['user_id'])) {
  457. $user = DBA::escape(api_unique_id_to_nurl($_GET['user_id']));
  458. if ($user == "") {
  459. throw new BadRequestException("User ID ".$_GET['user_id']." not found.");
  460. }
  461. $url = $user;
  462. $extra_query = "AND `contact`.`nurl` = '%s' ";
  463. if (api_user() !== false) {
  464. $extra_query .= "AND `contact`.`uid`=" . intval(api_user());
  465. }
  466. }
  467. if (is_null($user) && !empty($_GET['screen_name'])) {
  468. $user = DBA::escape($_GET['screen_name']);
  469. $extra_query = "AND `contact`.`nick` = '%s' ";
  470. if (api_user() !== false) {
  471. $extra_query .= "AND `contact`.`uid`=".intval(api_user());
  472. }
  473. }
  474. if (is_null($user) && !empty($_GET['profileurl'])) {
  475. $user = DBA::escape(Strings::normaliseLink($_GET['profileurl']));
  476. $extra_query = "AND `contact`.`nurl` = '%s' ";
  477. if (api_user() !== false) {
  478. $extra_query .= "AND `contact`.`uid`=".intval(api_user());
  479. }
  480. }
  481. // $called_api is the API path exploded on / and is expected to have at least 2 elements
  482. if (is_null($user) && ($a->argc > (count($called_api) - 1)) && (count($called_api) > 0)) {
  483. $argid = count($called_api);
  484. if (!empty($a->argv[$argid])) {
  485. $data = explode(".", $a->argv[$argid]);
  486. if (count($data) > 1) {
  487. list($user, $null) = $data;
  488. }
  489. }
  490. if (is_numeric($user)) {
  491. $user = DBA::escape(api_unique_id_to_nurl(intval($user)));
  492. if ($user != "") {
  493. $url = $user;
  494. $extra_query = "AND `contact`.`nurl` = '%s' ";
  495. if (api_user() !== false) {
  496. $extra_query .= "AND `contact`.`uid`=" . intval(api_user());
  497. }
  498. }
  499. } else {
  500. $user = DBA::escape($user);
  501. $extra_query = "AND `contact`.`nick` = '%s' ";
  502. if (api_user() !== false) {
  503. $extra_query .= "AND `contact`.`uid`=" . intval(api_user());
  504. }
  505. }
  506. }
  507. Logger::info(API_LOG_PREFIX . 'getting user {user}', ['module' => 'api', 'action' => 'get_user', 'user' => $user]);
  508. if (!$user) {
  509. if (api_user() === false) {
  510. api_login($a);
  511. return false;
  512. } else {
  513. $user = $_SESSION['uid'];
  514. $extra_query = "AND `contact`.`uid` = %d AND `contact`.`self` ";
  515. }
  516. }
  517. Logger::info(API_LOG_PREFIX . 'found user {user}', ['module' => 'api', 'action' => 'get_user', 'user' => $user, 'extra_query' => $extra_query]);
  518. // user info
  519. $uinfo = q(
  520. "SELECT *, `contact`.`id` AS `cid` FROM `contact`
  521. WHERE 1
  522. $extra_query",
  523. $user
  524. );
  525. // Selecting the id by priority, friendica first
  526. if (is_array($uinfo)) {
  527. api_best_nickname($uinfo);
  528. }
  529. // if the contact wasn't found, fetch it from the contacts with uid = 0
  530. if (!DBA::isResult($uinfo)) {
  531. if ($url == "") {
  532. throw new BadRequestException("User not found.");
  533. }
  534. $contact = DBA::selectFirst('contact', [], ['uid' => 0, 'nurl' => Strings::normaliseLink($url)]);
  535. if (DBA::isResult($contact)) {
  536. $ret = [
  537. 'id' => $contact["id"],
  538. 'id_str' => (string) $contact["id"],
  539. 'name' => $contact["name"],
  540. 'screen_name' => (($contact['nick']) ? $contact['nick'] : $contact['name']),
  541. 'location' => ($contact["location"] != "") ? $contact["location"] : ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol']),
  542. 'description' => BBCode::toPlaintext($contact["about"]),
  543. 'profile_image_url' => $contact["micro"],
  544. 'profile_image_url_https' => $contact["micro"],
  545. 'profile_image_url_profile_size' => $contact["thumb"],
  546. 'profile_image_url_large' => $contact["photo"],
  547. 'url' => $contact["url"],
  548. 'protected' => false,
  549. 'followers_count' => 0,
  550. 'friends_count' => 0,
  551. 'listed_count' => 0,
  552. 'created_at' => api_date($contact["created"]),
  553. 'favourites_count' => 0,
  554. 'utc_offset' => 0,
  555. 'time_zone' => 'UTC',
  556. 'geo_enabled' => false,
  557. 'verified' => false,
  558. 'statuses_count' => 0,
  559. 'lang' => '',
  560. 'contributors_enabled' => false,
  561. 'is_translator' => false,
  562. 'is_translation_enabled' => false,
  563. 'following' => false,
  564. 'follow_request_sent' => false,
  565. 'statusnet_blocking' => false,
  566. 'notifications' => false,
  567. 'statusnet_profile_url' => $contact["url"],
  568. 'uid' => 0,
  569. 'cid' => Contact::getIdForURL($contact["url"], api_user(), true),
  570. 'pid' => Contact::getIdForURL($contact["url"], 0, true),
  571. 'self' => 0,
  572. 'network' => $contact["network"],
  573. ];
  574. return $ret;
  575. } else {
  576. throw new BadRequestException("User ".$url." not found.");
  577. }
  578. }
  579. if ($uinfo[0]['self']) {
  580. if ($uinfo[0]['network'] == "") {
  581. $uinfo[0]['network'] = Protocol::DFRN;
  582. }
  583. $usr = DBA::selectFirst('user', ['default-location'], ['uid' => api_user()]);
  584. $profile = DBA::selectFirst('profile', ['about'], ['uid' => api_user(), 'is-default' => true]);
  585. }
  586. $countitems = 0;
  587. $countfriends = 0;
  588. $countfollowers = 0;
  589. $starred = 0;
  590. $pcontact_id = Contact::getIdForURL($uinfo[0]['url'], 0, true);
  591. if (!empty($profile['about'])) {
  592. $description = $profile['about'];
  593. } else {
  594. $description = $uinfo[0]["about"];
  595. }
  596. if (!empty($usr['default-location'])) {
  597. $location = $usr['default-location'];
  598. } elseif (!empty($uinfo[0]["location"])) {
  599. $location = $uinfo[0]["location"];
  600. } else {
  601. $location = ContactSelector::networkToName($uinfo[0]['network'], $uinfo[0]['url'], $uinfo[0]['protocol']);
  602. }
  603. $ret = [
  604. 'id' => intval($pcontact_id),
  605. 'id_str' => (string) intval($pcontact_id),
  606. 'name' => (($uinfo[0]['name']) ? $uinfo[0]['name'] : $uinfo[0]['nick']),
  607. 'screen_name' => (($uinfo[0]['nick']) ? $uinfo[0]['nick'] : $uinfo[0]['name']),
  608. 'location' => $location,
  609. 'description' => BBCode::toPlaintext($description),
  610. 'profile_image_url' => $uinfo[0]['micro'],
  611. 'profile_image_url_https' => $uinfo[0]['micro'],
  612. 'profile_image_url_profile_size' => $uinfo[0]["thumb"],
  613. 'profile_image_url_large' => $uinfo[0]["photo"],
  614. 'url' => $uinfo[0]['url'],
  615. 'protected' => false,
  616. 'followers_count' => intval($countfollowers),
  617. 'friends_count' => intval($countfriends),
  618. 'listed_count' => 0,
  619. 'created_at' => api_date($uinfo[0]['created']),
  620. 'favourites_count' => intval($starred),
  621. 'utc_offset' => "0",
  622. 'time_zone' => 'UTC',
  623. 'geo_enabled' => false,
  624. 'verified' => true,
  625. 'statuses_count' => intval($countitems),
  626. 'lang' => '',
  627. 'contributors_enabled' => false,
  628. 'is_translator' => false,
  629. 'is_translation_enabled' => false,
  630. 'following' => (($uinfo[0]['rel'] == Contact::FOLLOWER) || ($uinfo[0]['rel'] == Contact::FRIEND)),
  631. 'follow_request_sent' => false,
  632. 'statusnet_blocking' => false,
  633. 'notifications' => false,
  634. /// @TODO old way?
  635. //'statusnet_profile_url' => DI::baseUrl()."/contact/".$uinfo[0]['cid'],
  636. 'statusnet_profile_url' => $uinfo[0]['url'],
  637. 'uid' => intval($uinfo[0]['uid']),
  638. 'cid' => intval($uinfo[0]['cid']),
  639. 'pid' => Contact::getIdForURL($uinfo[0]["url"], 0, true),
  640. 'self' => $uinfo[0]['self'],
  641. 'network' => $uinfo[0]['network'],
  642. ];
  643. // If this is a local user and it uses Frio, we can get its color preferences.
  644. if ($ret['self']) {
  645. $theme_info = DBA::selectFirst('user', ['theme'], ['uid' => $ret['uid']]);
  646. if ($theme_info['theme'] === 'frio') {
  647. $schema = DI::pConfig()->get($ret['uid'], 'frio', 'schema');
  648. if ($schema && ($schema != '---')) {
  649. if (file_exists('view/theme/frio/schema/'.$schema.'.php')) {
  650. $schemefile = 'view/theme/frio/schema/'.$schema.'.php';
  651. require_once $schemefile;
  652. }
  653. } else {
  654. $nav_bg = DI::pConfig()->get($ret['uid'], 'frio', 'nav_bg');
  655. $link_color = DI::pConfig()->get($ret['uid'], 'frio', 'link_color');
  656. $bgcolor = DI::pConfig()->get($ret['uid'], 'frio', 'background_color');
  657. }
  658. if (empty($nav_bg)) {
  659. $nav_bg = "#708fa0";
  660. }
  661. if (empty($link_color)) {
  662. $link_color = "#6fdbe8";
  663. }
  664. if (empty($bgcolor)) {
  665. $bgcolor = "#ededed";
  666. }
  667. $ret['profile_sidebar_fill_color'] = str_replace('#', '', $nav_bg);
  668. $ret['profile_link_color'] = str_replace('#', '', $link_color);
  669. $ret['profile_background_color'] = str_replace('#', '', $bgcolor);
  670. }
  671. }
  672. return $ret;
  673. }
  674. /**
  675. * @brief return api-formatted array for item's author and owner
  676. *
  677. * @param App $a App
  678. * @param array $item item from db
  679. * @return array(array:author, array:owner)
  680. * @throws BadRequestException
  681. * @throws ImagickException
  682. * @throws InternalServerErrorException
  683. * @throws UnauthorizedException
  684. */
  685. function api_item_get_user(App $a, $item)
  686. {
  687. $status_user = api_get_user($a, $item['author-id'] ?? null);
  688. $author_user = $status_user;
  689. $status_user["protected"] = $item['private'] ?? 0;
  690. if (($item['thr-parent'] ?? '') == ($item['uri'] ?? '')) {
  691. $owner_user = api_get_user($a, $item['owner-id'] ?? null);
  692. } else {
  693. $owner_user = $author_user;
  694. }
  695. return ([$status_user, $author_user, $owner_user]);
  696. }
  697. /**
  698. * @brief walks recursively through an array with the possibility to change value and key
  699. *
  700. * @param array $array The array to walk through
  701. * @param callable $callback The callback function
  702. *
  703. * @return array the transformed array
  704. */
  705. function api_walk_recursive(array &$array, callable $callback)
  706. {
  707. $new_array = [];
  708. foreach ($array as $k => $v) {
  709. if (is_array($v)) {
  710. if ($callback($v, $k)) {
  711. $new_array[$k] = api_walk_recursive($v, $callback);
  712. }
  713. } else {
  714. if ($callback($v, $k)) {
  715. $new_array[$k] = $v;
  716. }
  717. }
  718. }
  719. $array = $new_array;
  720. return $array;
  721. }
  722. /**
  723. * @brief Callback function to transform the array in an array that can be transformed in a XML file
  724. *
  725. * @param mixed $item Array item value
  726. * @param string $key Array key
  727. *
  728. * @return boolean Should the array item be deleted?
  729. */
  730. function api_reformat_xml(&$item, &$key)
  731. {
  732. if (is_bool($item)) {
  733. $item = ($item ? "true" : "false");
  734. }
  735. if (substr($key, 0, 10) == "statusnet_") {
  736. $key = "statusnet:".substr($key, 10);
  737. } elseif (substr($key, 0, 10) == "friendica_") {
  738. $key = "friendica:".substr($key, 10);
  739. }
  740. /// @TODO old-lost code?
  741. //else
  742. // $key = "default:".$key;
  743. return true;
  744. }
  745. /**
  746. * @brief Creates the XML from a JSON style array
  747. *
  748. * @param array $data JSON style array
  749. * @param string $root_element Name of the root element
  750. *
  751. * @return string The XML data
  752. */
  753. function api_create_xml(array $data, $root_element)
  754. {
  755. $childname = key($data);
  756. $data2 = array_pop($data);
  757. $namespaces = ["" => "http://api.twitter.com",
  758. "statusnet" => "http://status.net/schema/api/1/",
  759. "friendica" => "http://friendi.ca/schema/api/1/",
  760. "georss" => "http://www.georss.org/georss"];
  761. /// @todo Auto detection of needed namespaces
  762. if (in_array($root_element, ["ok", "hash", "config", "version", "ids", "notes", "photos"])) {
  763. $namespaces = [];
  764. }
  765. if (is_array($data2)) {
  766. $key = key($data2);
  767. api_walk_recursive($data2, "api_reformat_xml");
  768. if ($key == "0") {
  769. $data4 = [];
  770. $i = 1;
  771. foreach ($data2 as $item) {
  772. $data4[$i++ . ":" . $childname] = $item;
  773. }
  774. $data2 = $data4;
  775. }
  776. }
  777. $data3 = [$root_element => $data2];
  778. $ret = XML::fromArray($data3, $xml, false, $namespaces);
  779. return $ret;
  780. }
  781. /**
  782. * @brief Formats the data according to the data type
  783. *
  784. * @param string $root_element Name of the root element
  785. * @param string $type Return type (atom, rss, xml, json)
  786. * @param array $data JSON style array
  787. *
  788. * @return array|string (string|array) XML data or JSON data
  789. */
  790. function api_format_data($root_element, $type, $data)
  791. {
  792. switch ($type) {
  793. case "atom":
  794. case "rss":
  795. case "xml":
  796. $ret = api_create_xml($data, $root_element);
  797. break;
  798. case "json":
  799. default:
  800. $ret = $data;
  801. break;
  802. }
  803. return $ret;
  804. }
  805. /**
  806. * TWITTER API
  807. */
  808. /**
  809. * Returns an HTTP 200 OK response code and a representation of the requesting user if authentication was successful;
  810. * returns a 401 status code and an error message if not.
  811. *
  812. * @see https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-account-verify_credentials
  813. *
  814. * @param string $type Return type (atom, rss, xml, json)
  815. * @return array|string
  816. * @throws BadRequestException
  817. * @throws ForbiddenException
  818. * @throws ImagickException
  819. * @throws InternalServerErrorException
  820. * @throws UnauthorizedException
  821. */
  822. function api_account_verify_credentials($type)
  823. {
  824. $a = DI::app();
  825. if (api_user() === false) {
  826. throw new ForbiddenException();
  827. }
  828. unset($_REQUEST["user_id"]);
  829. unset($_GET["user_id"]);
  830. unset($_REQUEST["screen_name"]);
  831. unset($_GET["screen_name"]);
  832. $skip_status = $_REQUEST['skip_status'] ?? false;
  833. $user_info = api_get_user($a);
  834. // "verified" isn't used here in the standard
  835. unset($user_info["verified"]);
  836. // - Adding last status
  837. if (!$skip_status) {
  838. $item = api_get_last_status($user_info['pid'], $user_info['uid']);
  839. if (!empty($item)) {
  840. $user_info['status'] = api_format_item($item, $type);
  841. }
  842. }
  843. // "uid" and "self" are only needed for some internal stuff, so remove it from here
  844. unset($user_info["uid"]);
  845. unset($user_info["self"]);
  846. return api_format_data("user", $type, ['user' => $user_info]);
  847. }
  848. /// @TODO move to top of file or somewhere better
  849. api_register_func('api/account/verify_credentials', 'api_account_verify_credentials', true);
  850. /**
  851. * Get data from $_POST or $_GET
  852. *
  853. * @param string $k
  854. * @return null
  855. */
  856. function requestdata($k)
  857. {
  858. if (!empty($_POST[$k])) {
  859. return $_POST[$k];
  860. }
  861. if (!empty($_GET[$k])) {
  862. return $_GET[$k];
  863. }
  864. return null;
  865. }
  866. /**
  867. * Deprecated function to upload media.
  868. *
  869. * @param string $type Return type (atom, rss, xml, json)
  870. *
  871. * @return array|string
  872. * @throws BadRequestException
  873. * @throws ForbiddenException
  874. * @throws ImagickException
  875. * @throws InternalServerErrorException
  876. * @throws UnauthorizedException
  877. */
  878. function api_statuses_mediap($type)
  879. {
  880. $a = DI::app();
  881. if (api_user() === false) {
  882. Logger::log('api_statuses_update: no user');
  883. throw new ForbiddenException();
  884. }
  885. $user_info = api_get_user($a);
  886. $_REQUEST['profile_uid'] = api_user();
  887. $_REQUEST['api_source'] = true;
  888. $txt = requestdata('status');
  889. /// @TODO old-lost code?
  890. //$txt = urldecode(requestdata('status'));
  891. if ((strpos($txt, '<') !== false) || (strpos($txt, '>') !== false)) {
  892. $txt = HTML::toBBCodeVideo($txt);
  893. $config = HTMLPurifier_Config::createDefault();
  894. $config->set('Cache.DefinitionImpl', null);
  895. $purifier = new HTMLPurifier($config);
  896. $txt = $purifier->purify($txt);
  897. }
  898. $txt = HTML::toBBCode($txt);
  899. $a->argv[1] = $user_info['screen_name']; //should be set to username?
  900. $picture = wall_upload_post($a, false);
  901. // now that we have the img url in bbcode we can add it to the status and insert the wall item.
  902. $_REQUEST['body'] = $txt . "\n\n" . '[url=' . $picture["albumpage"] . '][img]' . $picture["preview"] . "[/img][/url]";
  903. $item_id = item_post($a);
  904. // output the post that we just posted.
  905. return api_status_show($type, $item_id);
  906. }
  907. /// @TODO move this to top of file or somewhere better!
  908. api_register_func('api/statuses/mediap', 'api_statuses_mediap', true, API_METHOD_POST);
  909. /**
  910. * Updates the user’s current status.
  911. *
  912. * @param string $type Return type (atom, rss, xml, json)
  913. *
  914. * @return array|string
  915. * @throws BadRequestException
  916. * @throws ForbiddenException
  917. * @throws ImagickException
  918. * @throws InternalServerErrorException
  919. * @throws TooManyRequestsException
  920. * @throws UnauthorizedException
  921. * @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update
  922. */
  923. function api_statuses_update($type)
  924. {
  925. $a = DI::app();
  926. if (api_user() === false) {
  927. Logger::log('api_statuses_update: no user');
  928. throw new ForbiddenException();
  929. }
  930. api_get_user($a);
  931. // convert $_POST array items to the form we use for web posts.
  932. if (requestdata('htmlstatus')) {
  933. $txt = requestdata('htmlstatus');
  934. if ((strpos($txt, '<') !== false) || (strpos($txt, '>') !== false)) {
  935. $txt = HTML::toBBCodeVideo($txt);
  936. $config = HTMLPurifier_Config::createDefault();
  937. $config->set('Cache.DefinitionImpl', null);
  938. $purifier = new HTMLPurifier($config);
  939. $txt = $purifier->purify($txt);
  940. $_REQUEST['body'] = HTML::toBBCode($txt);
  941. }
  942. } else {
  943. $_REQUEST['body'] = requestdata('status');
  944. }
  945. $_REQUEST['title'] = requestdata('title');
  946. $parent = requestdata('in_reply_to_status_id');
  947. // Twidere sends "-1" if it is no reply ...
  948. if ($parent == -1) {
  949. $parent = "";
  950. }
  951. if (ctype_digit($parent)) {
  952. $_REQUEST['parent'] = $parent;
  953. } else {
  954. $_REQUEST['parent_uri'] = $parent;
  955. }
  956. if (requestdata('lat') && requestdata('long')) {
  957. $_REQUEST['coord'] = sprintf("%s %s", requestdata('lat'), requestdata('long'));
  958. }
  959. $_REQUEST['profile_uid'] = api_user();
  960. if (!$parent) {
  961. // Check for throttling (maximum posts per day, week and month)
  962. $throttle_day = Config::get('system', 'throttle_limit_day');
  963. if ($throttle_day > 0) {
  964. $datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60);
  965. $condition = ["`uid` = ? AND `wall` AND `received` > ?", api_user(), $datefrom];
  966. $posts_day = DBA::count('thread', $condition);
  967. if ($posts_day > $throttle_day) {
  968. Logger::log('Daily posting limit reached for user '.api_user(), Logger::DEBUG);
  969. // die(api_error($type, L10n::t("Daily posting limit of %d posts reached. The post was rejected.", $throttle_day));
  970. throw new TooManyRequestsException(L10n::tt("Daily posting limit of %d post reached. The post was rejected.", "Daily posting limit of %d posts reached. The post was rejected.", $throttle_day));
  971. }
  972. }
  973. $throttle_week = Config::get('system', 'throttle_limit_week');
  974. if ($throttle_week > 0) {
  975. $datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60*7);
  976. $condition = ["`uid` = ? AND `wall` AND `received` > ?", api_user(), $datefrom];
  977. $posts_week = DBA::count('thread', $condition);
  978. if ($posts_week > $throttle_week) {
  979. Logger::log('Weekly posting limit reached for user '.api_user(), Logger::DEBUG);
  980. // die(api_error($type, L10n::t("Weekly posting limit of %d posts reached. The post was rejected.", $throttle_week)));
  981. throw new TooManyRequestsException(L10n::tt("Weekly posting limit of %d post reached. The post was rejected.", "Weekly posting limit of %d posts reached. The post was rejected.", $throttle_week));
  982. }
  983. }
  984. $throttle_month = Config::get('system', 'throttle_limit_month');
  985. if ($throttle_month > 0) {
  986. $datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60*30);
  987. $condition = ["`uid` = ? AND `wall` AND `received` > ?", api_user(), $datefrom];
  988. $posts_month = DBA::count('thread', $condition);
  989. if ($posts_month > $throttle_month) {
  990. Logger::log('Monthly posting limit reached for user '.api_user(), Logger::DEBUG);
  991. // die(api_error($type, L10n::t("Monthly posting limit of %d posts reached. The post was rejected.", $throttle_month));
  992. throw new TooManyRequestsException(L10n::t("Monthly posting limit of %d post reached. The post was rejected.", "Monthly posting limit of %d posts reached. The post was rejected.", $throttle_month));
  993. }
  994. }
  995. }
  996. if (!empty($_FILES['media'])) {
  997. // upload the image if we have one
  998. $picture = wall_upload_post($a, false);
  999. if (is_array($picture)) {
  1000. $_REQUEST['body'] .= "\n\n" . '[url=' . $picture["albumpage"] . '][img]' . $picture["preview"] . "[/img][/url]";
  1001. }
  1002. }
  1003. if (requestdata('media_ids')) {
  1004. $ids = explode(',', requestdata('media_ids'));
  1005. foreach ($ids as $id) {
  1006. $r = q(
  1007. "SELECT `resource-id`, `scale`, `nickname`, `type`, `desc` FROM `photo` INNER JOIN `user` ON `user`.`uid` = `photo`.`uid` WHERE `resource-id` IN (SELECT `resource-id` FROM `photo` WHERE `id` = %d) AND `scale` > 0 AND `photo`.`uid` = %d ORDER BY `photo`.`width` DESC LIMIT 1",
  1008. intval($id),
  1009. api_user()
  1010. );
  1011. if (DBA::isResult($r)) {
  1012. $phototypes = Images::supportedTypes();
  1013. $ext = $phototypes[$r[0]['type']];
  1014. $description = $r[0]['desc'] ?? '';
  1015. $_REQUEST['body'] .= "\n\n" . '[url=' . DI::baseUrl() . '/photos/' . $r[0]['nickname'] . '/image/' . $r[0]['resource-id'] . ']';
  1016. $_REQUEST['body'] .= '[img=' . DI::baseUrl() . '/photo/' . $r[0]['resource-id'] . '-' . $r[0]['scale'] . '.' . $ext . ']' . $description . '[/img][/url]';
  1017. }
  1018. }
  1019. }
  1020. // set this so that the item_post() function is quiet and doesn't redirect or emit json
  1021. $_REQUEST['api_source'] = true;
  1022. if (empty($_REQUEST['source'])) {
  1023. $_REQUEST["source"] = api_source();
  1024. }
  1025. // call out normal post function
  1026. $item_id = item_post($a);
  1027. // output the post that we just posted.
  1028. return api_status_show($type, $item_id);
  1029. }
  1030. /// @TODO move to top of file or somewhere better
  1031. api_register_func('api/statuses/update', 'api_statuses_update', true, API_METHOD_POST);
  1032. api_register_func('api/statuses/update_with_media', 'api_statuses_update', true, API_METHOD_POST);
  1033. /**
  1034. * Uploads an image to Friendica.
  1035. *
  1036. * @return array
  1037. * @throws BadRequestException
  1038. * @throws ForbiddenException
  1039. * @throws ImagickException
  1040. * @throws InternalServerErrorException
  1041. * @throws UnauthorizedException
  1042. * @see https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload
  1043. */
  1044. function api_media_upload()
  1045. {
  1046. $a = DI::app();
  1047. if (api_user() === false) {
  1048. Logger::log('no user');
  1049. throw new ForbiddenException();
  1050. }
  1051. api_get_user($a);
  1052. if (empty($_FILES['media'])) {
  1053. // Output error
  1054. throw new BadRequestException("No media.");
  1055. }
  1056. $media = wall_upload_post($a, false);
  1057. if (!$media) {
  1058. // Output error
  1059. throw new InternalServerErrorException();
  1060. }
  1061. $returndata = [];
  1062. $returndata["media_id"] = $media["id"];
  1063. $returndata["media_id_string"] = (string)$media["id"];
  1064. $returndata["size"] = $media["size"];
  1065. $returndata["image"] = ["w" => $media["width"],
  1066. "h" => $media["height"],
  1067. "image_type" => $media["type"],
  1068. "friendica_preview_url" => $media["preview"]];
  1069. Logger::log("Media uploaded: " . print_r($returndata, true), Logger::DEBUG);
  1070. return ["media" => $returndata];
  1071. }
  1072. /// @TODO move to top of file or somewhere better
  1073. api_register_func('api/media/upload', 'api_media_upload', true, API_METHOD_POST);
  1074. /**
  1075. * Updates media meta data (picture descriptions)
  1076. *
  1077. * @param string $type Return type (atom, rss, xml, json)
  1078. *
  1079. * @return array|string
  1080. * @throws BadRequestException
  1081. * @throws ForbiddenException
  1082. * @throws ImagickException
  1083. * @throws InternalServerErrorException
  1084. * @throws TooManyRequestsException
  1085. * @throws UnauthorizedException
  1086. * @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update
  1087. *
  1088. * @todo Compare the corresponding Twitter function for correct return values
  1089. */
  1090. function api_media_metadata_create($type)
  1091. {
  1092. $a = DI::app();
  1093. if (api_user() === false) {
  1094. Logger::info('no user');
  1095. throw new ForbiddenException();
  1096. }
  1097. api_get_user($a);
  1098. $postdata = Network::postdata();
  1099. if (empty($postdata)) {
  1100. throw new BadRequestException("No post data");
  1101. }
  1102. $data = json_decode($postdata, true);
  1103. if (empty($data)) {
  1104. throw new BadRequestException("Invalid post data");
  1105. }
  1106. if (empty($data['media_id']) || empty($data['alt_text'])) {
  1107. throw new BadRequestException("Missing post data values");
  1108. }
  1109. if (empty($data['alt_text']['text'])) {
  1110. throw new BadRequestException("No alt text.");
  1111. }
  1112. Logger::info('Updating metadata', ['media_id' => $data['media_id']]);
  1113. $condition = ['id' => $data['media_id'], 'uid' => api_user()];
  1114. $photo = DBA::selectFirst('photo', ['resource-id'], $condition);
  1115. if (!DBA::isResult($photo)) {
  1116. throw new BadRequestException("Metadata not found.");
  1117. }
  1118. DBA::update('photo', ['desc' => $data['alt_text']['text']], ['resource-id' => $photo['resource-id']]);
  1119. }
  1120. api_register_func('api/media/metadata/create', 'api_media_metadata_create', true, API_METHOD_POST);
  1121. /**
  1122. * @param string $type Return format (atom, rss, xml, json)
  1123. * @param int $item_id
  1124. * @return string
  1125. * @throws Exception
  1126. */
  1127. function api_status_show($type, $item_id)
  1128. {
  1129. Logger::info(API_LOG_PREFIX . 'Start', ['action' => 'status_show', 'type' => $type, 'item_id' => $item_id]);
  1130. $status_info = [];
  1131. $item = api_get_item(['id' => $item_id]);
  1132. if (!empty($item)) {
  1133. $status_info = api_format_item($item, $type);
  1134. }
  1135. Logger::info(API_LOG_PREFIX . 'End', ['action' => 'get_status', 'status_info' => $status_info]);
  1136. return api_format_data('statuses', $type, ['status' => $status_info]);
  1137. }
  1138. /**
  1139. * Retrieves the last public status of the provided user info
  1140. *
  1141. * @param int $ownerId Public contact Id
  1142. * @param int $uid User Id
  1143. * @return array
  1144. * @throws Exception
  1145. */
  1146. function api_get_last_status($ownerId, $uid)
  1147. {
  1148. $condition = [
  1149. 'author-id'=> $ownerId,
  1150. 'uid' => $uid,
  1151. 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT],
  1152. 'private' => false
  1153. ];
  1154. $item = api_get_item($condition);
  1155. return $item;
  1156. }
  1157. /**
  1158. * Retrieves a single item record based on the provided condition and converts it for API use.
  1159. *
  1160. * @param array $condition Item table condition array
  1161. * @return array
  1162. * @throws Exception
  1163. */
  1164. function api_get_item(array $condition)
  1165. {
  1166. $item = Item::selectFirst(Item::DISPLAY_FIELDLIST, $condition, ['order' => ['id' => true]]);
  1167. return $item;
  1168. }
  1169. /**
  1170. * Returns extended information of a given user, specified by ID or screen name as per the required id parameter.
  1171. * The author's most recent status will be returned inline.
  1172. *
  1173. * @param string $type Return type (atom, rss, xml, json)
  1174. * @return array|string
  1175. * @throws BadRequestException
  1176. * @throws ImagickException
  1177. * @throws InternalServerErrorException
  1178. * @throws UnauthorizedException
  1179. * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-show
  1180. */
  1181. function api_users_show($type)
  1182. {
  1183. $a = Friendica\DI::app();
  1184. $user_info = api_get_user($a);
  1185. $item = api_get_last_status($user_info['pid'], $user_info['uid']);
  1186. if (!empty($item)) {
  1187. $user_info['status'] = api_format_item($item, $type);
  1188. }
  1189. // "uid" and "self" are only needed for some internal stuff, so remove it from here
  1190. unset($user_info['uid']);
  1191. unset($user_info['self']);
  1192. return api_format_data('user', $type, ['user' => $user_info]);
  1193. }
  1194. /// @TODO move to top of file or somewhere better
  1195. api_register_func('api/users/show', 'api_users_show');
  1196. api_register_func('api/externalprofile/show', 'api_users_show');
  1197. /**
  1198. * Search a public user account.
  1199. *
  1200. * @param string $type Return type (atom, rss, xml, json)
  1201. *
  1202. * @return array|string
  1203. * @throws BadRequestException
  1204. * @throws ImagickException
  1205. * @throws InternalServerErrorException
  1206. * @throws UnauthorizedException
  1207. * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-search
  1208. */
  1209. function api_users_search($type)
  1210. {
  1211. $a = DI::app();
  1212. $userlist = [];
  1213. if (!empty($_GET['q'])) {
  1214. $contacts = Contact::selectToArray(
  1215. ['id'],
  1216. [
  1217. '`uid` = 0 AND (`name` = ? OR `nick` = ? OR `url` = ? OR `addr` = ?)',
  1218. $_GET['q'],
  1219. $_GET['q'],
  1220. $_GET['q'],
  1221. $_GET['q'],
  1222. ]
  1223. );
  1224. if (DBA::isResult($contacts)) {
  1225. $k = 0;
  1226. foreach ($contacts as $contact) {
  1227. $user_info = api_get_user($a, $contact['id']);
  1228. if ($type == 'xml') {
  1229. $userlist[$k++ . ':user'] = $user_info;
  1230. } else {
  1231. $userlist[] = $user_info;
  1232. }
  1233. }
  1234. $userlist = ['users' => $userlist];
  1235. } else {
  1236. throw new NotFoundException('User ' . $_GET['q'] . ' not found.');
  1237. }
  1238. } else {
  1239. throw new BadRequestException('No search term specified.');
  1240. }
  1241. return api_format_data('users', $type, $userlist);
  1242. }
  1243. /// @TODO move to top of file or somewhere better
  1244. api_register_func('api/users/search', 'api_users_search');
  1245. /**
  1246. * Return user objects
  1247. *
  1248. * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-lookup
  1249. *
  1250. * @param string $type Return format: json or xml
  1251. *
  1252. * @return array|string
  1253. * @throws BadRequestException
  1254. * @throws ImagickException
  1255. * @throws InternalServerErrorException
  1256. * @throws NotFoundException if the results are empty.
  1257. * @throws UnauthorizedException
  1258. */
  1259. function api_users_lookup($type)
  1260. {
  1261. $users = [];
  1262. if (!empty($_REQUEST['user_id'])) {
  1263. foreach (explode(',', $_REQUEST['user_id']) as $id) {
  1264. if (!empty($id)) {
  1265. $users[] = api_get_user(DI::app(), $id);
  1266. }
  1267. }
  1268. }
  1269. if (empty($users)) {
  1270. throw new NotFoundException;
  1271. }
  1272. return api_format_data("users", $type, ['users' => $users]);
  1273. }
  1274. /// @TODO move to top of file or somewhere better
  1275. api_register_func('api/users/lookup', 'api_users_lookup', true);
  1276. /**
  1277. * Returns statuses that match a specified query.
  1278. *
  1279. * @see https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets
  1280. *
  1281. * @param string $type Return format: json, xml, atom, rss
  1282. *
  1283. * @return array|string
  1284. * @throws BadRequestException if the "q" parameter is missing.
  1285. * @throws ForbiddenException
  1286. * @throws ImagickException
  1287. * @throws InternalServerErrorException
  1288. * @throws UnauthorizedException
  1289. */
  1290. function api_search($type)
  1291. {
  1292. $a = DI::app();
  1293. $user_info = api_get_user($a);
  1294. if (api_user() === false || $user_info === false) {
  1295. throw new ForbiddenException();
  1296. }
  1297. if (empty($_REQUEST['q'])) {
  1298. throw new BadRequestException('q parameter is required.');
  1299. }
  1300. $searchTerm = trim(rawurldecode($_REQUEST['q']));
  1301. $data = [];
  1302. $data['status'] = [];
  1303. $count = 15;
  1304. $exclude_replies = !empty($_REQUEST['exclude_replies']);
  1305. if (!empty($_REQUEST['rpp'])) {
  1306. $count = $_REQUEST['rpp'];
  1307. } elseif (!empty($_REQUEST['count'])) {
  1308. $count = $_REQUEST['count'];
  1309. }
  1310. $since_id = $_REQUEST['since_id'] ?? 0;
  1311. $max_id = $_REQUEST['max_id'] ?? 0;
  1312. $page = $_REQUEST['page'] ?? 1;
  1313. $start = max(0, ($page - 1) * $count);
  1314. $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
  1315. if (preg_match('/^#(\w+)$/', $searchTerm, $matches) === 1 && isset($matches[1])) {
  1316. $searchTerm = $matches[1];
  1317. $condition = ["`oid` > ?
  1318. AND (`uid` = 0 OR (`uid` = ? AND NOT `global`))
  1319. AND `otype` = ? AND `type` = ? AND `term` = ?",
  1320. $since_id, local_user(), TERM_OBJ_POST, TERM_HASHTAG, $searchTerm];
  1321. if ($max_id > 0) {
  1322. $condition[0] .= ' AND `oid` <= ?';
  1323. $condition[] = $max_id;
  1324. }
  1325. $terms = DBA::select('term', ['oid'], $condition, []);
  1326. $itemIds = [];
  1327. while ($term = DBA::fetch($terms)) {
  1328. $itemIds[] = $term['oid'];
  1329. }
  1330. DBA::close($terms);
  1331. if (empty($itemIds)) {
  1332. return api_format_data('statuses', $type, $data);
  1333. }
  1334. $preCondition = ['`id` IN (' . implode(', ', $itemIds) . ')'];
  1335. if ($exclude_replies) {
  1336. $preCondition[] = '`id` = `parent`';
  1337. }
  1338. $condition = [implode(' AND ', $preCondition)];
  1339. } else {
  1340. $condition = ["`id` > ?
  1341. " . ($exclude_replies ? " AND `id` = `parent` " : ' ') . "
  1342. AND (`uid` = 0 OR (`uid` = ? AND NOT `global`))
  1343. AND `body` LIKE CONCAT('%',?,'%')",
  1344. $since_id, api_user(), $_REQUEST['q']];
  1345. if ($max_id > 0) {
  1346. $condition[0] .= ' AND `id` <= ?';
  1347. $condition[] = $max_id;
  1348. }
  1349. }
  1350. $statuses = [];
  1351. if (parse_url($searchTerm, PHP_URL_SCHEME) != '') {
  1352. $id = Item::fetchByLink($searchTerm, api_user());
  1353. if (!$id) {
  1354. // Public post
  1355. $id = Item::fetchByLink($searchTerm);
  1356. }
  1357. if (!empty($id)) {
  1358. $statuses = Item::select([], ['id' => $id]);
  1359. }
  1360. }
  1361. $statuses = $statuses ?: Item::selectForUser(api_user(), [], $condition, $params);
  1362. $data['status'] = api_format_items(Item::inArray($statuses), $user_info);
  1363. bindComments($data['status']);
  1364. return api_format_data('statuses', $type, $data);
  1365. }
  1366. /// @TODO move to top of file or somewhere better
  1367. api_register_func('api/search/tweets', 'api_search', true);
  1368. api_register_func('api/search', 'api_search', true);
  1369. /**
  1370. * Returns the most recent statuses posted by the user and the users they follow.
  1371. *
  1372. * @see https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-home_timeline
  1373. *
  1374. * @param string $type Return type (atom, rss, xml, json)
  1375. *
  1376. * @return array|string
  1377. * @throws BadRequestException
  1378. * @throws ForbiddenException
  1379. * @throws ImagickException
  1380. * @throws InternalServerErrorException
  1381. * @throws UnauthorizedException
  1382. * @todo Optional parameters
  1383. * @todo Add reply info
  1384. */
  1385. function api_statuses_home_timeline($type)
  1386. {
  1387. $a = DI::app();
  1388. $user_info = api_get_user($a);
  1389. if (api_user() === false || $user_info === false) {
  1390. throw new ForbiddenException();
  1391. }
  1392. unset($_REQUEST["user_id"]);
  1393. unset($_GET["user_id"]);
  1394. unset($_REQUEST["screen_name"]);
  1395. unset($_GET["screen_name"]);
  1396. // get last network messages
  1397. // params
  1398. $count = $_REQUEST['count'] ?? 20;
  1399. $page = $_REQUEST['page']?? 0;
  1400. $since_id = $_REQUEST['since_id'] ?? 0;
  1401. $max_id = $_REQUEST['max_id'] ?? 0;
  1402. $exclude_replies = !empty($_REQUEST['exclude_replies']);
  1403. $conversation_id = $_REQUEST['conversation_id'] ?? 0;
  1404. $start = max(0, ($page - 1) * $count);
  1405. $condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `item`.`id` > ?",
  1406. api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
  1407. if ($max_id > 0) {
  1408. $condition[0] .= " AND `item`.`id` <= ?";
  1409. $condition[] = $max_id;
  1410. }
  1411. if ($exclude_replies) {
  1412. $condition[0] .= ' AND `item`.`parent` = `item`.`id`';
  1413. }
  1414. if ($conversation_id > 0) {
  1415. $condition[0] .= " AND `item`.`parent` = ?";
  1416. $condition[] = $conversation_id;
  1417. }
  1418. $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
  1419. $statuses = Item::selectForUser(api_user(), [], $condition, $params);
  1420. $items = Item::inArray($statuses);
  1421. $ret = api_format_items($items, $user_info, false, $type);
  1422. // Set all posts from the query above to seen
  1423. $idarray = [];
  1424. foreach ($items as $item) {
  1425. $idarray[] = intval($item["id"]);
  1426. }
  1427. if (!empty($idarray)) {
  1428. $unseen = Item::exists(['unseen' => true, 'id' => $idarray]);
  1429. if ($unseen) {
  1430. Item::update(['unseen' => false], ['unseen' => true, 'id' => $idarray]);
  1431. }
  1432. }
  1433. bindComments($ret);
  1434. $data = ['status' => $ret];
  1435. switch ($type) {
  1436. case "atom":
  1437. break;
  1438. case "rss":
  1439. $data = api_rss_extra($a, $data, $user_info);
  1440. break;
  1441. }
  1442. return api_format_data("statuses", $type, $data);
  1443. }
  1444. /// @TODO move to top of file or somewhere better
  1445. api_register_func('api/statuses/home_timeline', 'api_statuses_home_timeline', true);
  1446. api_register_func('api/statuses/friends_timeline', 'api_statuses_home_timeline', true);
  1447. /**
  1448. * Returns the most recent statuses from public users.
  1449. *
  1450. * @param string $type Return type (atom, rss, xml, json)
  1451. *
  1452. * @return array|string
  1453. * @throws BadRequestException
  1454. * @throws ForbiddenException
  1455. * @throws ImagickException
  1456. * @throws InternalServerErrorException
  1457. * @throws UnauthorizedException
  1458. */
  1459. function api_statuses_public_timeline($type)
  1460. {
  1461. $a = DI::app();
  1462. $user_info = api_get_user($a);
  1463. if (api_user() === false || $user_info === false) {
  1464. throw new ForbiddenException();
  1465. }
  1466. // get last network messages
  1467. // params
  1468. $count = $_REQUEST['count'] ?? 20;
  1469. $page = $_REQUEST['page'] ?? 1;
  1470. $since_id = $_REQUEST['since_id'] ?? 0;
  1471. $max_id = $_REQUEST['max_id'] ?? 0;
  1472. $exclude_replies = (!empty($_REQUEST['exclude_replies']) ? 1 : 0);
  1473. $conversation_id = $_REQUEST['conversation_id'] ?? 0;
  1474. $start = max(0, ($page - 1) * $count);
  1475. if ($exclude_replies && !$conversation_id) {
  1476. $condition = ["`gravity` IN (?, ?) AND `iid` > ? AND NOT `private` AND `wall` AND NOT `user`.`hidewall` AND NOT `author`.`hidden`",
  1477. GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
  1478. if ($max_id > 0) {
  1479. $condition[0] .= " AND `thread`.`iid` <= ?";
  1480. $condition[] = $max_id;
  1481. }
  1482. $params = ['order' => ['iid' => true], 'limit' => [$start, $count]];
  1483. $statuses = Item::selectThreadForUser(api_user(), Item::DISPLAY_FIELDLIST, $condition, $params);
  1484. $r = Item::inArray($statuses);
  1485. } else {
  1486. $condition = ["`gravity` IN (?, ?) AND `id` > ? AND NOT `private` AND `wall` AND NOT `user`.`hidewall` AND `item`.`origin` AND NOT `author`.`hidden`",
  1487. GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
  1488. if ($max_id > 0) {
  1489. $condition[0] .= " AND `item`.`id` <= ?";
  1490. $condition[] = $max_id;
  1491. }
  1492. if ($conversation_id > 0) {
  1493. $condition[0] .= " AND `item`.`parent` = ?";
  1494. $condition[] = $conversation_id;
  1495. }
  1496. $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
  1497. $statuses = Item::selectForUser(api_user(), [], $condition, $params);
  1498. $r = Item::inArray($statuses);
  1499. }
  1500. $ret = api_format_items($r, $user_info, false, $type);
  1501. bindComments($ret);
  1502. $data = ['status' => $ret];
  1503. switch ($type) {
  1504. case "atom":
  1505. break;
  1506. case "rss":
  1507. $data = api_rss_extra($a, $data, $user_info);
  1508. break;
  1509. }
  1510. return api_format_data("statuses", $type, $data);
  1511. }
  1512. /// @TODO move to top of file or somewhere better
  1513. api_register_func('api/statuses/public_timeline', 'api_statuses_public_timeline', true);
  1514. /**
  1515. * Returns the most recent statuses posted by users this node knows about.
  1516. *
  1517. * @brief Returns the list of public federated posts this node knows about
  1518. *
  1519. * @param string $type Return format: json, xml, atom, rss
  1520. * @return array|string
  1521. * @throws BadRequestException
  1522. * @throws ForbiddenException
  1523. * @throws ImagickException
  1524. * @throws InternalServerErrorException
  1525. * @throws UnauthorizedException
  1526. */
  1527. function api_statuses_networkpublic_timeline($type)
  1528. {
  1529. $a = DI::app();
  1530. $user_info = api_get_user($a);
  1531. if (api_user() === false || $user_info === false) {
  1532. throw new ForbiddenException();
  1533. }
  1534. $since_id = $_REQUEST['since_id'] ?? 0;
  1535. $max_id = $_REQUEST['max_id'] ?? 0;
  1536. // pagination
  1537. $count = $_REQUEST['count'] ?? 20;
  1538. $page = $_REQUEST['page'] ?? 1;
  1539. $start = max(0, ($page - 1) * $count);
  1540. $condition = ["`uid` = 0 AND `gravity` IN (?, ?) AND `thread`.`iid` > ? AND NOT `private`",
  1541. GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
  1542. if ($max_id > 0) {
  1543. $condition[0] .= " AND `thread`.`iid` <= ?";
  1544. $condition[] = $max_id;
  1545. }
  1546. $params = ['order' => ['iid' => true], 'limit' => [$start, $count]];
  1547. $statuses = Item::selectThreadForUser(api_user(), Item::DISPLAY_FIELDLIST, $condition, $params);
  1548. $ret = api_format_items(Item::inArray($statuses), $user_info, false, $type);
  1549. bindComments($ret);
  1550. $data = ['status' => $ret];
  1551. switch ($type) {
  1552. case "atom":
  1553. break;
  1554. case "rss":
  1555. $data = api_rss_extra($a, $data, $user_info);
  1556. break;
  1557. }
  1558. return api_format_data("statuses", $type, $data);
  1559. }
  1560. /// @TODO move to top of file or somewhere better
  1561. api_register_func('api/statuses/networkpublic_timeline', 'api_statuses_networkpublic_timeline', true);
  1562. /**
  1563. * Returns a single status.
  1564. *
  1565. * @param string $type Return type (atom, rss, xml, json)
  1566. *
  1567. * @return array|string
  1568. * @throws BadRequestException
  1569. * @throws ForbiddenException
  1570. * @throws ImagickException
  1571. * @throws InternalServerErrorException
  1572. * @throws UnauthorizedException
  1573. * @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-show-id
  1574. */
  1575. function api_statuses_show($type)
  1576. {
  1577. $a = DI::app();
  1578. $user_info = api_get_user($a);
  1579. if (api_user() === false || $user_info === false) {
  1580. throw new ForbiddenException();
  1581. }
  1582. // params
  1583. $id = intval($a->argv[3] ?? 0);
  1584. if ($id == 0) {
  1585. $id = intval($_REQUEST['id'] ?? 0);
  1586. }
  1587. // Hotot workaround
  1588. if ($id == 0) {
  1589. $id = intval($a->argv[4] ?? 0);
  1590. }
  1591. Logger::log('API: api_statuses_show: ' . $id);
  1592. $conversation = !empty($_REQUEST['conversation']);
  1593. // try to fetch the item for the local user - or the public item, if there is no local one
  1594. $uri_item = Item::selectFirst(['uri'], ['id' => $id]);
  1595. if (!DBA::isResult($uri_item)) {
  1596. throw new BadRequestException("There is no status with this id.");
  1597. }
  1598. $item = Item::selectFirst(['id'], ['uri' => $uri_item['uri'], 'uid' => [0, api_user()]], ['order' => ['uid' => true]]);
  1599. if (!DBA::isResult($item)) {
  1600. throw new BadRequestException("There is no status with this id.");
  1601. }
  1602. $id = $item['id'];
  1603. if ($conversation) {
  1604. $condition = ['parent' => $id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]];
  1605. $params = ['order' => ['id' => true]];
  1606. } else {
  1607. $condition = ['id' => $id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]];
  1608. $params = [];
  1609. }
  1610. $statuses = Item::selectForUser(api_user(), [], $condition, $params);
  1611. /// @TODO How about copying this to above methods which don't check $r ?
  1612. if (!DBA::isResult($statuses)) {
  1613. throw new BadRequestException("There is no status with this id.");
  1614. }
  1615. $ret = api_format_items(Item::inArray($statuses), $user_info, false, $type);
  1616. if ($conversation) {
  1617. $data = ['status' => $ret];
  1618. return api_format_data("statuses", $type, $data);
  1619. } else {
  1620. $data = ['status' => $ret[0]];
  1621. return api_format_data("status", $type, $data);
  1622. }
  1623. }
  1624. /// @TODO move to top of file or somewhere better
  1625. api_register_func('api/statuses/show', 'api_statuses_show', true);
  1626. /**
  1627. *
  1628. * @param string $type Return type (atom, rss, xml, json)
  1629. *
  1630. * @return array|string
  1631. * @throws BadRequestException
  1632. * @throws ForbiddenException
  1633. * @throws ImagickException
  1634. * @throws InternalServerErrorException
  1635. * @throws UnauthorizedException
  1636. * @todo nothing to say?
  1637. */
  1638. function api_conversation_show($type)
  1639. {
  1640. $a = DI::app();
  1641. $user_info = api_get_user($a);
  1642. if (api_user() === false || $user_info === false) {
  1643. throw new ForbiddenException();
  1644. }
  1645. // params
  1646. $id = intval($a->argv[3] ?? 0);
  1647. $since_id = intval($_REQUEST['since_id'] ?? 0);
  1648. $max_id = intval($_REQUEST['max_id'] ?? 0);
  1649. $count = intval($_REQUEST['count'] ?? 20);
  1650. $page = intval($_REQUEST['page'] ?? 1);
  1651. $start = max(0, ($page - 1) * $count);
  1652. if ($id == 0) {
  1653. $id = intval($_REQUEST['id'] ?? 0);
  1654. }
  1655. // Hotot workaround
  1656. if ($id == 0) {
  1657. $id = intval($a->argv[4] ?? 0);
  1658. }
  1659. Logger::info(API_LOG_PREFIX . '{subaction}', ['module' => 'api', 'action' => 'conversation', 'subaction' => 'show', 'id' => $id]);
  1660. // try to fetch the item for the local user - or the public item, if there is no local one
  1661. $item = Item::selectFirst(['parent-uri'], ['id' => $id]);
  1662. if (!DBA::isResult($item)) {
  1663. throw new BadRequestException("There is no status with this id.");
  1664. }
  1665. $parent = Item::selectFirst(['id'], ['uri' => $item['parent-uri'], 'uid' => [0, api_user()]], ['order' => ['uid' => true]]);
  1666. if (!DBA::isResult($parent)) {
  1667. throw new BadRequestException("There is no status with this id.");
  1668. }
  1669. $id = $parent['id'];
  1670. $condition = ["`parent` = ? AND `uid` IN (0, ?) AND `gravity` IN (?, ?) AND `item`.`id` > ?",
  1671. $id, api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
  1672. if ($max_id > 0) {
  1673. $condition[0] .= " AND `item`.`id` <= ?";
  1674. $condition[] = $max_id;
  1675. }
  1676. $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
  1677. $statuses = Item::selectForUser(api_user(), [], $condition, $params);
  1678. if (!DBA::isResult($statuses)) {
  1679. throw new BadRequestException("There is no status with id $id.");
  1680. }
  1681. $ret = api_format_items(Item::inArray($statuses), $user_info, false, $type);
  1682. $data = ['status' => $ret];
  1683. return api_format_data("statuses", $type, $data);
  1684. }
  1685. /// @TODO move to top of file or somewhere better
  1686. api_register_func('api/conversation/show', 'api_conversation_show', true);
  1687. api_register_func('api/statusnet/conversation', 'api_conversation_show', true);
  1688. /**
  1689. * Repeats a status.
  1690. *
  1691. * @param string $type Return type (atom, rss, xml, json)
  1692. *
  1693. * @return array|string
  1694. * @throws BadRequestException
  1695. * @throws ForbiddenException
  1696. * @throws ImagickException
  1697. * @throws InternalServerErrorException
  1698. * @throws UnauthorizedException
  1699. * @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-retweet-id
  1700. */
  1701. function api_statuses_repeat($type)
  1702. {
  1703. global $called_api;
  1704. $a = DI::app();
  1705. if (api_user() === false) {
  1706. throw new ForbiddenException();
  1707. }
  1708. api_get_user($a);
  1709. // params
  1710. $id = intval($a->argv[3] ?? 0);
  1711. if ($id == 0) {
  1712. $id = intval($_REQUEST['id'] ?? 0);
  1713. }
  1714. // Hotot workaround
  1715. if ($id == 0) {
  1716. $id = intval($a->argv[4] ?? 0);
  1717. }
  1718. Logger::log('API: api_statuses_repeat: '.$id);
  1719. $fields = ['body', 'title', 'attach', 'tag', 'author-name', 'author-link', 'author-avatar', 'guid', 'created', 'plink'];
  1720. $item = Item::selectFirst($fields, ['id' => $id, 'private' => false]);
  1721. if (DBA::isResult($item) && $item['body'] != "") {
  1722. if (strpos($item['body'], "[/share]") !== false) {
  1723. $pos = strpos($item['body'], "[share");
  1724. $post = substr($item['body'], $pos);
  1725. } else {
  1726. $post = share_header($item['author-name'], $item['author-link'], $item['author-avatar'], $item['guid'], $item['created'], $item['plink']);
  1727. if (!empty($item['title'])) {
  1728. $post .= '[h3]' . $item['title'] . "[/h3]\n";
  1729. }
  1730. $post .= $item['body'];
  1731. $post .= "[/share]";
  1732. }
  1733. $_REQUEST['body'] = $post;
  1734. $_REQUEST['tag'] = $item['tag'];
  1735. $_REQUEST['attach'] = $item['attach'];
  1736. $_REQUEST['profile_uid'] = api_user();
  1737. $_REQUEST['api_source'] = true;
  1738. if (empty($_REQUEST['source'])) {
  1739. $_REQUEST["source"] = api_source();
  1740. }
  1741. $item_id = item_post($a);
  1742. } else {
  1743. throw new ForbiddenException();
  1744. }
  1745. // output the post that we just posted.
  1746. $called_api = [];
  1747. return api_status_show($type, $item_id);
  1748. }
  1749. /// @TODO move to top of file or somewhere better
  1750. api_register_func('api/statuses/retweet', 'api_statuses_repeat', true, API_METHOD_POST);
  1751. /**
  1752. * Destroys a specific status.
  1753. *
  1754. * @param string $type Return type (atom, rss, xml, json)
  1755. *
  1756. * @return array|string
  1757. * @throws BadRequestException
  1758. * @throws ForbiddenException
  1759. * @throws ImagickException
  1760. * @throws InternalServerErrorException
  1761. * @throws UnauthorizedException
  1762. * @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-destroy-id
  1763. */
  1764. function api_statuses_destroy($type)
  1765. {
  1766. $a = DI::app();
  1767. if (api_user() === false) {
  1768. throw new ForbiddenException();
  1769. }
  1770. api_get_user($a);
  1771. // params
  1772. $id = intval($a->argv[3] ?? 0);
  1773. if ($id == 0) {
  1774. $id = intval($_REQUEST['id'] ?? 0);
  1775. }
  1776. // Hotot workaround
  1777. if ($id == 0) {
  1778. $id = intval($a->argv[4] ?? 0);
  1779. }
  1780. Logger::log('API: api_statuses_destroy: '.$id);
  1781. $ret = api_statuses_show($type);
  1782. Item::deleteForUser(['id' => $id], api_user());
  1783. return $ret;
  1784. }
  1785. /// @TODO move to top of file or somewhere better
  1786. api_register_func('api/statuses/destroy', 'api_statuses_destroy', true, API_METHOD_DELETE);
  1787. /**
  1788. * Returns the most recent mentions.
  1789. *
  1790. * @param string $type Return type (atom, rss, xml, json)
  1791. *
  1792. * @return array|string
  1793. * @throws BadRequestException
  1794. * @throws ForbiddenException
  1795. * @throws ImagickException
  1796. * @throws InternalServerErrorException
  1797. * @throws UnauthorizedException
  1798. * @see http://developer.twitter.com/doc/get/statuses/mentions
  1799. */
  1800. function api_statuses_mentions($type)
  1801. {
  1802. $a = DI::app();
  1803. $user_info = api_get_user($a);
  1804. if (api_user() === false || $user_info === false) {
  1805. throw new ForbiddenException();
  1806. }
  1807. unset($_REQUEST["user_id"]);
  1808. unset($_GET["user_id"]);
  1809. unset($_REQUEST["screen_name"]);
  1810. unset($_GET["screen_name"]);
  1811. // get last network messages
  1812. // params
  1813. $since_id = $_REQUEST['since_id'] ?? 0;
  1814. $max_id = $_REQUEST['max_id'] ?? 0;
  1815. $count = $_REQUEST['count'] ?? 20;
  1816. $page = $_REQUEST['page'] ?? 1;
  1817. $start = max(0, ($page - 1) * $count);
  1818. $query = "SELECT `item`.`id` FROM `user-item`
  1819. INNER JOIN `item` ON `item`.`id` = `user-item`.`iid` AND `item`.`gravity` IN (?, ?)
  1820. WHERE (`user-item`.`hidden` IS NULL OR NOT `user-item`.`hidden`) AND
  1821. `user-item`.`uid` = ? AND `user-item`.`notification-type` & ? != 0
  1822. AND `user-item`.`iid` > ?";
  1823. $condition = [GRAVITY_PARENT, GRAVITY_COMMENT, api_user(),
  1824. UserItem::NOTIF_EXPLICIT_TAGGED | UserItem::NOTIF_IMPLICIT_TAGGED |
  1825. UserItem::NOTIF_THREAD_COMMENT | UserItem::NOTIF_DIRECT_COMMENT |
  1826. UserItem::NOTIF_DIRECT_THREAD_COMMENT,
  1827. $since_id];
  1828. if ($max_id > 0) {
  1829. $query .= " AND `item`.`id` <= ?";
  1830. $condition[] = $max_id;
  1831. }
  1832. $query .= " ORDER BY `user-item`.`iid` DESC LIMIT ?, ?";
  1833. $condition[] = $start;
  1834. $condition[] = $count;
  1835. $useritems = DBA::p($query, $condition);
  1836. $itemids = [];
  1837. while ($useritem = DBA::fetch($useritems)) {
  1838. $itemids[] = $useritem['id'];
  1839. }
  1840. DBA::close($useritems);
  1841. $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
  1842. $statuses = Item::selectForUser(api_user(), [], ['id' => $itemids], $params);
  1843. $ret = api_format_items(Item::inArray($statuses), $user_info, false, $type);
  1844. $data = ['status' => $ret];
  1845. switch ($type) {
  1846. case "atom":
  1847. break;
  1848. case "rss":
  1849. $data = api_rss_extra($a, $data, $user_info);
  1850. break;
  1851. }
  1852. return api_format_data("statuses", $type, $data);
  1853. }
  1854. /// @TODO move to top of file or somewhere better
  1855. api_register_func('api/statuses/mentions', 'api_statuses_mentions', true);
  1856. api_register_func('api/statuses/replies', 'api_statuses_mentions', true);
  1857. /**
  1858. * Returns the most recent statuses posted by the user.
  1859. *
  1860. * @brief Returns a user's public timeline
  1861. *
  1862. * @param string $type Either "json" or "xml"
  1863. * @return string|array
  1864. * @throws BadRequestException
  1865. * @throws ForbiddenException
  1866. * @throws ImagickException
  1867. * @throws InternalServerErrorException
  1868. * @throws UnauthorizedException
  1869. * @see https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-user_timeline
  1870. */
  1871. function api_statuses_user_timeline($type)
  1872. {
  1873. $a = DI::app();
  1874. $user_info = api_get_user($a);
  1875. if (api_user() === false || $user_info === false) {
  1876. throw new ForbiddenException();
  1877. }
  1878. Logger::log(
  1879. "api_statuses_user_timeline: api_user: ". api_user() .
  1880. "\nuser_info: ".print_r($user_info, true) .
  1881. "\n_REQUEST: ".print_r($_REQUEST, true),
  1882. Logger::DEBUG
  1883. );
  1884. $since_id = $_REQUEST['since_id'] ?? 0;
  1885. $max_id = $_REQUEST['max_id'] ?? 0;
  1886. $exclude_replies = !empty($_REQUEST['exclude_replies']);
  1887. $conversation_id = $_REQUEST['conversation_id'] ?? 0;
  1888. // pagination
  1889. $count = $_REQUEST['count'] ?? 20;
  1890. $page = $_REQUEST['page'] ?? 1;
  1891. $start = max(0, ($page - 1) * $count);
  1892. $condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `item`.`id` > ? AND `item`.`contact-id` = ?",
  1893. api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, $user_info['cid']];
  1894. if ($user_info['self'] == 1) {
  1895. $condition[0] .= ' AND `item`.`wall` ';
  1896. }
  1897. if ($exclude_replies) {
  1898. $condition[0] .= ' AND `item`.`parent` = `item`.`id`';
  1899. }
  1900. if ($conversation_id > 0) {
  1901. $condition[0] .= " AND `item`.`parent` = ?";
  1902. $condition[] = $conversation_id;
  1903. }
  1904. if ($max_id > 0) {
  1905. $condition[0] .= " AND `item`.`id` <= ?";
  1906. $condition[] = $max_id;
  1907. }
  1908. $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
  1909. $statuses = Item::selectForUser(api_user(), [], $condition, $params);
  1910. $ret = api_format_items(Item::inArray($statuses), $user_info, true, $type);
  1911. bindComments($ret);
  1912. $data = ['status' => $ret];
  1913. switch ($type) {
  1914. case "atom":
  1915. break;
  1916. case "rss":
  1917. $data = api_rss_extra($a, $data, $user_info);
  1918. break;
  1919. }
  1920. return api_format_data("statuses", $type, $data);
  1921. }
  1922. /// @TODO move to top of file or somewhere better
  1923. api_register_func('api/statuses/user_timeline', 'api_statuses_user_timeline', true);
  1924. /**
  1925. * Star/unstar an item.
  1926. * param: id : id of the item
  1927. *
  1928. * @param string $type Return type (atom, rss, xml, json)
  1929. *
  1930. * @return array|string
  1931. * @throws BadRequestException
  1932. * @throws ForbiddenException
  1933. * @throws ImagickException
  1934. * @throws InternalServerErrorException
  1935. * @throws UnauthorizedException
  1936. * @see https://web.archive.org/web/20131019055350/https://dev.twitter.com/docs/api/1/post/favorites/create/%3Aid
  1937. */
  1938. function api_favorites_create_destroy($type)
  1939. {
  1940. $a = DI::app();
  1941. if (api_user() === false) {
  1942. throw new ForbiddenException();
  1943. }
  1944. // for versioned api.
  1945. /// @TODO We need a better global soluton
  1946. $action_argv_id = 2;
  1947. if (count($a->argv) > 1 && $a->argv[1] == "1.1") {
  1948. $action_argv_id = 3;
  1949. }
  1950. if ($a->argc <= $action_argv_id) {
  1951. throw new BadRequestException("Invalid request.");
  1952. }
  1953. $action = str_replace("." . $type, "", $a->argv[$action_argv_id]);
  1954. if ($a->argc == $action_argv_id + 2) {
  1955. $itemid = intval($a->argv[$action_argv_id + 1] ?? 0);
  1956. } else {
  1957. $itemid = intval($_REQUEST['id'] ?? 0);
  1958. }
  1959. $item = Item::selectFirstForUser(api_user(), [], ['id' => $itemid, 'uid' => api_user()]);
  1960. if (!DBA::isResult($item)) {
  1961. throw new BadRequestException("Invalid item.");
  1962. }
  1963. switch ($action) {
  1964. case "create":
  1965. $item['starred'] = 1;
  1966. break;
  1967. case "destroy":
  1968. $item['starred'] = 0;
  1969. break;
  1970. default:
  1971. throw new BadRequestException("Invalid action ".$action);
  1972. }
  1973. $r = Item::update(['starred' => $item['starred']], ['id' => $itemid]);
  1974. if ($r === false) {
  1975. throw new InternalServerErrorException("DB error");
  1976. }
  1977. $user_info = api_get_user($a);
  1978. $rets = api_format_items([$item], $user_info, false, $type);
  1979. $ret = $rets[0];
  1980. $data = ['status' => $ret];
  1981. switch ($type) {
  1982. case "atom":
  1983. break;
  1984. case "rss":
  1985. $data = api_rss_extra($a, $data, $user_info);
  1986. break;
  1987. }
  1988. return api_format_data("status", $type, $data);
  1989. }
  1990. /// @TODO move to top of file or somewhere better
  1991. api_register_func('api/favorites/create', 'api_favorites_create_destroy', true, API_METHOD_POST);
  1992. api_register_func('api/favorites/destroy', 'api_favorites_create_destroy', true, API_METHOD_DELETE);
  1993. /**
  1994. * Returns the most recent favorite statuses.
  1995. *
  1996. * @param string $type Return type (atom, rss, xml, json)
  1997. *
  1998. * @return string|array
  1999. * @throws BadRequestException
  2000. * @throws ForbiddenException
  2001. * @throws ImagickException
  2002. * @throws InternalServerErrorException
  2003. * @throws UnauthorizedException
  2004. */
  2005. function api_favorites($type)
  2006. {
  2007. global $called_api;
  2008. $a = DI::app();
  2009. $user_info = api_get_user($a);
  2010. if (api_user() === false || $user_info === false) {
  2011. throw new ForbiddenException();
  2012. }
  2013. $called_api = [];
  2014. // in friendica starred item are private
  2015. // return favorites only for self
  2016. Logger::info(API_LOG_PREFIX . 'for {self}', ['module' => 'api', 'action' => 'favorites', 'self' => $user_info['self']]);
  2017. if ($user_info['self'] == 0) {
  2018. $ret = [];
  2019. } else {
  2020. // params
  2021. $since_id = $_REQUEST['since_id'] ?? 0;
  2022. $max_id = $_REQUEST['max_id'] ?? 0;
  2023. $count = $_GET['count'] ?? 20;
  2024. $page = $_REQUEST['page'] ?? 1;
  2025. $start = max(0, ($page - 1) * $count);
  2026. $condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `id` > ? AND `starred`",
  2027. api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
  2028. $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
  2029. if ($max_id > 0) {
  2030. $condition[0] .= " AND `item`.`id` <= ?";
  2031. $condition[] = $max_id;
  2032. }
  2033. $statuses = Item::selectForUser(api_user(), [], $condition, $params);
  2034. $ret = api_format_items(Item::inArray($statuses), $user_info, false, $type);
  2035. }
  2036. bindComments($ret);
  2037. $data = ['status' => $ret];
  2038. switch ($type) {
  2039. case "atom":
  2040. break;
  2041. case "rss":
  2042. $data = api_rss_extra($a, $data, $user_info);
  2043. break;
  2044. }
  2045. return api_format_data("statuses", $type, $data);
  2046. }
  2047. /// @TODO move to top of file or somewhere better
  2048. api_register_func('api/favorites', 'api_favorites', true);
  2049. /**
  2050. *
  2051. * @param array $item
  2052. * @param array $recipient
  2053. * @param array $sender
  2054. *
  2055. * @return array
  2056. * @throws InternalServerErrorException
  2057. */
  2058. function api_format_messages($item, $recipient, $sender)
  2059. {
  2060. // standard meta information
  2061. $ret = [
  2062. 'id' => $item['id'],
  2063. 'sender_id' => $sender['id'],
  2064. 'text' => "",
  2065. 'recipient_id' => $recipient['id'],
  2066. 'created_at' => api_date($item['created'] ?? DateTimeFormat::utcNow()),
  2067. 'sender_screen_name' => $sender['screen_name'],
  2068. 'recipient_screen_name' => $recipient['screen_name'],
  2069. 'sender' => $sender,
  2070. 'recipient' => $recipient,
  2071. 'title' => "",
  2072. 'friendica_seen' => $item['seen'] ?? 0,
  2073. 'friendica_parent_uri' => $item['parent-uri'] ?? '',
  2074. ];
  2075. // "uid" and "self" are only needed for some internal stuff, so remove it from here
  2076. if (isset($ret['sender']['uid'])) {
  2077. unset($ret['sender']['uid']);
  2078. }
  2079. if (isset($ret['sender']['self'])) {
  2080. unset($ret['sender']['self']);
  2081. }
  2082. if (isset($ret['recipient']['uid'])) {
  2083. unset($ret['recipient']['uid']);
  2084. }
  2085. if (isset($ret['recipient']['self'])) {
  2086. unset($ret['recipient']['self']);
  2087. }
  2088. //don't send title to regular StatusNET requests to avoid confusing these apps
  2089. if (!empty($_GET['getText'])) {
  2090. $ret['title'] = $item['title'];
  2091. if ($_GET['getText'] == 'html') {
  2092. $ret['text'] = BBCode::convert($item['body'], false);
  2093. } elseif ($_GET['getText'] == 'plain') {
  2094. $ret['text'] = trim(HTML::toPlaintext(BBCode::convert(api_clean_plain_items($item['body']), false, 2, true), 0));
  2095. }
  2096. } else {
  2097. $ret['text'] = $item['title'] . "\n" . HTML::toPlaintext(BBCode::convert(api_clean_plain_items($item['body']), false, 2, true), 0);
  2098. }
  2099. if (!empty($_GET['getUserObjects']) && $_GET['getUserObjects'] == 'false') {
  2100. unset($ret['sender']);
  2101. unset($ret['recipient']);
  2102. }
  2103. return $ret;
  2104. }
  2105. /**
  2106. *
  2107. * @param array $item
  2108. *
  2109. * @return array
  2110. * @throws InternalServerErrorException
  2111. */
  2112. function api_convert_item($item)
  2113. {
  2114. $body = $item['body'];
  2115. $entities = api_get_entitities($statustext, $body);
  2116. // Add pictures to the attachment array and remove them from the body
  2117. $attachments = api_get_attachments($body);
  2118. // Workaround for ostatus messages where the title is identically to the body
  2119. $html = BBCode::convert(api_clean_plain_items($body), false, 2, true);
  2120. $statusbody = trim(HTML::toPlaintext($html, 0));
  2121. // handle data: images
  2122. $statusbody = api_format_items_embeded_images($item, $statusbody);
  2123. $statustitle = trim($item['title']);
  2124. if (($statustitle != '') && (strpos($statusbody, $statustitle) !== false)) {
  2125. $statustext = trim($statusbody);
  2126. } else {
  2127. $statustext = trim($statustitle."\n\n".$statusbody);
  2128. }
  2129. if ((($item['network'] ?? Protocol::PHANTOM) == Protocol::FEED) && (mb_strlen($statustext)> 1000)) {
  2130. $statustext = mb_substr($statustext, 0, 1000) . "... \n" . ($item['plink'] ?? '');
  2131. }
  2132. $statushtml = BBCode::convert(BBCode::removeAttachment($body), false);
  2133. // Workaround for clients with limited HTML parser functionality
  2134. $search = ["<br>", "<blockquote>", "</blockquote>",
  2135. "<h1>", "</h1>", "<h2>", "</h2>",
  2136. "<h3>", "</h3>", "<h4>", "</h4>",
  2137. "<h5>", "</h5>", "<h6>", "</h6>"];
  2138. $replace = ["<br>", "<br><blockquote>", "</blockquote><br>",
  2139. "<br><h1>", "</h1><br>", "<br><h2>", "</h2><br>",
  2140. "<br><h3>", "</h3><br>", "<br><h4>", "</h4><br>",
  2141. "<br><h5>", "</h5><br>", "<br><h6>", "</h6><br>"];
  2142. $statushtml = str_replace($search, $replace, $statushtml);
  2143. if ($item['title'] != "") {
  2144. $statushtml = "<br><h4>" . BBCode::convert($item['title']) . "</h4><br>" . $statushtml;
  2145. }
  2146. do {
  2147. $oldtext = $statushtml;
  2148. $statushtml = str_replace("<br><br>", "<br>", $statushtml);
  2149. } while ($oldtext != $statushtml);
  2150. if (substr($statushtml, 0, 4) == '<br>') {
  2151. $statushtml = substr($statushtml, 4);
  2152. }
  2153. if (substr($statushtml, 0, -4) == '<br>') {
  2154. $statushtml = substr($statushtml, -4);
  2155. }
  2156. // feeds without body should contain the link
  2157. if ((($item['network'] ?? Protocol::PHANTOM) == Protocol::FEED) && (strlen($item['body']) == 0)) {
  2158. $statushtml .= BBCode::convert($item['plink']);
  2159. }
  2160. return [
  2161. "text" => $statustext,
  2162. "html" => $statushtml,
  2163. "attachments" => $attachments,
  2164. "entities" => $entities
  2165. ];
  2166. }
  2167. /**
  2168. *
  2169. * @param string $body
  2170. *
  2171. * @return array
  2172. * @throws InternalServerErrorException
  2173. */
  2174. function api_get_attachments(&$body)
  2175. {
  2176. $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
  2177. $body = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $body);
  2178. $URLSearchString = "^\[\]";
  2179. if (!preg_match_all("/\[img\]([$URLSearchString]*)\[\/img\]/ism", $body, $images)) {
  2180. return [];
  2181. }
  2182. // Remove all embedded pictures, since they are added as attachments
  2183. foreach ($images[0] as $orig) {
  2184. $body = str_replace($orig, '', $body);
  2185. }
  2186. $attachments = [];
  2187. foreach ($images[1] as $image) {
  2188. $imagedata = Images::getInfoFromURLCached($image);
  2189. if ($imagedata) {
  2190. $attachments[] = ["url" => $image, "mimetype" => $imagedata["mime"], "size" => $imagedata["size"]];
  2191. }
  2192. }
  2193. return $attachments;
  2194. }
  2195. /**
  2196. *
  2197. * @param string $text
  2198. * @param string $bbcode
  2199. *
  2200. * @return array
  2201. * @throws InternalServerErrorException
  2202. * @todo Links at the first character of the post
  2203. */
  2204. function api_get_entitities(&$text, $bbcode)
  2205. {
  2206. $include_entities = strtolower($_REQUEST['include_entities'] ?? 'false');
  2207. if ($include_entities != "true") {
  2208. preg_match_all("/\[img](.*?)\[\/img\]/ism", $bbcode, $images);
  2209. foreach ($images[1] as $image) {
  2210. $replace = ProxyUtils::proxifyUrl($image);
  2211. $text = str_replace($image, $replace, $text);
  2212. }
  2213. return [];
  2214. }
  2215. $bbcode = BBCode::cleanPictureLinks($bbcode);
  2216. // Change pure links in text to bbcode uris
  2217. $bbcode = preg_replace("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", '$1[url=$2]$2[/url]', $bbcode);
  2218. $entities = [];
  2219. $entities["hashtags"] = [];
  2220. $entities["symbols"] = [];
  2221. $entities["urls"] = [];
  2222. $entities["user_mentions"] = [];
  2223. $URLSearchString = "^\[\]";
  2224. $bbcode = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '#$2', $bbcode);
  2225. $bbcode = preg_replace("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism", '[url=$1]$2[/url]', $bbcode);
  2226. $bbcode = preg_replace("/\[video\](.*?)\[\/video\]/ism", '[url=$1]$1[/url]', $bbcode);
  2227. $bbcode = preg_replace(
  2228. "/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism",
  2229. '[url=https://www.youtube.com/watch?v=$1]https://www.youtube.com/watch?v=$1[/url]',
  2230. $bbcode
  2231. );
  2232. $bbcode = preg_replace("/\[youtube\](.*?)\[\/youtube\]/ism", '[url=$1]$1[/url]', $bbcode);
  2233. $bbcode = preg_replace(
  2234. "/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism",
  2235. '[url=https://vimeo.com/$1]https://vimeo.com/$1[/url]',
  2236. $bbcode
  2237. );
  2238. $bbcode = preg_replace("/\[vimeo\](.*?)\[\/vimeo\]/ism", '[url=$1]$1[/url]', $bbcode);
  2239. $bbcode = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $bbcode);
  2240. preg_match_all("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $bbcode, $urls);
  2241. $ordered_urls = [];
  2242. foreach ($urls[1] as $id => $url) {
  2243. $start = iconv_strpos($text, $url, 0, "UTF-8");
  2244. if (!($start === false)) {
  2245. $ordered_urls[$start] = ["url" => $url, "title" => $urls[2][$id]];
  2246. }
  2247. }
  2248. ksort($ordered_urls);
  2249. $offset = 0;
  2250. foreach ($ordered_urls as $url) {
  2251. if ((substr($url["title"], 0, 7) != "http://") && (substr($url["title"], 0, 8) != "https://")
  2252. && !strpos($url["title"], "http://") && !strpos($url["title"], "https://")
  2253. ) {
  2254. $display_url = $url["title"];
  2255. } else {
  2256. $display_url = str_replace(["http://www.", "https://www."], ["", ""], $url["url"]);
  2257. $display_url = str_replace(["http://", "https://"], ["", ""], $display_url);
  2258. if (strlen($display_url) > 26) {
  2259. $display_url = substr($display_url, 0, 25)."…";
  2260. }
  2261. }
  2262. $start = iconv_strpos($text, $url["url"], $offset, "UTF-8");
  2263. if (!($start === false)) {
  2264. $entities["urls"][] = ["url" => $url["url"],
  2265. "expanded_url" => $url["url"],
  2266. "display_url" => $display_url,
  2267. "indices" => [$start, $start+strlen($url["url"])]];
  2268. $offset = $start + 1;
  2269. }
  2270. }
  2271. preg_match_all("/\[img\=(.*?)\](.*?)\[\/img\]/ism", $bbcode, $images, PREG_SET_ORDER);
  2272. $ordered_images = [];
  2273. foreach ($images as $image) {
  2274. $start = iconv_strpos($text, $image[1], 0, "UTF-8");
  2275. if (!($start === false)) {
  2276. $ordered_images[$start] = ['url' => $image[1], 'alt' => $image[2]];
  2277. }
  2278. }
  2279. preg_match_all("/\[img](.*?)\[\/img\]/ism", $bbcode, $images);
  2280. foreach ($images[1] as $image) {
  2281. $start = iconv_strpos($text, $image, 0, "UTF-8");
  2282. if (!($start === false)) {
  2283. $ordered_images[$start] = ['url' => $image, 'alt' => ''];
  2284. }
  2285. }
  2286. $offset = 0;
  2287. foreach ($ordered_images as $image) {
  2288. $url = $image['url'];
  2289. $ext_alt_text = $image['alt'];
  2290. $display_url = str_replace(["http://www.", "https://www."], ["", ""], $url);
  2291. $display_url = str_replace(["http://", "https://"], ["", ""], $display_url);
  2292. if (strlen($display_url) > 26) {
  2293. $display_url = substr($display_url, 0, 25)."…";
  2294. }
  2295. $start = iconv_strpos($text, $url, $offset, "UTF-8");
  2296. if (!($start === false)) {
  2297. $image = Images::getInfoFromURLCached($url);
  2298. if ($image) {
  2299. // If image cache is activated, then use the following sizes:
  2300. // thumb (150), small (340), medium (600) and large (1024)
  2301. if (!Config::get("system", "proxy_disabled")) {
  2302. $media_url = ProxyUtils::proxifyUrl($url);
  2303. $sizes = [];
  2304. $scale = Images::getScalingDimensions($image[0], $image[1], 150);
  2305. $sizes["thumb"] = ["w" => $scale["width"], "h" => $scale["height"], "resize" => "fit"];
  2306. if (($image[0] > 150) || ($image[1] > 150)) {
  2307. $scale = Images::getScalingDimensions($image[0], $image[1], 340);
  2308. $sizes["small"] = ["w" => $scale["width"], "h" => $scale["height"], "resize" => "fit"];
  2309. }
  2310. $scale = Images::getScalingDimensions($image[0], $image[1], 600);
  2311. $sizes["medium"] = ["w" => $scale["width"], "h" => $scale["height"], "resize" => "fit"];
  2312. if (($image[0] > 600) || ($image[1] > 600)) {
  2313. $scale = Images::getScalingDimensions($image[0], $image[1], 1024);
  2314. $sizes["large"] = ["w" => $scale["width"], "h" => $scale["height"], "resize" => "fit"];
  2315. }
  2316. } else {
  2317. $media_url = $url;
  2318. $sizes["medium"] = ["w" => $image[0], "h" => $image[1], "resize" => "fit"];
  2319. }
  2320. $entities["media"][] = [
  2321. "id" => $start+1,
  2322. "id_str" => (string) ($start + 1),
  2323. "indices" => [$start, $start+strlen($url)],
  2324. "media_url" => Strings::normaliseLink($media_url),
  2325. "media_url_https" => $media_url,
  2326. "url" => $url,
  2327. "display_url" => $display_url,
  2328. "expanded_url" => $url,
  2329. "ext_alt_text" => $ext_alt_text,
  2330. "type" => "photo",
  2331. "sizes" => $sizes];
  2332. }
  2333. $offset = $start + 1;
  2334. }
  2335. }
  2336. return $entities;
  2337. }
  2338. /**
  2339. *
  2340. * @param array $item
  2341. * @param string $text
  2342. *
  2343. * @return string
  2344. */
  2345. function api_format_items_embeded_images($item, $text)
  2346. {
  2347. $text = preg_replace_callback(
  2348. '|data:image/([^;]+)[^=]+=*|m',
  2349. function () use ($item) {
  2350. return DI::baseUrl() . '/display/' . $item['guid'];
  2351. },
  2352. $text
  2353. );
  2354. return $text;
  2355. }
  2356. /**
  2357. * @brief return <a href='url'>name</a> as array
  2358. *
  2359. * @param string $txt text
  2360. * @return array
  2361. * 'name' => 'name',
  2362. * 'url => 'url'
  2363. */
  2364. function api_contactlink_to_array($txt)
  2365. {
  2366. $match = [];
  2367. $r = preg_match_all('|<a href="([^"]*)">([^<]*)</a>|', $txt, $match);
  2368. if ($r && count($match)==3) {
  2369. $res = [
  2370. 'name' => $match[2],
  2371. 'url' => $match[1]
  2372. ];
  2373. } else {
  2374. $res = [
  2375. 'name' => $txt,
  2376. 'url' => ""
  2377. ];
  2378. }
  2379. return $res;
  2380. }
  2381. /**
  2382. * @brief return likes, dislikes and attend status for item
  2383. *
  2384. * @param array $item array
  2385. * @param string $type Return type (atom, rss, xml, json)
  2386. *
  2387. * @return array
  2388. * likes => int count,
  2389. * dislikes => int count
  2390. * @throws BadRequestException
  2391. * @throws ImagickException
  2392. * @throws InternalServerErrorException
  2393. * @throws UnauthorizedException
  2394. */
  2395. function api_format_items_activities($item, $type = "json")
  2396. {
  2397. $a = DI::app();
  2398. $activities = [
  2399. 'like' => [],
  2400. 'dislike' => [],
  2401. 'attendyes' => [],
  2402. 'attendno' => [],
  2403. 'attendmaybe' => [],
  2404. 'announce' => [],
  2405. ];
  2406. $condition = ['uid' => $item['uid'], 'thr-parent' => $item['uri'], 'gravity' => GRAVITY_ACTIVITY];
  2407. $ret = Item::selectForUser($item['uid'], ['author-id', 'verb'], $condition);
  2408. while ($parent_item = Item::fetch($ret)) {
  2409. // not used as result should be structured like other user data
  2410. //builtin_activity_puller($i, $activities);
  2411. // get user data and add it to the array of the activity
  2412. $user = api_get_user($a, $parent_item['author-id']);
  2413. switch ($parent_item['verb']) {
  2414. case Activity::LIKE:
  2415. $activities['like'][] = $user;
  2416. break;
  2417. case Activity::DISLIKE:
  2418. $activities['dislike'][] = $user;
  2419. break;
  2420. case Activity::ATTEND:
  2421. $activities['attendyes'][] = $user;
  2422. break;
  2423. case Activity::ATTENDNO:
  2424. $activities['attendno'][] = $user;
  2425. break;
  2426. case Activity::ATTENDMAYBE:
  2427. $activities['attendmaybe'][] = $user;
  2428. break;
  2429. case Activity::ANNOUNCE:
  2430. $activities['announce'][] = $user;
  2431. break;
  2432. default:
  2433. break;
  2434. }
  2435. }
  2436. DBA::close($ret);
  2437. if ($type == "xml") {
  2438. $xml_activities = [];
  2439. foreach ($activities as $k => $v) {
  2440. // change xml element from "like" to "friendica:like"
  2441. $xml_activities["friendica:".$k] = $v;
  2442. // add user data into xml output
  2443. $k_user = 0;
  2444. foreach ($v as $user) {
  2445. $xml_activities["friendica:".$k][$k_user++.":user"] = $user;
  2446. }
  2447. }
  2448. $activities = $xml_activities;
  2449. }
  2450. return $activities;
  2451. }
  2452. /**
  2453. * @brief return data from profiles
  2454. *
  2455. * @param array $profile_row array containing data from db table 'profile'
  2456. * @return array
  2457. * @throws InternalServerErrorException
  2458. */
  2459. function api_format_items_profiles($profile_row)
  2460. {
  2461. $profile = [
  2462. 'profile_id' => $profile_row['id'],
  2463. 'profile_name' => $profile_row['profile-name'],
  2464. 'is_default' => $profile_row['is-default'] ? true : false,
  2465. 'hide_friends' => $profile_row['hide-friends'] ? true : false,
  2466. 'profile_photo' => $profile_row['photo'],
  2467. 'profile_thumb' => $profile_row['thumb'],
  2468. 'publish' => $profile_row['publish'] ? true : false,
  2469. 'net_publish' => $profile_row['net-publish'] ? true : false,
  2470. 'description' => $profile_row['pdesc'],
  2471. 'date_of_birth' => $profile_row['dob'],
  2472. 'address' => $profile_row['address'],
  2473. 'city' => $profile_row['locality'],
  2474. 'region' => $profile_row['region'],
  2475. 'postal_code' => $profile_row['postal-code'],
  2476. 'country' => $profile_row['country-name'],
  2477. 'hometown' => $profile_row['hometown'],
  2478. 'gender' => $profile_row['gender'],
  2479. 'marital' => $profile_row['marital'],
  2480. 'marital_with' => $profile_row['with'],
  2481. 'marital_since' => $profile_row['howlong'],
  2482. 'sexual' => $profile_row['sexual'],
  2483. 'politic' => $profile_row['politic'],
  2484. 'religion' => $profile_row['religion'],
  2485. 'public_keywords' => $profile_row['pub_keywords'],
  2486. 'private_keywords' => $profile_row['prv_keywords'],
  2487. 'likes' => BBCode::convert(api_clean_plain_items($profile_row['likes']) , false, 2),
  2488. 'dislikes' => BBCode::convert(api_clean_plain_items($profile_row['dislikes']) , false, 2),
  2489. 'about' => BBCode::convert(api_clean_plain_items($profile_row['about']) , false, 2),
  2490. 'music' => BBCode::convert(api_clean_plain_items($profile_row['music']) , false, 2),
  2491. 'book' => BBCode::convert(api_clean_plain_items($profile_row['book']) , false, 2),
  2492. 'tv' => BBCode::convert(api_clean_plain_items($profile_row['tv']) , false, 2),
  2493. 'film' => BBCode::convert(api_clean_plain_items($profile_row['film']) , false, 2),
  2494. 'interest' => BBCode::convert(api_clean_plain_items($profile_row['interest']) , false, 2),
  2495. 'romance' => BBCode::convert(api_clean_plain_items($profile_row['romance']) , false, 2),
  2496. 'work' => BBCode::convert(api_clean_plain_items($profile_row['work']) , false, 2),
  2497. 'education' => BBCode::convert(api_clean_plain_items($profile_row['education']), false, 2),
  2498. 'social_networks' => BBCode::convert(api_clean_plain_items($profile_row['contact']) , false, 2),
  2499. 'homepage' => $profile_row['homepage'],
  2500. 'users' => null
  2501. ];
  2502. return $profile;
  2503. }
  2504. /**
  2505. * @brief format items to be returned by api
  2506. *
  2507. * @param array $items array of items
  2508. * @param array $user_info
  2509. * @param bool $filter_user filter items by $user_info
  2510. * @param string $type Return type (atom, rss, xml, json)
  2511. * @return array
  2512. * @throws BadRequestException
  2513. * @throws ImagickException
  2514. * @throws InternalServerErrorException
  2515. * @throws UnauthorizedException
  2516. */
  2517. function api_format_items($items, $user_info, $filter_user = false, $type = "json")
  2518. {
  2519. $a = Friendica\DI::app();
  2520. $ret = [];
  2521. foreach ((array)$items as $item) {
  2522. list($status_user, $author_user, $owner_user) = api_item_get_user($a, $item);
  2523. // Look if the posts are matching if they should be filtered by user id
  2524. if ($filter_user && ($status_user["id"] != $user_info["id"])) {
  2525. continue;
  2526. }
  2527. $status = api_format_item($item, $type, $status_user, $author_user, $owner_user);
  2528. $ret[] = $status;
  2529. }
  2530. return $ret;
  2531. }
  2532. /**
  2533. * @param array $item Item record
  2534. * @param string $type Return format (atom, rss, xml, json)
  2535. * @param array $status_user User record of the item author, can be provided by api_item_get_user()
  2536. * @param array $author_user User record of the item author, can be provided by api_item_get_user()
  2537. * @param array $owner_user User record of the item owner, can be provided by api_item_get_user()
  2538. * @return array API-formatted status
  2539. * @throws BadRequestException
  2540. * @throws ImagickException
  2541. * @throws InternalServerErrorException
  2542. * @throws UnauthorizedException
  2543. */
  2544. function api_format_item($item, $type = "json", $status_user = null, $author_user = null, $owner_user = null)
  2545. {
  2546. $a = Friendica\DI::app();
  2547. if (empty($status_user) || empty($author_user) || empty($owner_user)) {
  2548. list($status_user, $author_user, $owner_user) = api_item_get_user($a, $item);
  2549. }
  2550. localize_item($item);
  2551. $in_reply_to = api_in_reply_to($item);
  2552. $converted = api_convert_item($item);
  2553. if ($type == "xml") {
  2554. $geo = "georss:point";
  2555. } else {
  2556. $geo = "geo";
  2557. }
  2558. $status = [
  2559. 'text' => $converted["text"],
  2560. 'truncated' => false,
  2561. 'created_at'=> api_date($item['created']),
  2562. 'in_reply_to_status_id' => $in_reply_to['status_id'],
  2563. 'in_reply_to_status_id_str' => $in_reply_to['status_id_str'],
  2564. 'source' => (($item['app']) ? $item['app'] : 'web'),
  2565. 'id' => intval($item['id']),
  2566. 'id_str' => (string) intval($item['id']),
  2567. 'in_reply_to_user_id' => $in_reply_to['user_id'],
  2568. 'in_reply_to_user_id_str' => $in_reply_to['user_id_str'],
  2569. 'in_reply_to_screen_name' => $in_reply_to['screen_name'],
  2570. $geo => null,
  2571. 'favorited' => $item['starred'] ? true : false,
  2572. 'user' => $status_user,
  2573. 'friendica_author' => $author_user,
  2574. 'friendica_owner' => $owner_user,
  2575. 'friendica_private' => $item['private'] == 1,
  2576. //'entities' => NULL,
  2577. 'statusnet_html' => $converted["html"],
  2578. 'statusnet_conversation_id' => $item['parent'],
  2579. 'external_url' => DI::baseUrl() . "/display/" . $item['guid'],
  2580. 'friendica_activities' => api_format_items_activities($item, $type),
  2581. 'friendica_title' => $item['title'],
  2582. 'friendica_html' => BBCode::convert($item['body'], false)
  2583. ];
  2584. if (count($converted["attachments"]) > 0) {
  2585. $status["attachments"] = $converted["attachments"];
  2586. }
  2587. if (count($converted["entities"]) > 0) {
  2588. $status["entities"] = $converted["entities"];
  2589. }
  2590. if ($status["source"] == 'web') {
  2591. $status["source"] = ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']);
  2592. } elseif (ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']) != $status["source"]) {
  2593. $status["source"] = trim($status["source"].' ('.ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']).')');
  2594. }
  2595. $retweeted_item = [];
  2596. $quoted_item = [];
  2597. if ($item["id"] == $item["parent"]) {
  2598. $body = $item['body'];
  2599. $retweeted_item = api_share_as_retweet($item);
  2600. if ($body != $item['body']) {
  2601. $quoted_item = $retweeted_item;
  2602. $retweeted_item = [];
  2603. }
  2604. }
  2605. if (empty($retweeted_item) && ($item['owner-id'] == $item['author-id'])) {
  2606. $announce = api_get_announce($item);
  2607. if (!empty($announce)) {
  2608. $retweeted_item = $item;
  2609. $item = $announce;
  2610. $status['friendica_owner'] = api_get_user($a, $announce['author-id']);
  2611. }
  2612. }
  2613. if (!empty($quoted_item)) {
  2614. if ($quoted_item['id'] != $item['id']) {
  2615. $quoted_status = api_format_item($quoted_item);
  2616. /// @todo Only remove the attachments that are also contained in the quotes status
  2617. unset($status['attachments']);
  2618. unset($status['entities']);
  2619. } else {
  2620. $conv_quoted = api_convert_item($quoted_item);
  2621. $quoted_status = $status;
  2622. unset($quoted_status['attachments']);
  2623. unset($quoted_status['entities']);
  2624. unset($quoted_status['statusnet_conversation_id']);
  2625. $quoted_status['text'] = $conv_quoted['text'];
  2626. $quoted_status['statusnet_html'] = $conv_quoted['html'];
  2627. try {
  2628. $quoted_status["user"] = api_get_user($a, $quoted_item["author-id"]);
  2629. } catch (BadRequestException $e) {
  2630. // user not found. should be found?
  2631. /// @todo check if the user should be always found
  2632. $quoted_status["user"] = [];
  2633. }
  2634. }
  2635. unset($quoted_status['friendica_author']);
  2636. unset($quoted_status['friendica_owner']);
  2637. unset($quoted_status['friendica_activities']);
  2638. unset($quoted_status['friendica_private']);
  2639. }
  2640. if (!empty($retweeted_item)) {
  2641. $retweeted_status = $status;
  2642. unset($retweeted_status['friendica_author']);
  2643. unset($retweeted_status['friendica_owner']);
  2644. unset($retweeted_status['friendica_activities']);
  2645. unset($retweeted_status['friendica_private']);
  2646. unset($retweeted_status['statusnet_conversation_id']);
  2647. $status['user'] = $status['friendica_owner'];
  2648. try {
  2649. $retweeted_status["user"] = api_get_user($a, $retweeted_item["author-id"]);
  2650. } catch (BadRequestException $e) {
  2651. // user not found. should be found?
  2652. /// @todo check if the user should be always found
  2653. $retweeted_status["user"] = [];
  2654. }
  2655. $rt_converted = api_convert_item($retweeted_item);
  2656. $retweeted_status['text'] = $rt_converted["text"];
  2657. $retweeted_status['statusnet_html'] = $rt_converted["html"];
  2658. $retweeted_status['created_at'] = api_date($retweeted_item['created']);
  2659. if (!empty($quoted_status)) {
  2660. $retweeted_status['quoted_status'] = $quoted_status;
  2661. }
  2662. $status['friendica_author'] = $retweeted_status['user'];
  2663. $status['retweeted_status'] = $retweeted_status;
  2664. } elseif (!empty($quoted_status)) {
  2665. $root_status = api_convert_item($item);
  2666. $status['text'] = $root_status["text"];
  2667. $status['statusnet_html'] = $root_status["html"];
  2668. $status['quoted_status'] = $quoted_status;
  2669. }
  2670. // "uid" and "self" are only needed for some internal stuff, so remove it from here
  2671. unset($status["user"]["uid"]);
  2672. unset($status["user"]["self"]);
  2673. if ($item["coord"] != "") {
  2674. $coords = explode(' ', $item["coord"]);
  2675. if (count($coords) == 2) {
  2676. if ($type == "json") {
  2677. $status["geo"] = ['type' => 'Point',
  2678. 'coordinates' => [(float) $coords[0],
  2679. (float) $coords[1]]];
  2680. } else {// Not sure if this is the official format - if someone founds a documentation we can check
  2681. $status["georss:point"] = $item["coord"];
  2682. }
  2683. }
  2684. }
  2685. return $status;
  2686. }
  2687. /**
  2688. * Returns the remaining number of API requests available to the user before the API limit is reached.
  2689. *
  2690. * @param string $type Return type (atom, rss, xml, json)
  2691. *
  2692. * @return array|string
  2693. * @throws Exception
  2694. */
  2695. function api_account_rate_limit_status($type)
  2696. {
  2697. if ($type == "xml") {
  2698. $hash = [
  2699. 'remaining-hits' => '150',
  2700. '@attributes' => ["type" => "integer"],
  2701. 'hourly-limit' => '150',
  2702. '@attributes2' => ["type" => "integer"],
  2703. 'reset-time' => DateTimeFormat::utc('now + 1 hour', DateTimeFormat::ATOM),
  2704. '@attributes3' => ["type" => "datetime"],
  2705. 'reset_time_in_seconds' => strtotime('now + 1 hour'),
  2706. '@attributes4' => ["type" => "integer"],
  2707. ];
  2708. } else {
  2709. $hash = [
  2710. 'reset_time_in_seconds' => strtotime('now + 1 hour'),
  2711. 'remaining_hits' => '150',
  2712. 'hourly_limit' => '150',
  2713. 'reset_time' => api_date(DateTimeFormat::utc('now + 1 hour', DateTimeFormat::ATOM)),
  2714. ];
  2715. }
  2716. return api_format_data('hash', $type, ['hash' => $hash]);
  2717. }
  2718. /// @TODO move to top of file or somewhere better
  2719. api_register_func('api/account/rate_limit_status', 'api_account_rate_limit_status', true);
  2720. /**
  2721. * Returns the string "ok" in the requested format with a 200 OK HTTP status code.
  2722. *
  2723. * @param string $type Return type (atom, rss, xml, json)
  2724. *
  2725. * @return array|string
  2726. */
  2727. function api_help_test($type)
  2728. {
  2729. if ($type == 'xml') {
  2730. $ok = "true";
  2731. } else {
  2732. $ok = "ok";
  2733. }
  2734. return api_format_data('ok', $type, ["ok" => $ok]);
  2735. }
  2736. /// @TODO move to top of file or somewhere better
  2737. api_register_func('api/help/test', 'api_help_test', false);
  2738. /**
  2739. * Returns all lists the user subscribes to.
  2740. *
  2741. * @param string $type Return type (atom, rss, xml, json)
  2742. *
  2743. * @return array|string
  2744. * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-list
  2745. */
  2746. function api_lists_list($type)
  2747. {
  2748. $ret = [];
  2749. /// @TODO $ret is not filled here?
  2750. return api_format_data('lists', $type, ["lists_list" => $ret]);
  2751. }
  2752. /// @TODO move to top of file or somewhere better
  2753. api_register_func('api/lists/list', 'api_lists_list', true);
  2754. api_register_func('api/lists/subscriptions', 'api_lists_list', true);
  2755. /**
  2756. * Returns all groups the user owns.
  2757. *
  2758. * @param string $type Return type (atom, rss, xml, json)
  2759. *
  2760. * @return array|string
  2761. * @throws BadRequestException
  2762. * @throws ForbiddenException
  2763. * @throws ImagickException
  2764. * @throws InternalServerErrorException
  2765. * @throws UnauthorizedException
  2766. * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-ownerships
  2767. */
  2768. function api_lists_ownerships($type)
  2769. {
  2770. $a = DI::app();
  2771. if (api_user() === false) {
  2772. throw new ForbiddenException();
  2773. }
  2774. // params
  2775. $user_info = api_get_user($a);
  2776. $uid = $user_info['uid'];
  2777. $groups = DBA::select('group', [], ['deleted' => 0, 'uid' => $uid]);
  2778. // loop through all groups
  2779. $lists = [];
  2780. foreach ($groups as $group) {
  2781. if ($group['visible']) {
  2782. $mode = 'public';
  2783. } else {
  2784. $mode = 'private';
  2785. }
  2786. $lists[] = [
  2787. 'name' => $group['name'],
  2788. 'id' => intval($group['id']),
  2789. 'id_str' => (string) $group['id'],
  2790. 'user' => $user_info,
  2791. 'mode' => $mode
  2792. ];
  2793. }
  2794. return api_format_data("lists", $type, ['lists' => ['lists' => $lists]]);
  2795. }
  2796. /// @TODO move to top of file or somewhere better
  2797. api_register_func('api/lists/ownerships', 'api_lists_ownerships', true);
  2798. /**
  2799. * Returns recent statuses from users in the specified group.
  2800. *
  2801. * @param string $type Return type (atom, rss, xml, json)
  2802. *
  2803. * @return array|string
  2804. * @throws BadRequestException
  2805. * @throws ForbiddenException
  2806. * @throws ImagickException
  2807. * @throws InternalServerErrorException
  2808. * @throws UnauthorizedException
  2809. * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-ownerships
  2810. */
  2811. function api_lists_statuses($type)
  2812. {
  2813. $a = DI::app();
  2814. $user_info = api_get_user($a);
  2815. if (api_user() === false || $user_info === false) {
  2816. throw new ForbiddenException();
  2817. }
  2818. unset($_REQUEST["user_id"]);
  2819. unset($_GET["user_id"]);
  2820. unset($_REQUEST["screen_name"]);
  2821. unset($_GET["screen_name"]);
  2822. if (empty($_REQUEST['list_id'])) {
  2823. throw new BadRequestException('list_id not specified');
  2824. }
  2825. // params
  2826. $count = $_REQUEST['count'] ?? 20;
  2827. $page = $_REQUEST['page'] ?? 1;
  2828. $since_id = $_REQUEST['since_id'] ?? 0;
  2829. $max_id = $_REQUEST['max_id'] ?? 0;
  2830. $exclude_replies = (!empty($_REQUEST['exclude_replies']) ? 1 : 0);
  2831. $conversation_id = $_REQUEST['conversation_id'] ?? 0;
  2832. $start = max(0, ($page - 1) * $count);
  2833. $condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `id` > ? AND `group_member`.`gid` = ?",
  2834. api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, $_REQUEST['list_id']];
  2835. if ($max_id > 0) {
  2836. $condition[0] .= " AND `item`.`id` <= ?";
  2837. $condition[] = $max_id;
  2838. }
  2839. if ($exclude_replies > 0) {
  2840. $condition[0] .= ' AND `item`.`parent` = `item`.`id`';
  2841. }
  2842. if ($conversation_id > 0) {
  2843. $condition[0] .= " AND `item`.`parent` = ?";
  2844. $condition[] = $conversation_id;
  2845. }
  2846. $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
  2847. $statuses = Item::selectForUser(api_user(), [], $condition, $params);
  2848. $items = api_format_items(Item::inArray($statuses), $user_info, false, $type);
  2849. $data = ['status' => $items];
  2850. switch ($type) {
  2851. case "atom":
  2852. break;
  2853. case "rss":
  2854. $data = api_rss_extra($a, $data, $user_info);
  2855. break;
  2856. }
  2857. return api_format_data("statuses", $type, $data);
  2858. }
  2859. /// @TODO move to top of file or somewhere better
  2860. api_register_func('api/lists/statuses', 'api_lists_statuses', true);
  2861. /**
  2862. * Considers friends and followers lists to be private and won't return
  2863. * anything if any user_id parameter is passed.
  2864. *
  2865. * @brief Returns either the friends of the follower list
  2866. *
  2867. * @param string $qtype Either "friends" or "followers"
  2868. * @return boolean|array
  2869. * @throws BadRequestException
  2870. * @throws ForbiddenException
  2871. * @throws ImagickException
  2872. * @throws InternalServerErrorException
  2873. * @throws UnauthorizedException
  2874. */
  2875. function api_statuses_f($qtype)
  2876. {
  2877. $a = DI::app();
  2878. if (api_user() === false) {
  2879. throw new ForbiddenException();
  2880. }
  2881. // pagination
  2882. $count = $_GET['count'] ?? 20;
  2883. $page = $_GET['page'] ?? 1;
  2884. $start = max(0, ($page - 1) * $count);
  2885. $user_info = api_get_user($a);
  2886. if (!empty($_GET['cursor']) && $_GET['cursor'] == 'undefined') {
  2887. /* this is to stop Hotot to load friends multiple times
  2888. * I'm not sure if I'm missing return something or
  2889. * is a bug in hotot. Workaround, meantime
  2890. */
  2891. /*$ret=Array();
  2892. return array('$users' => $ret);*/
  2893. return false;
  2894. }
  2895. $sql_extra = '';
  2896. if ($qtype == 'friends') {
  2897. $sql_extra = sprintf(" AND ( `rel` = %d OR `rel` = %d ) ", intval(Contact::SHARING), intval(Contact::FRIEND));
  2898. } elseif ($qtype == 'followers') {
  2899. $sql_extra = sprintf(" AND ( `rel` = %d OR `rel` = %d ) ", intval(Contact::FOLLOWER), intval(Contact::FRIEND));
  2900. }
  2901. // friends and followers only for self
  2902. if ($user_info['self'] == 0) {
  2903. $sql_extra = " AND false ";
  2904. }
  2905. if ($qtype == 'blocks') {
  2906. $sql_filter = 'AND `blocked` AND NOT `pending`';
  2907. } elseif ($qtype == 'incoming') {
  2908. $sql_filter = 'AND `pending`';
  2909. } else {
  2910. $sql_filter = 'AND (NOT `blocked` OR `pending`)';
  2911. }
  2912. $r = q(
  2913. "SELECT `nurl`
  2914. FROM `contact`
  2915. WHERE `uid` = %d
  2916. AND NOT `self`
  2917. $sql_filter
  2918. $sql_extra
  2919. ORDER BY `nick`
  2920. LIMIT %d, %d",
  2921. intval(api_user()),
  2922. intval($start),
  2923. intval($count)
  2924. );
  2925. $ret = [];
  2926. foreach ($r as $cid) {
  2927. $user = api_get_user($a, $cid['nurl']);
  2928. // "uid" and "self" are only needed for some internal stuff, so remove it from here
  2929. unset($user["uid"]);
  2930. unset($user["self"]);
  2931. if ($user) {
  2932. $ret[] = $user;
  2933. }
  2934. }
  2935. return ['user' => $ret];
  2936. }
  2937. /**
  2938. * Returns the user's friends.
  2939. *
  2940. * @brief Returns the list of friends of the provided user
  2941. *
  2942. * @deprecated By Twitter API in favor of friends/list
  2943. *
  2944. * @param string $type Either "json" or "xml"
  2945. * @return boolean|string|array
  2946. * @throws BadRequestException
  2947. * @throws ForbiddenException
  2948. */
  2949. function api_statuses_friends($type)
  2950. {
  2951. $data = api_statuses_f("friends");
  2952. if ($data === false) {
  2953. return false;
  2954. }
  2955. return api_format_data("users", $type, $data);
  2956. }
  2957. /**
  2958. * Returns the user's followers.
  2959. *
  2960. * @brief Returns the list of followers of the provided user
  2961. *
  2962. * @deprecated By Twitter API in favor of friends/list
  2963. *
  2964. * @param string $type Either "json" or "xml"
  2965. * @return boolean|string|array
  2966. * @throws BadRequestException
  2967. * @throws ForbiddenException
  2968. */
  2969. function api_statuses_followers($type)
  2970. {
  2971. $data = api_statuses_f("followers");
  2972. if ($data === false) {
  2973. return false;
  2974. }
  2975. return api_format_data("users", $type, $data);
  2976. }
  2977. /// @TODO move to top of file or somewhere better
  2978. api_register_func('api/statuses/friends', 'api_statuses_friends', true);
  2979. api_register_func('api/statuses/followers', 'api_statuses_followers', true);
  2980. /**
  2981. * Returns the list of blocked users
  2982. *
  2983. * @see https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-blocks-list
  2984. *
  2985. * @param string $type Either "json" or "xml"
  2986. *
  2987. * @return boolean|string|array
  2988. * @throws BadRequestException
  2989. * @throws ForbiddenException
  2990. */
  2991. function api_blocks_list($type)
  2992. {
  2993. $data = api_statuses_f('blocks');
  2994. if ($data === false) {
  2995. return false;
  2996. }
  2997. return api_format_data("users", $type, $data);
  2998. }
  2999. /// @TODO move to top of file or somewhere better
  3000. api_register_func('api/blocks/list', 'api_blocks_list', true);
  3001. /**
  3002. * Returns the list of pending users IDs
  3003. *
  3004. * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-incoming
  3005. *
  3006. * @param string $type Either "json" or "xml"
  3007. *
  3008. * @return boolean|string|array
  3009. * @throws BadRequestException
  3010. * @throws ForbiddenException
  3011. */
  3012. function api_friendships_incoming($type)
  3013. {
  3014. $data = api_statuses_f('incoming');
  3015. if ($data === false) {
  3016. return false;
  3017. }
  3018. $ids = [];
  3019. foreach ($data['user'] as $user) {
  3020. $ids[] = $user['id'];
  3021. }
  3022. return api_format_data("ids", $type, ['id' => $ids]);
  3023. }
  3024. /// @TODO move to top of file or somewhere better
  3025. api_register_func('api/friendships/incoming', 'api_friendships_incoming', true);
  3026. /**
  3027. * Returns the instance's configuration information.
  3028. *
  3029. * @param string $type Return type (atom, rss, xml, json)
  3030. *
  3031. * @return array|string
  3032. * @throws InternalServerErrorException
  3033. */
  3034. function api_statusnet_config($type)
  3035. {
  3036. $name = Config::get('config', 'sitename');
  3037. $server = DI::baseUrl()->getHostname();
  3038. $logo = DI::baseUrl() . '/images/friendica-64.png';
  3039. $email = Config::get('config', 'admin_email');
  3040. $closed = intval(Config::get('config', 'register_policy')) === \Friendica\Module\Register::CLOSED ? 'true' : 'false';
  3041. $private = Config::get('system', 'block_public') ? 'true' : 'false';
  3042. $textlimit = (string) Config::get('config', 'api_import_size', Config::get('config', 'max_import_size', 200000));
  3043. $ssl = Config::get('system', 'have_ssl') ? 'true' : 'false';
  3044. $sslserver = Config::get('system', 'have_ssl') ? str_replace('http:', 'https:', DI::baseUrl()) : '';
  3045. $config = [
  3046. 'site' => ['name' => $name,'server' => $server, 'theme' => 'default', 'path' => '',
  3047. 'logo' => $logo, 'fancy' => true, 'language' => 'en', 'email' => $email, 'broughtby' => '',
  3048. 'broughtbyurl' => '', 'timezone' => 'UTC', 'closed' => $closed, 'inviteonly' => false,
  3049. 'private' => $private, 'textlimit' => $textlimit, 'sslserver' => $sslserver, 'ssl' => $ssl,
  3050. 'shorturllength' => '30',
  3051. 'friendica' => [
  3052. 'FRIENDICA_PLATFORM' => FRIENDICA_PLATFORM,
  3053. 'FRIENDICA_VERSION' => FRIENDICA_VERSION,
  3054. 'DFRN_PROTOCOL_VERSION' => DFRN_PROTOCOL_VERSION,
  3055. 'DB_UPDATE_VERSION' => DB_UPDATE_VERSION
  3056. ]
  3057. ],
  3058. ];
  3059. return api_format_data('config', $type, ['config' => $config]);
  3060. }
  3061. /// @TODO move to top of file or somewhere better
  3062. api_register_func('api/gnusocial/config', 'api_statusnet_config', false);
  3063. api_register_func('api/statusnet/config', 'api_statusnet_config', false);
  3064. /**
  3065. *
  3066. * @param string $type Return type (atom, rss, xml, json)
  3067. *
  3068. * @return array|string
  3069. */
  3070. function api_statusnet_version($type)
  3071. {
  3072. // liar
  3073. $fake_statusnet_version = "0.9.7";
  3074. return api_format_data('version', $type, ['version' => $fake_statusnet_version]);
  3075. }
  3076. /// @TODO move to top of file or somewhere better
  3077. api_register_func('api/gnusocial/version', 'api_statusnet_version', false);
  3078. api_register_func('api/statusnet/version', 'api_statusnet_version', false);
  3079. /**
  3080. *
  3081. * @param string $type Return type (atom, rss, xml, json)
  3082. *
  3083. * @param int $rel A contact relationship constant
  3084. * @return array|string|void
  3085. * @throws BadRequestException
  3086. * @throws ForbiddenException
  3087. * @throws ImagickException
  3088. * @throws InternalServerErrorException
  3089. * @throws UnauthorizedException
  3090. * @todo use api_format_data() to return data
  3091. */
  3092. function api_ff_ids($type, int $rel)
  3093. {
  3094. if (!api_user()) {
  3095. throw new ForbiddenException();
  3096. }
  3097. $a = DI::app();
  3098. api_get_user($a);
  3099. $stringify_ids = $_REQUEST['stringify_ids'] ?? false;
  3100. $contacts = DBA::p("SELECT `pcontact`.`id`
  3101. FROM `contact`
  3102. INNER JOIN `contact` AS `pcontact`
  3103. ON `contact`.`nurl` = `pcontact`.`nurl`
  3104. AND `pcontact`.`uid` = 0
  3105. WHERE `contact`.`uid` = ?
  3106. AND NOT `contact`.`self`
  3107. AND `contact`.`rel` IN (?, ?)",
  3108. api_user(),
  3109. $rel,
  3110. Contact::FRIEND
  3111. );
  3112. $ids = [];
  3113. foreach (DBA::toArray($contacts) as $contact) {
  3114. if ($stringify_ids) {
  3115. $ids[] = $contact['id'];
  3116. } else {
  3117. $ids[] = intval($contact['id']);
  3118. }
  3119. }
  3120. return api_format_data('ids', $type, ['id' => $ids]);
  3121. }
  3122. /**
  3123. * Returns the ID of every user the user is following.
  3124. *
  3125. * @param string $type Return type (atom, rss, xml, json)
  3126. *
  3127. * @return array|string
  3128. * @throws BadRequestException
  3129. * @throws ForbiddenException
  3130. * @throws ImagickException
  3131. * @throws InternalServerErrorException
  3132. * @throws UnauthorizedException
  3133. * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friends-ids
  3134. */
  3135. function api_friends_ids($type)
  3136. {
  3137. return api_ff_ids($type, Contact::SHARING);
  3138. }
  3139. /**
  3140. * Returns the ID of every user following the user.
  3141. *
  3142. * @param string $type Return type (atom, rss, xml, json)
  3143. *
  3144. * @return array|string
  3145. * @throws BadRequestException
  3146. * @throws ForbiddenException
  3147. * @throws ImagickException
  3148. * @throws InternalServerErrorException
  3149. * @throws UnauthorizedException
  3150. * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-followers-ids
  3151. */
  3152. function api_followers_ids($type)
  3153. {
  3154. return api_ff_ids($type, Contact::FOLLOWER);
  3155. }
  3156. /// @TODO move to top of file or somewhere better
  3157. api_register_func('api/friends/ids', 'api_friends_ids', true);
  3158. api_register_func('api/followers/ids', 'api_followers_ids', true);
  3159. /**
  3160. * Sends a new direct message.
  3161. *
  3162. * @param string $type Return type (atom, rss, xml, json)
  3163. *
  3164. * @return array|string
  3165. * @throws BadRequestException
  3166. * @throws ForbiddenException
  3167. * @throws ImagickException
  3168. * @throws InternalServerErrorException
  3169. * @throws NotFoundException
  3170. * @throws UnauthorizedException
  3171. * @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-message
  3172. */
  3173. function api_direct_messages_new($type)
  3174. {
  3175. $a = DI::app();
  3176. if (api_user() === false) {
  3177. throw new ForbiddenException();
  3178. }
  3179. if (empty($_POST["text"]) || empty($_POST["screen_name"]) && empty($_POST["user_id"])) {
  3180. return;
  3181. }
  3182. $sender = api_get_user($a);
  3183. $recipient = null;
  3184. if (!empty($_POST['screen_name'])) {
  3185. $r = q(
  3186. "SELECT `id`, `nurl`, `network` FROM `contact` WHERE `uid`=%d AND `nick`='%s'",
  3187. intval(api_user()),
  3188. DBA::escape($_POST['screen_name'])
  3189. );
  3190. if (DBA::isResult($r)) {
  3191. // Selecting the id by priority, friendica first
  3192. api_best_nickname($r);
  3193. $recipient = api_get_user($a, $r[0]['nurl']);
  3194. }
  3195. } else {
  3196. $recipient = api_get_user($a, $_POST['user_id']);
  3197. }
  3198. if (empty($recipient)) {
  3199. throw new NotFoundException('Recipient not found');
  3200. }
  3201. $replyto = '';
  3202. if (!empty($_REQUEST['replyto'])) {
  3203. $r = q(
  3204. 'SELECT `parent-uri`, `title` FROM `mail` WHERE `uid`=%d AND `id`=%d',
  3205. intval(api_user()),
  3206. intval($_REQUEST['replyto'])
  3207. );
  3208. $replyto = $r[0]['parent-uri'];
  3209. $sub = $r[0]['title'];
  3210. } else {
  3211. if (!empty($_REQUEST['title'])) {
  3212. $sub = $_REQUEST['title'];
  3213. } else {
  3214. $sub = ((strlen($_POST['text'])>10) ? substr($_POST['text'], 0, 10)."...":$_POST['text']);
  3215. }
  3216. }
  3217. $id = Mail::send($recipient['cid'], $_POST['text'], $sub, $replyto);
  3218. if ($id > -1) {
  3219. $r = q("SELECT * FROM `mail` WHERE id=%d", intval($id));
  3220. $ret = api_format_messages($r[0], $recipient, $sender);
  3221. } else {
  3222. $ret = ["error"=>$id];
  3223. }
  3224. $data = ['direct_message'=>$ret];
  3225. switch ($type) {
  3226. case "atom":
  3227. break;
  3228. case "rss":
  3229. $data = api_rss_extra($a, $data, $sender);
  3230. break;
  3231. }
  3232. return api_format_data("direct-messages", $type, $data);
  3233. }
  3234. /// @TODO move to top of file or somewhere better
  3235. api_register_func('api/direct_messages/new', 'api_direct_messages_new', true, API_METHOD_POST);
  3236. /**
  3237. * Destroys a direct message.
  3238. *
  3239. * @brief delete a direct_message from mail table through api
  3240. *
  3241. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  3242. * @return string|array
  3243. * @throws BadRequestException
  3244. * @throws ForbiddenException
  3245. * @throws ImagickException
  3246. * @throws InternalServerErrorException
  3247. * @throws UnauthorizedException
  3248. * @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message
  3249. */
  3250. function api_direct_messages_destroy($type)
  3251. {
  3252. $a = DI::app();
  3253. if (api_user() === false) {
  3254. throw new ForbiddenException();
  3255. }
  3256. // params
  3257. $user_info = api_get_user($a);
  3258. //required
  3259. $id = $_REQUEST['id'] ?? 0;
  3260. // optional
  3261. $parenturi = $_REQUEST['friendica_parenturi'] ?? '';
  3262. $verbose = (!empty($_GET['friendica_verbose']) ? strtolower($_GET['friendica_verbose']) : "false");
  3263. /// @todo optional parameter 'include_entities' from Twitter API not yet implemented
  3264. $uid = $user_info['uid'];
  3265. // error if no id or parenturi specified (for clients posting parent-uri as well)
  3266. if ($verbose == "true" && ($id == 0 || $parenturi == "")) {
  3267. $answer = ['result' => 'error', 'message' => 'message id or parenturi not specified'];
  3268. return api_format_data("direct_messages_delete", $type, ['$result' => $answer]);
  3269. }
  3270. // BadRequestException if no id specified (for clients using Twitter API)
  3271. if ($id == 0) {
  3272. throw new BadRequestException('Message id not specified');
  3273. }
  3274. // add parent-uri to sql command if specified by calling app
  3275. $sql_extra = ($parenturi != "" ? " AND `parent-uri` = '" . DBA::escape($parenturi) . "'" : "");
  3276. // get data of the specified message id
  3277. $r = q(
  3278. "SELECT `id` FROM `mail` WHERE `uid` = %d AND `id` = %d" . $sql_extra,
  3279. intval($uid),
  3280. intval($id)
  3281. );
  3282. // error message if specified id is not in database
  3283. if (!DBA::isResult($r)) {
  3284. if ($verbose == "true") {
  3285. $answer = ['result' => 'error', 'message' => 'message id not in database'];
  3286. return api_format_data("direct_messages_delete", $type, ['$result' => $answer]);
  3287. }
  3288. /// @todo BadRequestException ok for Twitter API clients?
  3289. throw new BadRequestException('message id not in database');
  3290. }
  3291. // delete message
  3292. $result = q(
  3293. "DELETE FROM `mail` WHERE `uid` = %d AND `id` = %d" . $sql_extra,
  3294. intval($uid),
  3295. intval($id)
  3296. );
  3297. if ($verbose == "true") {
  3298. if ($result) {
  3299. // return success
  3300. $answer = ['result' => 'ok', 'message' => 'message deleted'];
  3301. return api_format_data("direct_message_delete", $type, ['$result' => $answer]);
  3302. } else {
  3303. $answer = ['result' => 'error', 'message' => 'unknown error'];
  3304. return api_format_data("direct_messages_delete", $type, ['$result' => $answer]);
  3305. }
  3306. }
  3307. /// @todo return JSON data like Twitter API not yet implemented
  3308. }
  3309. /// @TODO move to top of file or somewhere better
  3310. api_register_func('api/direct_messages/destroy', 'api_direct_messages_destroy', true, API_METHOD_DELETE);
  3311. /**
  3312. * Unfollow Contact
  3313. *
  3314. * @brief unfollow contact
  3315. *
  3316. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  3317. * @return string|array
  3318. * @throws BadRequestException
  3319. * @throws ForbiddenException
  3320. * @throws ImagickException
  3321. * @throws InternalServerErrorException
  3322. * @throws NotFoundException
  3323. * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-destroy.html
  3324. */
  3325. function api_friendships_destroy($type)
  3326. {
  3327. $uid = api_user();
  3328. if ($uid === false) {
  3329. throw new ForbiddenException();
  3330. }
  3331. $contact_id = $_REQUEST['user_id'] ?? 0;
  3332. if (empty($contact_id)) {
  3333. Logger::notice(API_LOG_PREFIX . 'No user_id specified', ['module' => 'api', 'action' => 'friendships_destroy']);
  3334. throw new BadRequestException("no user_id specified");
  3335. }
  3336. // Get Contact by given id
  3337. $contact = DBA::selectFirst('contact', ['url'], ['id' => $contact_id, 'uid' => 0, 'self' => false]);
  3338. if(!DBA::isResult($contact)) {
  3339. Logger::notice(API_LOG_PREFIX . 'No contact found for ID {contact}', ['module' => 'api', 'action' => 'friendships_destroy', 'contact' => $contact_id]);
  3340. throw new NotFoundException("no contact found to given ID");
  3341. }
  3342. $url = $contact["url"];
  3343. $condition = ["`uid` = ? AND (`rel` = ? OR `rel` = ?) AND (`nurl` = ? OR `alias` = ? OR `alias` = ?)",
  3344. $uid, Contact::SHARING, Contact::FRIEND, Strings::normaliseLink($url),
  3345. Strings::normaliseLink($url), $url];
  3346. $contact = DBA::selectFirst('contact', [], $condition);
  3347. if (!DBA::isResult($contact)) {
  3348. Logger::notice(API_LOG_PREFIX . 'Not following contact', ['module' => 'api', 'action' => 'friendships_destroy']);
  3349. throw new NotFoundException("Not following Contact");
  3350. }
  3351. if (!in_array($contact['network'], Protocol::NATIVE_SUPPORT)) {
  3352. Logger::notice(API_LOG_PREFIX . 'Not supported for {network}', ['module' => 'api', 'action' => 'friendships_destroy', 'network' => $contact['network']]);
  3353. throw new ExpectationFailedException("Not supported");
  3354. }
  3355. $dissolve = ($contact['rel'] == Contact::SHARING);
  3356. $owner = User::getOwnerDataById($uid);
  3357. if ($owner) {
  3358. Contact::terminateFriendship($owner, $contact, $dissolve);
  3359. }
  3360. else {
  3361. Logger::notice(API_LOG_PREFIX . 'No owner {uid} found', ['module' => 'api', 'action' => 'friendships_destroy', 'uid' => $uid]);
  3362. throw new NotFoundException("Error Processing Request");
  3363. }
  3364. // Sharing-only contacts get deleted as there no relationship any more
  3365. if ($dissolve) {
  3366. Contact::remove($contact['id']);
  3367. } else {
  3368. DBA::update('contact', ['rel' => Contact::FOLLOWER], ['id' => $contact['id']]);
  3369. }
  3370. // "uid" and "self" are only needed for some internal stuff, so remove it from here
  3371. unset($contact["uid"]);
  3372. unset($contact["self"]);
  3373. // Set screen_name since Twidere requests it
  3374. $contact["screen_name"] = $contact["nick"];
  3375. return api_format_data("friendships-destroy", $type, ['user' => $contact]);
  3376. }
  3377. api_register_func('api/friendships/destroy', 'api_friendships_destroy', true, API_METHOD_POST);
  3378. /**
  3379. *
  3380. * @param string $type Return type (atom, rss, xml, json)
  3381. * @param string $box
  3382. * @param string $verbose
  3383. *
  3384. * @return array|string
  3385. * @throws BadRequestException
  3386. * @throws ForbiddenException
  3387. * @throws ImagickException
  3388. * @throws InternalServerErrorException
  3389. * @throws UnauthorizedException
  3390. */
  3391. function api_direct_messages_box($type, $box, $verbose)
  3392. {
  3393. $a = DI::app();
  3394. if (api_user() === false) {
  3395. throw new ForbiddenException();
  3396. }
  3397. // params
  3398. $count = $_GET['count'] ?? 20;
  3399. $page = $_REQUEST['page'] ?? 1;
  3400. $since_id = $_REQUEST['since_id'] ?? 0;
  3401. $max_id = $_REQUEST['max_id'] ?? 0;
  3402. $user_id = $_REQUEST['user_id'] ?? '';
  3403. $screen_name = $_REQUEST['screen_name'] ?? '';
  3404. // caller user info
  3405. unset($_REQUEST["user_id"]);
  3406. unset($_GET["user_id"]);
  3407. unset($_REQUEST["screen_name"]);
  3408. unset($_GET["screen_name"]);
  3409. $user_info = api_get_user($a);
  3410. if ($user_info === false) {
  3411. throw new ForbiddenException();
  3412. }
  3413. $profile_url = $user_info["url"];
  3414. // pagination
  3415. $start = max(0, ($page - 1) * $count);
  3416. $sql_extra = "";
  3417. // filters
  3418. if ($box=="sentbox") {
  3419. $sql_extra = "`mail`.`from-url`='" . DBA::escape($profile_url) . "'";
  3420. } elseif ($box == "conversation") {
  3421. $sql_extra = "`mail`.`parent-uri`='" . DBA::escape($_GET['uri'] ?? '') . "'";
  3422. } elseif ($box == "all") {
  3423. $sql_extra = "true";
  3424. } elseif ($box == "inbox") {
  3425. $sql_extra = "`mail`.`from-url`!='" . DBA::escape($profile_url) . "'";
  3426. }
  3427. if ($max_id > 0) {
  3428. $sql_extra .= ' AND `mail`.`id` <= ' . intval($max_id);
  3429. }
  3430. if ($user_id != "") {
  3431. $sql_extra .= ' AND `mail`.`contact-id` = ' . intval($user_id);
  3432. } elseif ($screen_name !="") {
  3433. $sql_extra .= " AND `contact`.`nick` = '" . DBA::escape($screen_name). "'";
  3434. }
  3435. $r = q(
  3436. "SELECT `mail`.*, `contact`.`nurl` AS `contact-url` FROM `mail`,`contact` WHERE `mail`.`contact-id` = `contact`.`id` AND `mail`.`uid`=%d AND $sql_extra AND `mail`.`id` > %d ORDER BY `mail`.`id` DESC LIMIT %d,%d",
  3437. intval(api_user()),
  3438. intval($since_id),
  3439. intval($start),
  3440. intval($count)
  3441. );
  3442. if ($verbose == "true" && !DBA::isResult($r)) {
  3443. $answer = ['result' => 'error', 'message' => 'no mails available'];
  3444. return api_format_data("direct_messages_all", $type, ['$result' => $answer]);
  3445. }
  3446. $ret = [];
  3447. foreach ($r as $item) {
  3448. if ($box == "inbox" || $item['from-url'] != $profile_url) {
  3449. $recipient = $user_info;
  3450. $sender = api_get_user($a, Strings::normaliseLink($item['contact-url']));
  3451. } elseif ($box == "sentbox" || $item['from-url'] == $profile_url) {
  3452. $recipient = api_get_user($a, Strings::normaliseLink($item['contact-url']));
  3453. $sender = $user_info;
  3454. }
  3455. if (isset($recipient) && isset($sender)) {
  3456. $ret[] = api_format_messages($item, $recipient, $sender);
  3457. }
  3458. }
  3459. $data = ['direct_message' => $ret];
  3460. switch ($type) {
  3461. case "atom":
  3462. break;
  3463. case "rss":
  3464. $data = api_rss_extra($a, $data, $user_info);
  3465. break;
  3466. }
  3467. return api_format_data("direct-messages", $type, $data);
  3468. }
  3469. /**
  3470. * Returns the most recent direct messages sent by the user.
  3471. *
  3472. * @param string $type Return type (atom, rss, xml, json)
  3473. *
  3474. * @return array|string
  3475. * @throws BadRequestException
  3476. * @throws ForbiddenException
  3477. * @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-sent-message
  3478. */
  3479. function api_direct_messages_sentbox($type)
  3480. {
  3481. $verbose = !empty($_GET['friendica_verbose']) ? strtolower($_GET['friendica_verbose']) : "false";
  3482. return api_direct_messages_box($type, "sentbox", $verbose);
  3483. }
  3484. /**
  3485. * Returns the most recent direct messages sent to the user.
  3486. *
  3487. * @param string $type Return type (atom, rss, xml, json)
  3488. *
  3489. * @return array|string
  3490. * @throws BadRequestException
  3491. * @throws ForbiddenException
  3492. * @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-messages
  3493. */
  3494. function api_direct_messages_inbox($type)
  3495. {
  3496. $verbose = !empty($_GET['friendica_verbose']) ? strtolower($_GET['friendica_verbose']) : "false";
  3497. return api_direct_messages_box($type, "inbox", $verbose);
  3498. }
  3499. /**
  3500. *
  3501. * @param string $type Return type (atom, rss, xml, json)
  3502. *
  3503. * @return array|string
  3504. * @throws BadRequestException
  3505. * @throws ForbiddenException
  3506. */
  3507. function api_direct_messages_all($type)
  3508. {
  3509. $verbose = !empty($_GET['friendica_verbose']) ? strtolower($_GET['friendica_verbose']) : "false";
  3510. return api_direct_messages_box($type, "all", $verbose);
  3511. }
  3512. /**
  3513. *
  3514. * @param string $type Return type (atom, rss, xml, json)
  3515. *
  3516. * @return array|string
  3517. * @throws BadRequestException
  3518. * @throws ForbiddenException
  3519. */
  3520. function api_direct_messages_conversation($type)
  3521. {
  3522. $verbose = !empty($_GET['friendica_verbose']) ? strtolower($_GET['friendica_verbose']) : "false";
  3523. return api_direct_messages_box($type, "conversation", $verbose);
  3524. }
  3525. /// @TODO move to top of file or somewhere better
  3526. api_register_func('api/direct_messages/conversation', 'api_direct_messages_conversation', true);
  3527. api_register_func('api/direct_messages/all', 'api_direct_messages_all', true);
  3528. api_register_func('api/direct_messages/sent', 'api_direct_messages_sentbox', true);
  3529. api_register_func('api/direct_messages', 'api_direct_messages_inbox', true);
  3530. /**
  3531. * Returns an OAuth Request Token.
  3532. *
  3533. * @see https://oauth.net/core/1.0/#auth_step1
  3534. */
  3535. function api_oauth_request_token()
  3536. {
  3537. $oauth1 = new FKOAuth1();
  3538. try {
  3539. $r = $oauth1->fetch_request_token(OAuthRequest::from_request());
  3540. } catch (Exception $e) {
  3541. echo "error=" . OAuthUtil::urlencode_rfc3986($e->getMessage());
  3542. exit();
  3543. }
  3544. echo $r;
  3545. exit();
  3546. }
  3547. /**
  3548. * Returns an OAuth Access Token.
  3549. *
  3550. * @return array|string
  3551. * @see https://oauth.net/core/1.0/#auth_step3
  3552. */
  3553. function api_oauth_access_token()
  3554. {
  3555. $oauth1 = new FKOAuth1();
  3556. try {
  3557. $r = $oauth1->fetch_access_token(OAuthRequest::from_request());
  3558. } catch (Exception $e) {
  3559. echo "error=". OAuthUtil::urlencode_rfc3986($e->getMessage());
  3560. exit();
  3561. }
  3562. echo $r;
  3563. exit();
  3564. }
  3565. /// @TODO move to top of file or somewhere better
  3566. api_register_func('api/oauth/request_token', 'api_oauth_request_token', false);
  3567. api_register_func('api/oauth/access_token', 'api_oauth_access_token', false);
  3568. /**
  3569. * @brief delete a complete photoalbum with all containing photos from database through api
  3570. *
  3571. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  3572. * @return string|array
  3573. * @throws BadRequestException
  3574. * @throws ForbiddenException
  3575. * @throws InternalServerErrorException
  3576. */
  3577. function api_fr_photoalbum_delete($type)
  3578. {
  3579. if (api_user() === false) {
  3580. throw new ForbiddenException();
  3581. }
  3582. // input params
  3583. $album = $_REQUEST['album'] ?? '';
  3584. // we do not allow calls without album string
  3585. if ($album == "") {
  3586. throw new BadRequestException("no albumname specified");
  3587. }
  3588. // check if album is existing
  3589. $r = q(
  3590. "SELECT DISTINCT `resource-id` FROM `photo` WHERE `uid` = %d AND `album` = '%s'",
  3591. intval(api_user()),
  3592. DBA::escape($album)
  3593. );
  3594. if (!DBA::isResult($r)) {
  3595. throw new BadRequestException("album not available");
  3596. }
  3597. // function for setting the items to "deleted = 1" which ensures that comments, likes etc. are not shown anymore
  3598. // to the user and the contacts of the users (drop_items() performs the federation of the deletion to other networks
  3599. foreach ($r as $rr) {
  3600. $condition = ['uid' => local_user(), 'resource-id' => $rr['resource-id'], 'type' => 'photo'];
  3601. $photo_item = Item::selectFirstForUser(local_user(), ['id'], $condition);
  3602. if (!DBA::isResult($photo_item)) {
  3603. throw new InternalServerErrorException("problem with deleting items occured");
  3604. }
  3605. Item::deleteForUser(['id' => $photo_item['id']], api_user());
  3606. }
  3607. // now let's delete all photos from the album
  3608. $result = Photo::delete(['uid' => api_user(), 'album' => $album]);
  3609. // return success of deletion or error message
  3610. if ($result) {
  3611. $answer = ['result' => 'deleted', 'message' => 'album `' . $album . '` with all containing photos has been deleted.'];
  3612. return api_format_data("photoalbum_delete", $type, ['$result' => $answer]);
  3613. } else {
  3614. throw new InternalServerErrorException("unknown error - deleting from database failed");
  3615. }
  3616. }
  3617. /**
  3618. * @brief update the name of the album for all photos of an album
  3619. *
  3620. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  3621. * @return string|array
  3622. * @throws BadRequestException
  3623. * @throws ForbiddenException
  3624. * @throws InternalServerErrorException
  3625. */
  3626. function api_fr_photoalbum_update($type)
  3627. {
  3628. if (api_user() === false) {
  3629. throw new ForbiddenException();
  3630. }
  3631. // input params
  3632. $album = $_REQUEST['album'] ?? '';
  3633. $album_new = $_REQUEST['album_new'] ?? '';
  3634. // we do not allow calls without album string
  3635. if ($album == "") {
  3636. throw new BadRequestException("no albumname specified");
  3637. }
  3638. if ($album_new == "") {
  3639. throw new BadRequestException("no new albumname specified");
  3640. }
  3641. // check if album is existing
  3642. if (!Photo::exists(['uid' => api_user(), 'album' => $album])) {
  3643. throw new BadRequestException("album not available");
  3644. }
  3645. // now let's update all photos to the albumname
  3646. $result = Photo::update(['album' => $album_new], ['uid' => api_user(), 'album' => $album]);
  3647. // return success of updating or error message
  3648. if ($result) {
  3649. $answer = ['result' => 'updated', 'message' => 'album `' . $album . '` with all containing photos has been renamed to `' . $album_new . '`.'];
  3650. return api_format_data("photoalbum_update", $type, ['$result' => $answer]);
  3651. } else {
  3652. throw new InternalServerErrorException("unknown error - updating in database failed");
  3653. }
  3654. }
  3655. /**
  3656. * @brief list all photos of the authenticated user
  3657. *
  3658. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  3659. * @return string|array
  3660. * @throws ForbiddenException
  3661. * @throws InternalServerErrorException
  3662. */
  3663. function api_fr_photos_list($type)
  3664. {
  3665. if (api_user() === false) {
  3666. throw new ForbiddenException();
  3667. }
  3668. $r = q(
  3669. "SELECT `resource-id`, MAX(scale) AS `scale`, `album`, `filename`, `type`, MAX(`created`) AS `created`,
  3670. MAX(`edited`) AS `edited`, MAX(`desc`) AS `desc` FROM `photo`
  3671. WHERE `uid` = %d AND `album` != 'Contact Photos' GROUP BY `resource-id`, `album`, `filename`, `type`",
  3672. intval(local_user())
  3673. );
  3674. $typetoext = [
  3675. 'image/jpeg' => 'jpg',
  3676. 'image/png' => 'png',
  3677. 'image/gif' => 'gif'
  3678. ];
  3679. $data = ['photo'=>[]];
  3680. if (DBA::isResult($r)) {
  3681. foreach ($r as $rr) {
  3682. $photo = [];
  3683. $photo['id'] = $rr['resource-id'];
  3684. $photo['album'] = $rr['album'];
  3685. $photo['filename'] = $rr['filename'];
  3686. $photo['type'] = $rr['type'];
  3687. $thumb = DI::baseUrl() . "/photo/" . $rr['resource-id'] . "-" . $rr['scale'] . "." . $typetoext[$rr['type']];
  3688. $photo['created'] = $rr['created'];
  3689. $photo['edited'] = $rr['edited'];
  3690. $photo['desc'] = $rr['desc'];
  3691. if ($type == "xml") {
  3692. $data['photo'][] = ["@attributes" => $photo, "1" => $thumb];
  3693. } else {
  3694. $photo['thumb'] = $thumb;
  3695. $data['photo'][] = $photo;
  3696. }
  3697. }
  3698. }
  3699. return api_format_data("photos", $type, $data);
  3700. }
  3701. /**
  3702. * @brief upload a new photo or change an existing photo
  3703. *
  3704. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  3705. * @return string|array
  3706. * @throws BadRequestException
  3707. * @throws ForbiddenException
  3708. * @throws ImagickException
  3709. * @throws InternalServerErrorException
  3710. * @throws NotFoundException
  3711. */
  3712. function api_fr_photo_create_update($type)
  3713. {
  3714. if (api_user() === false) {
  3715. throw new ForbiddenException();
  3716. }
  3717. // input params
  3718. $photo_id = $_REQUEST['photo_id'] ?? null;
  3719. $desc = $_REQUEST['desc'] ?? null;
  3720. $album = $_REQUEST['album'] ?? null;
  3721. $album_new = $_REQUEST['album_new'] ?? null;
  3722. $allow_cid = $_REQUEST['allow_cid'] ?? null;
  3723. $deny_cid = $_REQUEST['deny_cid' ] ?? null;
  3724. $allow_gid = $_REQUEST['allow_gid'] ?? null;
  3725. $deny_gid = $_REQUEST['deny_gid' ] ?? null;
  3726. $visibility = !empty($_REQUEST['visibility']) && $_REQUEST['visibility'] !== "false";
  3727. // do several checks on input parameters
  3728. // we do not allow calls without album string
  3729. if ($album == null) {
  3730. throw new BadRequestException("no albumname specified");
  3731. }
  3732. // if photo_id == null --> we are uploading a new photo
  3733. if ($photo_id == null) {
  3734. $mode = "create";
  3735. // error if no media posted in create-mode
  3736. if (empty($_FILES['media'])) {
  3737. // Output error
  3738. throw new BadRequestException("no media data submitted");
  3739. }
  3740. // album_new will be ignored in create-mode
  3741. $album_new = "";
  3742. } else {
  3743. $mode = "update";
  3744. // check if photo is existing in databasei
  3745. if (!Photo::exists(['resource-id' => $photo_id, 'uid' => api_user(), 'album' => $album])) {
  3746. throw new BadRequestException("photo not available");
  3747. }
  3748. }
  3749. // checks on acl strings provided by clients
  3750. $acl_input_error = false;
  3751. $acl_input_error |= check_acl_input($allow_cid);
  3752. $acl_input_error |= check_acl_input($deny_cid);
  3753. $acl_input_error |= check_acl_input($allow_gid);
  3754. $acl_input_error |= check_acl_input($deny_gid);
  3755. if ($acl_input_error) {
  3756. throw new BadRequestException("acl data invalid");
  3757. }
  3758. // now let's upload the new media in create-mode
  3759. if ($mode == "create") {
  3760. $media = $_FILES['media'];
  3761. $data = save_media_to_database("photo", $media, $type, $album, trim($allow_cid), trim($deny_cid), trim($allow_gid), trim($deny_gid), $desc, $visibility);
  3762. // return success of updating or error message
  3763. if (!is_null($data)) {
  3764. return api_format_data("photo_create", $type, $data);
  3765. } else {
  3766. throw new InternalServerErrorException("unknown error - uploading photo failed, see Friendica log for more information");
  3767. }
  3768. }
  3769. // now let's do the changes in update-mode
  3770. if ($mode == "update") {
  3771. $updated_fields = [];
  3772. if (!is_null($desc)) {
  3773. $updated_fields['desc'] = $desc;
  3774. }
  3775. if (!is_null($album_new)) {
  3776. $updated_fields['album'] = $album_new;
  3777. }
  3778. if (!is_null($allow_cid)) {
  3779. $allow_cid = trim($allow_cid);
  3780. $updated_fields['allow_cid'] = $allow_cid;
  3781. }
  3782. if (!is_null($deny_cid)) {
  3783. $deny_cid = trim($deny_cid);
  3784. $updated_fields['deny_cid'] = $deny_cid;
  3785. }
  3786. if (!is_null($allow_gid)) {
  3787. $allow_gid = trim($allow_gid);
  3788. $updated_fields['allow_gid'] = $allow_gid;
  3789. }
  3790. if (!is_null($deny_gid)) {
  3791. $deny_gid = trim($deny_gid);
  3792. $updated_fields['deny_gid'] = $deny_gid;
  3793. }
  3794. $result = false;
  3795. if (count($updated_fields) > 0) {
  3796. $nothingtodo = false;
  3797. $result = Photo::update($updated_fields, ['uid' => api_user(), 'resource-id' => $photo_id, 'album' => $album]);
  3798. } else {
  3799. $nothingtodo = true;
  3800. }
  3801. if (!empty($_FILES['media'])) {
  3802. $nothingtodo = false;
  3803. $media = $_FILES['media'];
  3804. $data = save_media_to_database("photo", $media, $type, $album, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $desc, 0, $visibility, $photo_id);
  3805. if (!is_null($data)) {
  3806. return api_format_data("photo_update", $type, $data);
  3807. }
  3808. }
  3809. // return success of updating or error message
  3810. if ($result) {
  3811. $answer = ['result' => 'updated', 'message' => 'Image id `' . $photo_id . '` has been updated.'];
  3812. return api_format_data("photo_update", $type, ['$result' => $answer]);
  3813. } else {
  3814. if ($nothingtodo) {
  3815. $answer = ['result' => 'cancelled', 'message' => 'Nothing to update for image id `' . $photo_id . '`.'];
  3816. return api_format_data("photo_update", $type, ['$result' => $answer]);
  3817. }
  3818. throw new InternalServerErrorException("unknown error - update photo entry in database failed");
  3819. }
  3820. }
  3821. throw new InternalServerErrorException("unknown error - this error on uploading or updating a photo should never happen");
  3822. }
  3823. /**
  3824. * @brief delete a single photo from the database through api
  3825. *
  3826. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  3827. * @return string|array
  3828. * @throws BadRequestException
  3829. * @throws ForbiddenException
  3830. * @throws InternalServerErrorException
  3831. */
  3832. function api_fr_photo_delete($type)
  3833. {
  3834. if (api_user() === false) {
  3835. throw new ForbiddenException();
  3836. }
  3837. // input params
  3838. $photo_id = $_REQUEST['photo_id'] ?? null;
  3839. // do several checks on input parameters
  3840. // we do not allow calls without photo id
  3841. if ($photo_id == null) {
  3842. throw new BadRequestException("no photo_id specified");
  3843. }
  3844. // check if photo is existing in database
  3845. if (!Photo::exists(['resource-id' => $photo_id, 'uid' => api_user()])) {
  3846. throw new BadRequestException("photo not available");
  3847. }
  3848. // now we can perform on the deletion of the photo
  3849. $result = Photo::delete(['uid' => api_user(), 'resource-id' => $photo_id]);
  3850. // return success of deletion or error message
  3851. if ($result) {
  3852. // retrieve the id of the parent element (the photo element)
  3853. $condition = ['uid' => local_user(), 'resource-id' => $photo_id, 'type' => 'photo'];
  3854. $photo_item = Item::selectFirstForUser(local_user(), ['id'], $condition);
  3855. if (!DBA::isResult($photo_item)) {
  3856. throw new InternalServerErrorException("problem with deleting items occured");
  3857. }
  3858. // function for setting the items to "deleted = 1" which ensures that comments, likes etc. are not shown anymore
  3859. // to the user and the contacts of the users (drop_items() do all the necessary magic to avoid orphans in database and federate deletion)
  3860. Item::deleteForUser(['id' => $photo_item['id']], api_user());
  3861. $answer = ['result' => 'deleted', 'message' => 'photo with id `' . $photo_id . '` has been deleted from server.'];
  3862. return api_format_data("photo_delete", $type, ['$result' => $answer]);
  3863. } else {
  3864. throw new InternalServerErrorException("unknown error on deleting photo from database table");
  3865. }
  3866. }
  3867. /**
  3868. * @brief returns the details of a specified photo id, if scale is given, returns the photo data in base 64
  3869. *
  3870. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  3871. * @return string|array
  3872. * @throws BadRequestException
  3873. * @throws ForbiddenException
  3874. * @throws InternalServerErrorException
  3875. * @throws NotFoundException
  3876. */
  3877. function api_fr_photo_detail($type)
  3878. {
  3879. if (api_user() === false) {
  3880. throw new ForbiddenException();
  3881. }
  3882. if (empty($_REQUEST['photo_id'])) {
  3883. throw new BadRequestException("No photo id.");
  3884. }
  3885. $scale = (!empty($_REQUEST['scale']) ? intval($_REQUEST['scale']) : false);
  3886. $photo_id = $_REQUEST['photo_id'];
  3887. // prepare json/xml output with data from database for the requested photo
  3888. $data = prepare_photo_data($type, $scale, $photo_id);
  3889. return api_format_data("photo_detail", $type, $data);
  3890. }
  3891. /**
  3892. * Updates the user’s profile image.
  3893. *
  3894. * @brief updates the profile image for the user (either a specified profile or the default profile)
  3895. *
  3896. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  3897. *
  3898. * @return string|array
  3899. * @throws BadRequestException
  3900. * @throws ForbiddenException
  3901. * @throws ImagickException
  3902. * @throws InternalServerErrorException
  3903. * @throws NotFoundException
  3904. * @see https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_image
  3905. */
  3906. function api_account_update_profile_image($type)
  3907. {
  3908. if (api_user() === false) {
  3909. throw new ForbiddenException();
  3910. }
  3911. // input params
  3912. $profile_id = $_REQUEST['profile_id'] ?? 0;
  3913. // error if image data is missing
  3914. if (empty($_FILES['image'])) {
  3915. throw new BadRequestException("no media data submitted");
  3916. }
  3917. // check if specified profile id is valid
  3918. if ($profile_id != 0) {
  3919. $profile = DBA::selectFirst('profile', ['is-default'], ['uid' => api_user(), 'id' => $profile_id]);
  3920. // error message if specified profile id is not in database
  3921. if (!DBA::isResult($profile)) {
  3922. throw new BadRequestException("profile_id not available");
  3923. }
  3924. $is_default_profile = $profile['is-default'];
  3925. } else {
  3926. $is_default_profile = 1;
  3927. }
  3928. // get mediadata from image or media (Twitter call api/account/update_profile_image provides image)
  3929. $media = null;
  3930. if (!empty($_FILES['image'])) {
  3931. $media = $_FILES['image'];
  3932. } elseif (!empty($_FILES['media'])) {
  3933. $media = $_FILES['media'];
  3934. }
  3935. // save new profile image
  3936. $data = save_media_to_database("profileimage", $media, $type, L10n::t('Profile Photos'), "", "", "", "", "", $is_default_profile);
  3937. // get filetype
  3938. if (is_array($media['type'])) {
  3939. $filetype = $media['type'][0];
  3940. } else {
  3941. $filetype = $media['type'];
  3942. }
  3943. if ($filetype == "image/jpeg") {
  3944. $fileext = "jpg";
  3945. } elseif ($filetype == "image/png") {
  3946. $fileext = "png";
  3947. } else {
  3948. throw new InternalServerErrorException('Unsupported filetype');
  3949. }
  3950. // change specified profile or all profiles to the new resource-id
  3951. if ($is_default_profile) {
  3952. $condition = ["`profile` AND `resource-id` != ? AND `uid` = ?", $data['photo']['id'], api_user()];
  3953. Photo::update(['profile' => false], $condition);
  3954. } else {
  3955. $fields = ['photo' => DI::baseUrl() . '/photo/' . $data['photo']['id'] . '-4.' . $fileext,
  3956. 'thumb' => DI::baseUrl() . '/photo/' . $data['photo']['id'] . '-5.' . $fileext];
  3957. DBA::update('profile', $fields, ['id' => $_REQUEST['profile'], 'uid' => api_user()]);
  3958. }
  3959. Contact::updateSelfFromUserID(api_user(), true);
  3960. // Update global directory in background
  3961. $url = DI::baseUrl() . '/profile/' . DI::app()->user['nickname'];
  3962. if ($url && strlen(Config::get('system', 'directory'))) {
  3963. Worker::add(PRIORITY_LOW, "Directory", $url);
  3964. }
  3965. Worker::add(PRIORITY_LOW, 'ProfileUpdate', api_user());
  3966. // output for client
  3967. if ($data) {
  3968. return api_account_verify_credentials($type);
  3969. } else {
  3970. // SaveMediaToDatabase failed for some reason
  3971. throw new InternalServerErrorException("image upload failed");
  3972. }
  3973. }
  3974. // place api-register for photoalbum calls before 'api/friendica/photo', otherwise this function is never reached
  3975. api_register_func('api/friendica/photoalbum/delete', 'api_fr_photoalbum_delete', true, API_METHOD_DELETE);
  3976. api_register_func('api/friendica/photoalbum/update', 'api_fr_photoalbum_update', true, API_METHOD_POST);
  3977. api_register_func('api/friendica/photos/list', 'api_fr_photos_list', true);
  3978. api_register_func('api/friendica/photo/create', 'api_fr_photo_create_update', true, API_METHOD_POST);
  3979. api_register_func('api/friendica/photo/update', 'api_fr_photo_create_update', true, API_METHOD_POST);
  3980. api_register_func('api/friendica/photo/delete', 'api_fr_photo_delete', true, API_METHOD_DELETE);
  3981. api_register_func('api/friendica/photo', 'api_fr_photo_detail', true);
  3982. api_register_func('api/account/update_profile_image', 'api_account_update_profile_image', true, API_METHOD_POST);
  3983. /**
  3984. * Update user profile
  3985. *
  3986. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  3987. *
  3988. * @return array|string
  3989. * @throws BadRequestException
  3990. * @throws ForbiddenException
  3991. * @throws ImagickException
  3992. * @throws InternalServerErrorException
  3993. * @throws UnauthorizedException
  3994. */
  3995. function api_account_update_profile($type)
  3996. {
  3997. $local_user = api_user();
  3998. $api_user = api_get_user(DI::app());
  3999. if (!empty($_POST['name'])) {
  4000. DBA::update('profile', ['name' => $_POST['name']], ['uid' => $local_user]);
  4001. DBA::update('user', ['username' => $_POST['name']], ['uid' => $local_user]);
  4002. DBA::update('contact', ['name' => $_POST['name']], ['uid' => $local_user, 'self' => 1]);
  4003. DBA::update('contact', ['name' => $_POST['name']], ['id' => $api_user['id']]);
  4004. }
  4005. if (isset($_POST['description'])) {
  4006. DBA::update('profile', ['about' => $_POST['description']], ['uid' => $local_user]);
  4007. DBA::update('contact', ['about' => $_POST['description']], ['uid' => $local_user, 'self' => 1]);
  4008. DBA::update('contact', ['about' => $_POST['description']], ['id' => $api_user['id']]);
  4009. }
  4010. Worker::add(PRIORITY_LOW, 'ProfileUpdate', $local_user);
  4011. // Update global directory in background
  4012. if ($api_user['url'] && strlen(Config::get('system', 'directory'))) {
  4013. Worker::add(PRIORITY_LOW, "Directory", $api_user['url']);
  4014. }
  4015. return api_account_verify_credentials($type);
  4016. }
  4017. /// @TODO move to top of file or somewhere better
  4018. api_register_func('api/account/update_profile', 'api_account_update_profile', true, API_METHOD_POST);
  4019. /**
  4020. *
  4021. * @param string $acl_string
  4022. * @return bool
  4023. * @throws Exception
  4024. */
  4025. function check_acl_input($acl_string)
  4026. {
  4027. if (empty($acl_string)) {
  4028. return false;
  4029. }
  4030. $contact_not_found = false;
  4031. // split <x><y><z> into array of cid's
  4032. preg_match_all("/<[A-Za-z0-9]+>/", $acl_string, $array);
  4033. // check for each cid if it is available on server
  4034. $cid_array = $array[0];
  4035. foreach ($cid_array as $cid) {
  4036. $cid = str_replace("<", "", $cid);
  4037. $cid = str_replace(">", "", $cid);
  4038. $condition = ['id' => $cid, 'uid' => api_user()];
  4039. $contact_not_found |= !DBA::exists('contact', $condition);
  4040. }
  4041. return $contact_not_found;
  4042. }
  4043. /**
  4044. * @param string $mediatype
  4045. * @param array $media
  4046. * @param string $type
  4047. * @param string $album
  4048. * @param string $allow_cid
  4049. * @param string $deny_cid
  4050. * @param string $allow_gid
  4051. * @param string $deny_gid
  4052. * @param string $desc
  4053. * @param integer $profile
  4054. * @param boolean $visibility
  4055. * @param string $photo_id
  4056. * @return array
  4057. * @throws BadRequestException
  4058. * @throws ForbiddenException
  4059. * @throws ImagickException
  4060. * @throws InternalServerErrorException
  4061. * @throws NotFoundException
  4062. * @throws UnauthorizedException
  4063. */
  4064. function save_media_to_database($mediatype, $media, $type, $album, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $desc, $profile = 0, $visibility = false, $photo_id = null)
  4065. {
  4066. $visitor = 0;
  4067. $src = "";
  4068. $filetype = "";
  4069. $filename = "";
  4070. $filesize = 0;
  4071. if (is_array($media)) {
  4072. if (is_array($media['tmp_name'])) {
  4073. $src = $media['tmp_name'][0];
  4074. } else {
  4075. $src = $media['tmp_name'];
  4076. }
  4077. if (is_array($media['name'])) {
  4078. $filename = basename($media['name'][0]);
  4079. } else {
  4080. $filename = basename($media['name']);
  4081. }
  4082. if (is_array($media['size'])) {
  4083. $filesize = intval($media['size'][0]);
  4084. } else {
  4085. $filesize = intval($media['size']);
  4086. }
  4087. if (is_array($media['type'])) {
  4088. $filetype = $media['type'][0];
  4089. } else {
  4090. $filetype = $media['type'];
  4091. }
  4092. }
  4093. if ($filetype == "") {
  4094. $filetype = Images::guessType($filename);
  4095. }
  4096. $imagedata = @getimagesize($src);
  4097. if ($imagedata) {
  4098. $filetype = $imagedata['mime'];
  4099. }
  4100. Logger::log(
  4101. "File upload src: " . $src . " - filename: " . $filename .
  4102. " - size: " . $filesize . " - type: " . $filetype,
  4103. Logger::DEBUG
  4104. );
  4105. // check if there was a php upload error
  4106. if ($filesize == 0 && $media['error'] == 1) {
  4107. throw new InternalServerErrorException("image size exceeds PHP config settings, file was rejected by server");
  4108. }
  4109. // check against max upload size within Friendica instance
  4110. $maximagesize = Config::get('system', 'maximagesize');
  4111. if ($maximagesize && ($filesize > $maximagesize)) {
  4112. $formattedBytes = Strings::formatBytes($maximagesize);
  4113. throw new InternalServerErrorException("image size exceeds Friendica config setting (uploaded size: $formattedBytes)");
  4114. }
  4115. // create Photo instance with the data of the image
  4116. $imagedata = @file_get_contents($src);
  4117. $Image = new Image($imagedata, $filetype);
  4118. if (!$Image->isValid()) {
  4119. throw new InternalServerErrorException("unable to process image data");
  4120. }
  4121. // check orientation of image
  4122. $Image->orient($src);
  4123. @unlink($src);
  4124. // check max length of images on server
  4125. $max_length = Config::get('system', 'max_image_length');
  4126. if (!$max_length) {
  4127. $max_length = MAX_IMAGE_LENGTH;
  4128. }
  4129. if ($max_length > 0) {
  4130. $Image->scaleDown($max_length);
  4131. Logger::log("File upload: Scaling picture to new size " . $max_length, Logger::DEBUG);
  4132. }
  4133. $width = $Image->getWidth();
  4134. $height = $Image->getHeight();
  4135. // create a new resource-id if not already provided
  4136. $resource_id = ($photo_id == null) ? Photo::newResource() : $photo_id;
  4137. if ($mediatype == "photo") {
  4138. // upload normal image (scales 0, 1, 2)
  4139. Logger::log("photo upload: starting new photo upload", Logger::DEBUG);
  4140. $r = Photo::store($Image, local_user(), $visitor, $resource_id, $filename, $album, 0, 0, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
  4141. if (!$r) {
  4142. Logger::log("photo upload: image upload with scale 0 (original size) failed");
  4143. }
  4144. if ($width > 640 || $height > 640) {
  4145. $Image->scaleDown(640);
  4146. $r = Photo::store($Image, local_user(), $visitor, $resource_id, $filename, $album, 1, 0, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
  4147. if (!$r) {
  4148. Logger::log("photo upload: image upload with scale 1 (640x640) failed");
  4149. }
  4150. }
  4151. if ($width > 320 || $height > 320) {
  4152. $Image->scaleDown(320);
  4153. $r = Photo::store($Image, local_user(), $visitor, $resource_id, $filename, $album, 2, 0, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
  4154. if (!$r) {
  4155. Logger::log("photo upload: image upload with scale 2 (320x320) failed");
  4156. }
  4157. }
  4158. Logger::log("photo upload: new photo upload ended", Logger::DEBUG);
  4159. } elseif ($mediatype == "profileimage") {
  4160. // upload profile image (scales 4, 5, 6)
  4161. Logger::log("photo upload: starting new profile image upload", Logger::DEBUG);
  4162. if ($width > 300 || $height > 300) {
  4163. $Image->scaleDown(300);
  4164. $r = Photo::store($Image, local_user(), $visitor, $resource_id, $filename, $album, 4, $profile, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
  4165. if (!$r) {
  4166. Logger::log("photo upload: profile image upload with scale 4 (300x300) failed");
  4167. }
  4168. }
  4169. if ($width > 80 || $height > 80) {
  4170. $Image->scaleDown(80);
  4171. $r = Photo::store($Image, local_user(), $visitor, $resource_id, $filename, $album, 5, $profile, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
  4172. if (!$r) {
  4173. Logger::log("photo upload: profile image upload with scale 5 (80x80) failed");
  4174. }
  4175. }
  4176. if ($width > 48 || $height > 48) {
  4177. $Image->scaleDown(48);
  4178. $r = Photo::store($Image, local_user(), $visitor, $resource_id, $filename, $album, 6, $profile, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
  4179. if (!$r) {
  4180. Logger::log("photo upload: profile image upload with scale 6 (48x48) failed");
  4181. }
  4182. }
  4183. $Image->__destruct();
  4184. Logger::log("photo upload: new profile image upload ended", Logger::DEBUG);
  4185. }
  4186. if (isset($r) && $r) {
  4187. // create entry in 'item'-table on new uploads to enable users to comment/like/dislike the photo
  4188. if ($photo_id == null && $mediatype == "photo") {
  4189. post_photo_item($resource_id, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $filetype, $visibility);
  4190. }
  4191. // on success return image data in json/xml format (like /api/friendica/photo does when no scale is given)
  4192. return prepare_photo_data($type, false, $resource_id);
  4193. } else {
  4194. throw new InternalServerErrorException("image upload failed");
  4195. }
  4196. }
  4197. /**
  4198. *
  4199. * @param string $hash
  4200. * @param string $allow_cid
  4201. * @param string $deny_cid
  4202. * @param string $allow_gid
  4203. * @param string $deny_gid
  4204. * @param string $filetype
  4205. * @param boolean $visibility
  4206. * @throws InternalServerErrorException
  4207. */
  4208. function post_photo_item($hash, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $filetype, $visibility = false)
  4209. {
  4210. // get data about the api authenticated user
  4211. $uri = Item::newURI(intval(api_user()));
  4212. $owner_record = DBA::selectFirst('contact', [], ['uid' => api_user(), 'self' => true]);
  4213. $arr = [];
  4214. $arr['guid'] = System::createUUID();
  4215. $arr['uid'] = intval(api_user());
  4216. $arr['uri'] = $uri;
  4217. $arr['parent-uri'] = $uri;
  4218. $arr['type'] = 'photo';
  4219. $arr['wall'] = 1;
  4220. $arr['resource-id'] = $hash;
  4221. $arr['contact-id'] = $owner_record['id'];
  4222. $arr['owner-name'] = $owner_record['name'];
  4223. $arr['owner-link'] = $owner_record['url'];
  4224. $arr['owner-avatar'] = $owner_record['thumb'];
  4225. $arr['author-name'] = $owner_record['name'];
  4226. $arr['author-link'] = $owner_record['url'];
  4227. $arr['author-avatar'] = $owner_record['thumb'];
  4228. $arr['title'] = "";
  4229. $arr['allow_cid'] = $allow_cid;
  4230. $arr['allow_gid'] = $allow_gid;
  4231. $arr['deny_cid'] = $deny_cid;
  4232. $arr['deny_gid'] = $deny_gid;
  4233. $arr['visible'] = $visibility;
  4234. $arr['origin'] = 1;
  4235. $typetoext = [
  4236. 'image/jpeg' => 'jpg',
  4237. 'image/png' => 'png',
  4238. 'image/gif' => 'gif'
  4239. ];
  4240. // adds link to the thumbnail scale photo
  4241. $arr['body'] = '[url=' . DI::baseUrl() . '/photos/' . $owner_record['nick'] . '/image/' . $hash . ']'
  4242. . '[img]' . DI::baseUrl() . '/photo/' . $hash . '-' . "2" . '.'. $typetoext[$filetype] . '[/img]'
  4243. . '[/url]';
  4244. // do the magic for storing the item in the database and trigger the federation to other contacts
  4245. Item::insert($arr);
  4246. }
  4247. /**
  4248. *
  4249. * @param string $type
  4250. * @param int $scale
  4251. * @param string $photo_id
  4252. *
  4253. * @return array
  4254. * @throws BadRequestException
  4255. * @throws ForbiddenException
  4256. * @throws ImagickException
  4257. * @throws InternalServerErrorException
  4258. * @throws NotFoundException
  4259. * @throws UnauthorizedException
  4260. */
  4261. function prepare_photo_data($type, $scale, $photo_id)
  4262. {
  4263. $a = DI::app();
  4264. $user_info = api_get_user($a);
  4265. if ($user_info === false) {
  4266. throw new ForbiddenException();
  4267. }
  4268. $scale_sql = ($scale === false ? "" : sprintf("AND scale=%d", intval($scale)));
  4269. $data_sql = ($scale === false ? "" : "data, ");
  4270. // added allow_cid, allow_gid, deny_cid, deny_gid to output as string like stored in database
  4271. // clients needs to convert this in their way for further processing
  4272. $r = q(
  4273. "SELECT %s `resource-id`, `created`, `edited`, `title`, `desc`, `album`, `filename`,
  4274. `type`, `height`, `width`, `datasize`, `profile`, `allow_cid`, `deny_cid`, `allow_gid`, `deny_gid`,
  4275. MIN(`scale`) AS `minscale`, MAX(`scale`) AS `maxscale`
  4276. FROM `photo` WHERE `uid` = %d AND `resource-id` = '%s' %s GROUP BY
  4277. `resource-id`, `created`, `edited`, `title`, `desc`, `album`, `filename`,
  4278. `type`, `height`, `width`, `datasize`, `profile`, `allow_cid`, `deny_cid`, `allow_gid`, `deny_gid`",
  4279. $data_sql,
  4280. intval(local_user()),
  4281. DBA::escape($photo_id),
  4282. $scale_sql
  4283. );
  4284. $typetoext = [
  4285. 'image/jpeg' => 'jpg',
  4286. 'image/png' => 'png',
  4287. 'image/gif' => 'gif'
  4288. ];
  4289. // prepare output data for photo
  4290. if (DBA::isResult($r)) {
  4291. $data = ['photo' => $r[0]];
  4292. $data['photo']['id'] = $data['photo']['resource-id'];
  4293. if ($scale !== false) {
  4294. $data['photo']['data'] = base64_encode($data['photo']['data']);
  4295. } else {
  4296. unset($data['photo']['datasize']); //needed only with scale param
  4297. }
  4298. if ($type == "xml") {
  4299. $data['photo']['links'] = [];
  4300. for ($k = intval($data['photo']['minscale']); $k <= intval($data['photo']['maxscale']); $k++) {
  4301. $data['photo']['links'][$k . ":link"]["@attributes"] = ["type" => $data['photo']['type'],
  4302. "scale" => $k,
  4303. "href" => DI::baseUrl() . "/photo/" . $data['photo']['resource-id'] . "-" . $k . "." . $typetoext[$data['photo']['type']]];
  4304. }
  4305. } else {
  4306. $data['photo']['link'] = [];
  4307. // when we have profile images we could have only scales from 4 to 6, but index of array always needs to start with 0
  4308. $i = 0;
  4309. for ($k = intval($data['photo']['minscale']); $k <= intval($data['photo']['maxscale']); $k++) {
  4310. $data['photo']['link'][$i] = DI::baseUrl() . "/photo/" . $data['photo']['resource-id'] . "-" . $k . "." . $typetoext[$data['photo']['type']];
  4311. $i++;
  4312. }
  4313. }
  4314. unset($data['photo']['resource-id']);
  4315. unset($data['photo']['minscale']);
  4316. unset($data['photo']['maxscale']);
  4317. } else {
  4318. throw new NotFoundException();
  4319. }
  4320. // retrieve item element for getting activities (like, dislike etc.) related to photo
  4321. $condition = ['uid' => local_user(), 'resource-id' => $photo_id, 'type' => 'photo'];
  4322. $item = Item::selectFirstForUser(local_user(), ['id'], $condition);
  4323. $data['photo']['friendica_activities'] = api_format_items_activities($item, $type);
  4324. // retrieve comments on photo
  4325. $condition = ["`parent` = ? AND `uid` = ? AND (`gravity` IN (?, ?) OR `type`='photo')",
  4326. $item[0]['parent'], api_user(), GRAVITY_PARENT, GRAVITY_COMMENT];
  4327. $statuses = Item::selectForUser(api_user(), [], $condition);
  4328. // prepare output of comments
  4329. $commentData = api_format_items(Item::inArray($statuses), $user_info, false, $type);
  4330. $comments = [];
  4331. if ($type == "xml") {
  4332. $k = 0;
  4333. foreach ($commentData as $comment) {
  4334. $comments[$k++ . ":comment"] = $comment;
  4335. }
  4336. } else {
  4337. foreach ($commentData as $comment) {
  4338. $comments[] = $comment;
  4339. }
  4340. }
  4341. $data['photo']['friendica_comments'] = $comments;
  4342. // include info if rights on photo and rights on item are mismatching
  4343. $rights_mismatch = $data['photo']['allow_cid'] != $item[0]['allow_cid'] ||
  4344. $data['photo']['deny_cid'] != $item[0]['deny_cid'] ||
  4345. $data['photo']['allow_gid'] != $item[0]['allow_gid'] ||
  4346. $data['photo']['deny_cid'] != $item[0]['deny_cid'];
  4347. $data['photo']['rights_mismatch'] = $rights_mismatch;
  4348. return $data;
  4349. }
  4350. /**
  4351. * Similar as /mod/redir.php
  4352. * redirect to 'url' after dfrn auth
  4353. *
  4354. * Why this when there is mod/redir.php already?
  4355. * This use api_user() and api_login()
  4356. *
  4357. * params
  4358. * c_url: url of remote contact to auth to
  4359. * url: string, url to redirect after auth
  4360. */
  4361. function api_friendica_remoteauth()
  4362. {
  4363. $url = $_GET['url'] ?? '';
  4364. $c_url = $_GET['c_url'] ?? '';
  4365. if ($url === '' || $c_url === '') {
  4366. throw new BadRequestException("Wrong parameters.");
  4367. }
  4368. $c_url = Strings::normaliseLink($c_url);
  4369. // traditional DFRN
  4370. $contact = DBA::selectFirst('contact', [], ['uid' => api_user(), 'nurl' => $c_url]);
  4371. if (!DBA::isResult($contact)) {
  4372. throw new BadRequestException("Unknown contact");
  4373. }
  4374. $cid = $contact['id'];
  4375. $dfrn_id = $contact['issued-id'] ?: $contact['dfrn-id'];
  4376. if (($contact['network'] !== Protocol::DFRN) || empty($dfrn_id)) {
  4377. System::externalRedirect($url ?: $c_url);
  4378. }
  4379. if ($contact['duplex'] && $contact['issued-id']) {
  4380. $orig_id = $contact['issued-id'];
  4381. $dfrn_id = '1:' . $orig_id;
  4382. }
  4383. if ($contact['duplex'] && $contact['dfrn-id']) {
  4384. $orig_id = $contact['dfrn-id'];
  4385. $dfrn_id = '0:' . $orig_id;
  4386. }
  4387. $sec = Strings::getRandomHex();
  4388. $fields = ['uid' => api_user(), 'cid' => $cid, 'dfrn_id' => $dfrn_id,
  4389. 'sec' => $sec, 'expire' => time() + 45];
  4390. DBA::insert('profile_check', $fields);
  4391. Logger::info(API_LOG_PREFIX . 'for contact {contact}', ['module' => 'api', 'action' => 'friendica_remoteauth', 'contact' => $contact['name'], 'hey' => $sec]);
  4392. $dest = ($url ? '&destination_url=' . $url : '');
  4393. System::externalRedirect(
  4394. $contact['poll'] . '?dfrn_id=' . $dfrn_id
  4395. . '&dfrn_version=' . DFRN_PROTOCOL_VERSION
  4396. . '&type=profile&sec=' . $sec . $dest
  4397. );
  4398. }
  4399. api_register_func('api/friendica/remoteauth', 'api_friendica_remoteauth', true);
  4400. /**
  4401. * Return an item with announcer data if it had been announced
  4402. *
  4403. * @param array $item Item array
  4404. * @return array Item array with announce data
  4405. */
  4406. function api_get_announce($item)
  4407. {
  4408. // Quit if the item already has got a different owner and author
  4409. if ($item['owner-id'] != $item['author-id']) {
  4410. return [];
  4411. }
  4412. // Don't change original or Diaspora posts
  4413. if ($item['origin'] || in_array($item['network'], [Protocol::DIASPORA])) {
  4414. return [];
  4415. }
  4416. // Quit if we do now the original author and it had been a post from a native network
  4417. if (!empty($item['contact-uid']) && in_array($item['network'], Protocol::NATIVE_SUPPORT)) {
  4418. return [];
  4419. }
  4420. $fields = ['author-id', 'author-name', 'author-link', 'author-avatar'];
  4421. $activity = Item::activityToIndex(Activity::ANNOUNCE);
  4422. $condition = ['parent-uri' => $item['uri'], 'gravity' => GRAVITY_ACTIVITY, 'uid' => [0, $item['uid']], 'activity' => $activity];
  4423. $announce = Item::selectFirstForUser($item['uid'], $fields, $condition, ['order' => ['received' => true]]);
  4424. if (!DBA::isResult($announce)) {
  4425. return [];
  4426. }
  4427. return array_merge($item, $announce);
  4428. }
  4429. /**
  4430. * @brief Return the item shared, if the item contains only the [share] tag
  4431. *
  4432. * @param array $item Sharer item
  4433. * @return array|false Shared item or false if not a reshare
  4434. * @throws ImagickException
  4435. * @throws InternalServerErrorException
  4436. */
  4437. function api_share_as_retweet(&$item)
  4438. {
  4439. $body = trim($item["body"]);
  4440. if (Diaspora::isReshare($body, false) === false) {
  4441. if ($item['author-id'] == $item['owner-id']) {
  4442. return false;
  4443. } else {
  4444. // Reshares from OStatus, ActivityPub and Twitter
  4445. $reshared_item = $item;
  4446. $reshared_item['owner-id'] = $reshared_item['author-id'];
  4447. $reshared_item['owner-link'] = $reshared_item['author-link'];
  4448. $reshared_item['owner-name'] = $reshared_item['author-name'];
  4449. $reshared_item['owner-avatar'] = $reshared_item['author-avatar'];
  4450. return $reshared_item;
  4451. }
  4452. }
  4453. $reshared = Item::getShareArray($item);
  4454. if (empty($reshared)) {
  4455. return false;
  4456. }
  4457. $reshared_item = $item;
  4458. if (empty($reshared['shared']) || empty($reshared['profile']) || empty($reshared['author']) || empty($reshared['avatar']) || empty($reshared['posted'])) {
  4459. return false;
  4460. }
  4461. if (!empty($reshared['comment'])) {
  4462. $item['body'] = $reshared['comment'];
  4463. }
  4464. $reshared_item["share-pre-body"] = $reshared['comment'];
  4465. $reshared_item["body"] = $reshared['shared'];
  4466. $reshared_item["author-id"] = Contact::getIdForURL($reshared['profile'], 0, true);
  4467. $reshared_item["author-name"] = $reshared['author'];
  4468. $reshared_item["author-link"] = $reshared['profile'];
  4469. $reshared_item["author-avatar"] = $reshared['avatar'];
  4470. $reshared_item["plink"] = $reshared['link'] ?? '';
  4471. $reshared_item["created"] = $reshared['posted'];
  4472. $reshared_item["edited"] = $reshared['posted'];
  4473. // Try to fetch the original item
  4474. if (!empty($reshared['guid'])) {
  4475. $condition = ['guid' => $reshared['guid'], 'uid' => [0, $item['uid']]];
  4476. } elseif (!empty($reshared_item['plink']) && ($original_id = Item::searchByLink($reshared_item['plink']))) {
  4477. $condition = ['id' => $original_id];
  4478. } else {
  4479. $condition = [];
  4480. }
  4481. if (!empty($condition)) {
  4482. $original_item = Item::selectFirst([], $condition);
  4483. if (DBA::isResult($original_item)) {
  4484. $reshared_item = array_merge($reshared_item, $original_item);
  4485. }
  4486. }
  4487. return $reshared_item;
  4488. }
  4489. /**
  4490. *
  4491. * @param array $item
  4492. *
  4493. * @return array
  4494. * @throws Exception
  4495. */
  4496. function api_in_reply_to($item)
  4497. {
  4498. $in_reply_to = [];
  4499. $in_reply_to['status_id'] = null;
  4500. $in_reply_to['user_id'] = null;
  4501. $in_reply_to['status_id_str'] = null;
  4502. $in_reply_to['user_id_str'] = null;
  4503. $in_reply_to['screen_name'] = null;
  4504. if (($item['thr-parent'] != $item['uri']) && (intval($item['parent']) != intval($item['id']))) {
  4505. $parent = Item::selectFirst(['id'], ['uid' => $item['uid'], 'uri' => $item['thr-parent']]);
  4506. if (DBA::isResult($parent)) {
  4507. $in_reply_to['status_id'] = intval($parent['id']);
  4508. } else {
  4509. $in_reply_to['status_id'] = intval($item['parent']);
  4510. }
  4511. $in_reply_to['status_id_str'] = (string) intval($in_reply_to['status_id']);
  4512. $fields = ['author-nick', 'author-name', 'author-id', 'author-link'];
  4513. $parent = Item::selectFirst($fields, ['id' => $in_reply_to['status_id']]);
  4514. if (DBA::isResult($parent)) {
  4515. $in_reply_to['screen_name'] = (($parent['author-nick']) ? $parent['author-nick'] : $parent['author-name']);
  4516. $in_reply_to['user_id'] = intval($parent['author-id']);
  4517. $in_reply_to['user_id_str'] = (string) intval($parent['author-id']);
  4518. }
  4519. // There seems to be situation, where both fields are identical:
  4520. // https://github.com/friendica/friendica/issues/1010
  4521. // This is a bugfix for that.
  4522. if (intval($in_reply_to['status_id']) == intval($item['id'])) {
  4523. Logger::warning(API_LOG_PREFIX . 'ID {id} is similar to reply-to {reply-to}', ['module' => 'api', 'action' => 'in_reply_to', 'id' => $item['id'], 'reply-to' => $in_reply_to['status_id']]);
  4524. $in_reply_to['status_id'] = null;
  4525. $in_reply_to['user_id'] = null;
  4526. $in_reply_to['status_id_str'] = null;
  4527. $in_reply_to['user_id_str'] = null;
  4528. $in_reply_to['screen_name'] = null;
  4529. }
  4530. }
  4531. return $in_reply_to;
  4532. }
  4533. /**
  4534. *
  4535. * @param string $text
  4536. *
  4537. * @return string
  4538. * @throws InternalServerErrorException
  4539. */
  4540. function api_clean_plain_items($text)
  4541. {
  4542. $include_entities = strtolower($_REQUEST['include_entities'] ?? 'false');
  4543. $text = BBCode::cleanPictureLinks($text);
  4544. $URLSearchString = "^\[\]";
  4545. $text = preg_replace("/([!#@])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '$1$3', $text);
  4546. if ($include_entities == "true") {
  4547. $text = preg_replace("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '[url=$1]$1[/url]', $text);
  4548. }
  4549. // Simplify "attachment" element
  4550. $text = BBCode::removeAttachment($text);
  4551. return $text;
  4552. }
  4553. /**
  4554. *
  4555. * @param array $contacts
  4556. *
  4557. * @return void
  4558. */
  4559. function api_best_nickname(&$contacts)
  4560. {
  4561. $best_contact = [];
  4562. if (count($contacts) == 0) {
  4563. return;
  4564. }
  4565. foreach ($contacts as $contact) {
  4566. if ($contact["network"] == "") {
  4567. $contact["network"] = "dfrn";
  4568. $best_contact = [$contact];
  4569. }
  4570. }
  4571. if (sizeof($best_contact) == 0) {
  4572. foreach ($contacts as $contact) {
  4573. if ($contact["network"] == "dfrn") {
  4574. $best_contact = [$contact];
  4575. }
  4576. }
  4577. }
  4578. if (sizeof($best_contact) == 0) {
  4579. foreach ($contacts as $contact) {
  4580. if ($contact["network"] == "dspr") {
  4581. $best_contact = [$contact];
  4582. }
  4583. }
  4584. }
  4585. if (sizeof($best_contact) == 0) {
  4586. foreach ($contacts as $contact) {
  4587. if ($contact["network"] == "stat") {
  4588. $best_contact = [$contact];
  4589. }
  4590. }
  4591. }
  4592. if (sizeof($best_contact) == 0) {
  4593. foreach ($contacts as $contact) {
  4594. if ($contact["network"] == "pump") {
  4595. $best_contact = [$contact];
  4596. }
  4597. }
  4598. }
  4599. if (sizeof($best_contact) == 0) {
  4600. foreach ($contacts as $contact) {
  4601. if ($contact["network"] == "twit") {
  4602. $best_contact = [$contact];
  4603. }
  4604. }
  4605. }
  4606. if (sizeof($best_contact) == 1) {
  4607. $contacts = $best_contact;
  4608. } else {
  4609. $contacts = [$contacts[0]];
  4610. }
  4611. }
  4612. /**
  4613. * Return all or a specified group of the user with the containing contacts.
  4614. *
  4615. * @param string $type Return type (atom, rss, xml, json)
  4616. *
  4617. * @return array|string
  4618. * @throws BadRequestException
  4619. * @throws ForbiddenException
  4620. * @throws ImagickException
  4621. * @throws InternalServerErrorException
  4622. * @throws UnauthorizedException
  4623. */
  4624. function api_friendica_group_show($type)
  4625. {
  4626. $a = DI::app();
  4627. if (api_user() === false) {
  4628. throw new ForbiddenException();
  4629. }
  4630. // params
  4631. $user_info = api_get_user($a);
  4632. $gid = $_REQUEST['gid'] ?? 0;
  4633. $uid = $user_info['uid'];
  4634. // get data of the specified group id or all groups if not specified
  4635. if ($gid != 0) {
  4636. $r = q(
  4637. "SELECT * FROM `group` WHERE `deleted` = 0 AND `uid` = %d AND `id` = %d",
  4638. intval($uid),
  4639. intval($gid)
  4640. );
  4641. // error message if specified gid is not in database
  4642. if (!DBA::isResult($r)) {
  4643. throw new BadRequestException("gid not available");
  4644. }
  4645. } else {
  4646. $r = q(
  4647. "SELECT * FROM `group` WHERE `deleted` = 0 AND `uid` = %d",
  4648. intval($uid)
  4649. );
  4650. }
  4651. // loop through all groups and retrieve all members for adding data in the user array
  4652. $grps = [];
  4653. foreach ($r as $rr) {
  4654. $members = Contact::getByGroupId($rr['id']);
  4655. $users = [];
  4656. if ($type == "xml") {
  4657. $user_element = "users";
  4658. $k = 0;
  4659. foreach ($members as $member) {
  4660. $user = api_get_user($a, $member['nurl']);
  4661. $users[$k++.":user"] = $user;
  4662. }
  4663. } else {
  4664. $user_element = "user";
  4665. foreach ($members as $member) {
  4666. $user = api_get_user($a, $member['nurl']);
  4667. $users[] = $user;
  4668. }
  4669. }
  4670. $grps[] = ['name' => $rr['name'], 'gid' => $rr['id'], $user_element => $users];
  4671. }
  4672. return api_format_data("groups", $type, ['group' => $grps]);
  4673. }
  4674. api_register_func('api/friendica/group_show', 'api_friendica_group_show', true);
  4675. /**
  4676. * Delete the specified group of the user.
  4677. *
  4678. * @param string $type Return type (atom, rss, xml, json)
  4679. *
  4680. * @return array|string
  4681. * @throws BadRequestException
  4682. * @throws ForbiddenException
  4683. * @throws ImagickException
  4684. * @throws InternalServerErrorException
  4685. * @throws UnauthorizedException
  4686. */
  4687. function api_friendica_group_delete($type)
  4688. {
  4689. $a = DI::app();
  4690. if (api_user() === false) {
  4691. throw new ForbiddenException();
  4692. }
  4693. // params
  4694. $user_info = api_get_user($a);
  4695. $gid = $_REQUEST['gid'] ?? 0;
  4696. $name = $_REQUEST['name'] ?? '';
  4697. $uid = $user_info['uid'];
  4698. // error if no gid specified
  4699. if ($gid == 0 || $name == "") {
  4700. throw new BadRequestException('gid or name not specified');
  4701. }
  4702. // get data of the specified group id
  4703. $r = q(
  4704. "SELECT * FROM `group` WHERE `uid` = %d AND `id` = %d",
  4705. intval($uid),
  4706. intval($gid)
  4707. );
  4708. // error message if specified gid is not in database
  4709. if (!DBA::isResult($r)) {
  4710. throw new BadRequestException('gid not available');
  4711. }
  4712. // get data of the specified group id and group name
  4713. $rname = q(
  4714. "SELECT * FROM `group` WHERE `uid` = %d AND `id` = %d AND `name` = '%s'",
  4715. intval($uid),
  4716. intval($gid),
  4717. DBA::escape($name)
  4718. );
  4719. // error message if specified gid is not in database
  4720. if (!DBA::isResult($rname)) {
  4721. throw new BadRequestException('wrong group name');
  4722. }
  4723. // delete group
  4724. $ret = Group::removeByName($uid, $name);
  4725. if ($ret) {
  4726. // return success
  4727. $success = ['success' => $ret, 'gid' => $gid, 'name' => $name, 'status' => 'deleted', 'wrong users' => []];
  4728. return api_format_data("group_delete", $type, ['result' => $success]);
  4729. } else {
  4730. throw new BadRequestException('other API error');
  4731. }
  4732. }
  4733. api_register_func('api/friendica/group_delete', 'api_friendica_group_delete', true, API_METHOD_DELETE);
  4734. /**
  4735. * Delete a group.
  4736. *
  4737. * @param string $type Return type (atom, rss, xml, json)
  4738. *
  4739. * @return array|string
  4740. * @throws BadRequestException
  4741. * @throws ForbiddenException
  4742. * @throws ImagickException
  4743. * @throws InternalServerErrorException
  4744. * @throws UnauthorizedException
  4745. * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-destroy
  4746. */
  4747. function api_lists_destroy($type)
  4748. {
  4749. $a = DI::app();
  4750. if (api_user() === false) {
  4751. throw new ForbiddenException();
  4752. }
  4753. // params
  4754. $user_info = api_get_user($a);
  4755. $gid = $_REQUEST['list_id'] ?? 0;
  4756. $uid = $user_info['uid'];
  4757. // error if no gid specified
  4758. if ($gid == 0) {
  4759. throw new BadRequestException('gid not specified');
  4760. }
  4761. // get data of the specified group id
  4762. $group = DBA::selectFirst('group', [], ['uid' => $uid, 'id' => $gid]);
  4763. // error message if specified gid is not in database
  4764. if (!$group) {
  4765. throw new BadRequestException('gid not available');
  4766. }
  4767. if (Group::remove($gid)) {
  4768. $list = [
  4769. 'name' => $group['name'],
  4770. 'id' => intval($gid),
  4771. 'id_str' => (string) $gid,
  4772. 'user' => $user_info
  4773. ];
  4774. return api_format_data("lists", $type, ['lists' => $list]);
  4775. }
  4776. }
  4777. api_register_func('api/lists/destroy', 'api_lists_destroy', true, API_METHOD_DELETE);
  4778. /**
  4779. * Add a new group to the database.
  4780. *
  4781. * @param string $name Group name
  4782. * @param int $uid User ID
  4783. * @param array $users List of users to add to the group
  4784. *
  4785. * @return array
  4786. * @throws BadRequestException
  4787. */
  4788. function group_create($name, $uid, $users = [])
  4789. {
  4790. // error if no name specified
  4791. if ($name == "") {
  4792. throw new BadRequestException('group name not specified');
  4793. }
  4794. // get data of the specified group name
  4795. $rname = q(
  4796. "SELECT * FROM `group` WHERE `uid` = %d AND `name` = '%s' AND `deleted` = 0",
  4797. intval($uid),
  4798. DBA::escape($name)
  4799. );
  4800. // error message if specified group name already exists
  4801. if (DBA::isResult($rname)) {
  4802. throw new BadRequestException('group name already exists');
  4803. }
  4804. // check if specified group name is a deleted group
  4805. $rname = q(
  4806. "SELECT * FROM `group` WHERE `uid` = %d AND `name` = '%s' AND `deleted` = 1",
  4807. intval($uid),
  4808. DBA::escape($name)
  4809. );
  4810. // error message if specified group name already exists
  4811. if (DBA::isResult($rname)) {
  4812. $reactivate_group = true;
  4813. }
  4814. // create group
  4815. $ret = Group::create($uid, $name);
  4816. if ($ret) {
  4817. $gid = Group::getIdByName($uid, $name);
  4818. } else {
  4819. throw new BadRequestException('other API error');
  4820. }
  4821. // add members
  4822. $erroraddinguser = false;
  4823. $errorusers = [];
  4824. foreach ($users as $user) {
  4825. $cid = $user['cid'];
  4826. // check if user really exists as contact
  4827. $contact = q(
  4828. "SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d",
  4829. intval($cid),
  4830. intval($uid)
  4831. );
  4832. if (count($contact)) {
  4833. Group::addMember($gid, $cid);
  4834. } else {
  4835. $erroraddinguser = true;
  4836. $errorusers[] = $cid;
  4837. }
  4838. }
  4839. // return success message incl. missing users in array
  4840. $status = ($erroraddinguser ? "missing user" : ((isset($reactivate_group) && $reactivate_group) ? "reactivated" : "ok"));
  4841. return ['success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers];
  4842. }
  4843. /**
  4844. * Create the specified group with the posted array of contacts.
  4845. *
  4846. * @param string $type Return type (atom, rss, xml, json)
  4847. *
  4848. * @return array|string
  4849. * @throws BadRequestException
  4850. * @throws ForbiddenException
  4851. * @throws ImagickException
  4852. * @throws InternalServerErrorException
  4853. * @throws UnauthorizedException
  4854. */
  4855. function api_friendica_group_create($type)
  4856. {
  4857. $a = DI::app();
  4858. if (api_user() === false) {
  4859. throw new ForbiddenException();
  4860. }
  4861. // params
  4862. $user_info = api_get_user($a);
  4863. $name = $_REQUEST['name'] ?? '';
  4864. $uid = $user_info['uid'];
  4865. $json = json_decode($_POST['json'], true);
  4866. $users = $json['user'];
  4867. $success = group_create($name, $uid, $users);
  4868. return api_format_data("group_create", $type, ['result' => $success]);
  4869. }
  4870. api_register_func('api/friendica/group_create', 'api_friendica_group_create', true, API_METHOD_POST);
  4871. /**
  4872. * Create a new group.
  4873. *
  4874. * @param string $type Return type (atom, rss, xml, json)
  4875. *
  4876. * @return array|string
  4877. * @throws BadRequestException
  4878. * @throws ForbiddenException
  4879. * @throws ImagickException
  4880. * @throws InternalServerErrorException
  4881. * @throws UnauthorizedException
  4882. * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-create
  4883. */
  4884. function api_lists_create($type)
  4885. {
  4886. $a = DI::app();
  4887. if (api_user() === false) {
  4888. throw new ForbiddenException();
  4889. }
  4890. // params
  4891. $user_info = api_get_user($a);
  4892. $name = $_REQUEST['name'] ?? '';
  4893. $uid = $user_info['uid'];
  4894. $success = group_create($name, $uid);
  4895. if ($success['success']) {
  4896. $grp = [
  4897. 'name' => $success['name'],
  4898. 'id' => intval($success['gid']),
  4899. 'id_str' => (string) $success['gid'],
  4900. 'user' => $user_info
  4901. ];
  4902. return api_format_data("lists", $type, ['lists'=>$grp]);
  4903. }
  4904. }
  4905. api_register_func('api/lists/create', 'api_lists_create', true, API_METHOD_POST);
  4906. /**
  4907. * Update the specified group with the posted array of contacts.
  4908. *
  4909. * @param string $type Return type (atom, rss, xml, json)
  4910. *
  4911. * @return array|string
  4912. * @throws BadRequestException
  4913. * @throws ForbiddenException
  4914. * @throws ImagickException
  4915. * @throws InternalServerErrorException
  4916. * @throws UnauthorizedException
  4917. */
  4918. function api_friendica_group_update($type)
  4919. {
  4920. $a = DI::app();
  4921. if (api_user() === false) {
  4922. throw new ForbiddenException();
  4923. }
  4924. // params
  4925. $user_info = api_get_user($a);
  4926. $uid = $user_info['uid'];
  4927. $gid = $_REQUEST['gid'] ?? 0;
  4928. $name = $_REQUEST['name'] ?? '';
  4929. $json = json_decode($_POST['json'], true);
  4930. $users = $json['user'];
  4931. // error if no name specified
  4932. if ($name == "") {
  4933. throw new BadRequestException('group name not specified');
  4934. }
  4935. // error if no gid specified
  4936. if ($gid == "") {
  4937. throw new BadRequestException('gid not specified');
  4938. }
  4939. // remove members
  4940. $members = Contact::getByGroupId($gid);
  4941. foreach ($members as $member) {
  4942. $cid = $member['id'];
  4943. foreach ($users as $user) {
  4944. $found = ($user['cid'] == $cid ? true : false);
  4945. }
  4946. if (!isset($found) || !$found) {
  4947. Group::removeMemberByName($uid, $name, $cid);
  4948. }
  4949. }
  4950. // add members
  4951. $erroraddinguser = false;
  4952. $errorusers = [];
  4953. foreach ($users as $user) {
  4954. $cid = $user['cid'];
  4955. // check if user really exists as contact
  4956. $contact = q(
  4957. "SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d",
  4958. intval($cid),
  4959. intval($uid)
  4960. );
  4961. if (count($contact)) {
  4962. Group::addMember($gid, $cid);
  4963. } else {
  4964. $erroraddinguser = true;
  4965. $errorusers[] = $cid;
  4966. }
  4967. }
  4968. // return success message incl. missing users in array
  4969. $status = ($erroraddinguser ? "missing user" : "ok");
  4970. $success = ['success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers];
  4971. return api_format_data("group_update", $type, ['result' => $success]);
  4972. }
  4973. api_register_func('api/friendica/group_update', 'api_friendica_group_update', true, API_METHOD_POST);
  4974. /**
  4975. * Update information about a group.
  4976. *
  4977. * @param string $type Return type (atom, rss, xml, json)
  4978. *
  4979. * @return array|string
  4980. * @throws BadRequestException
  4981. * @throws ForbiddenException
  4982. * @throws ImagickException
  4983. * @throws InternalServerErrorException
  4984. * @throws UnauthorizedException
  4985. * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-update
  4986. */
  4987. function api_lists_update($type)
  4988. {
  4989. $a = DI::app();
  4990. if (api_user() === false) {
  4991. throw new ForbiddenException();
  4992. }
  4993. // params
  4994. $user_info = api_get_user($a);
  4995. $gid = $_REQUEST['list_id'] ?? 0;
  4996. $name = $_REQUEST['name'] ?? '';
  4997. $uid = $user_info['uid'];
  4998. // error if no gid specified
  4999. if ($gid == 0) {
  5000. throw new BadRequestException('gid not specified');
  5001. }
  5002. // get data of the specified group id
  5003. $group = DBA::selectFirst('group', [], ['uid' => $uid, 'id' => $gid]);
  5004. // error message if specified gid is not in database
  5005. if (!$group) {
  5006. throw new BadRequestException('gid not available');
  5007. }
  5008. if (Group::update($gid, $name)) {
  5009. $list = [
  5010. 'name' => $name,
  5011. 'id' => intval($gid),
  5012. 'id_str' => (string) $gid,
  5013. 'user' => $user_info
  5014. ];
  5015. return api_format_data("lists", $type, ['lists' => $list]);
  5016. }
  5017. }
  5018. api_register_func('api/lists/update', 'api_lists_update', true, API_METHOD_POST);
  5019. /**
  5020. *
  5021. * @param string $type Return type (atom, rss, xml, json)
  5022. *
  5023. * @return array|string
  5024. * @throws BadRequestException
  5025. * @throws ForbiddenException
  5026. * @throws ImagickException
  5027. * @throws InternalServerErrorException
  5028. */
  5029. function api_friendica_activity($type)
  5030. {
  5031. $a = DI::app();
  5032. if (api_user() === false) {
  5033. throw new ForbiddenException();
  5034. }
  5035. $verb = strtolower($a->argv[3]);
  5036. $verb = preg_replace("|\..*$|", "", $verb);
  5037. $id = $_REQUEST['id'] ?? 0;
  5038. $res = Item::performLike($id, $verb);
  5039. if ($res) {
  5040. if ($type == "xml") {
  5041. $ok = "true";
  5042. } else {
  5043. $ok = "ok";
  5044. }
  5045. return api_format_data('ok', $type, ['ok' => $ok]);
  5046. } else {
  5047. throw new BadRequestException('Error adding activity');
  5048. }
  5049. }
  5050. /// @TODO move to top of file or somewhere better
  5051. api_register_func('api/friendica/activity/like', 'api_friendica_activity', true, API_METHOD_POST);
  5052. api_register_func('api/friendica/activity/dislike', 'api_friendica_activity', true, API_METHOD_POST);
  5053. api_register_func('api/friendica/activity/attendyes', 'api_friendica_activity', true, API_METHOD_POST);
  5054. api_register_func('api/friendica/activity/attendno', 'api_friendica_activity', true, API_METHOD_POST);
  5055. api_register_func('api/friendica/activity/attendmaybe', 'api_friendica_activity', true, API_METHOD_POST);
  5056. api_register_func('api/friendica/activity/unlike', 'api_friendica_activity', true, API_METHOD_POST);
  5057. api_register_func('api/friendica/activity/undislike', 'api_friendica_activity', true, API_METHOD_POST);
  5058. api_register_func('api/friendica/activity/unattendyes', 'api_friendica_activity', true, API_METHOD_POST);
  5059. api_register_func('api/friendica/activity/unattendno', 'api_friendica_activity', true, API_METHOD_POST);
  5060. api_register_func('api/friendica/activity/unattendmaybe', 'api_friendica_activity', true, API_METHOD_POST);
  5061. /**
  5062. * @brief Returns notifications
  5063. *
  5064. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  5065. * @return string|array
  5066. * @throws BadRequestException
  5067. * @throws ForbiddenException
  5068. * @throws InternalServerErrorException
  5069. */
  5070. function api_friendica_notification($type)
  5071. {
  5072. $a = DI::app();
  5073. if (api_user() === false) {
  5074. throw new ForbiddenException();
  5075. }
  5076. if ($a->argc!==3) {
  5077. throw new BadRequestException("Invalid argument count");
  5078. }
  5079. $notes = DI::notify()->getAll([], ['seen' => 'ASC', 'date' => 'DESC'], 50);
  5080. if ($type == "xml") {
  5081. $xmlnotes = [];
  5082. if (!empty($notes)) {
  5083. foreach ($notes as $note) {
  5084. $xmlnotes[] = ["@attributes" => $note];
  5085. }
  5086. }
  5087. $notes = $xmlnotes;
  5088. }
  5089. return api_format_data("notes", $type, ['note' => $notes]);
  5090. }
  5091. /**
  5092. * POST request with 'id' param as notification id
  5093. *
  5094. * @brief Set notification as seen and returns associated item (if possible)
  5095. *
  5096. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  5097. * @return string|array
  5098. * @throws BadRequestException
  5099. * @throws ForbiddenException
  5100. * @throws ImagickException
  5101. * @throws InternalServerErrorException
  5102. * @throws UnauthorizedException
  5103. */
  5104. function api_friendica_notification_seen($type)
  5105. {
  5106. $a = DI::app();
  5107. $user_info = api_get_user($a);
  5108. if (api_user() === false || $user_info === false) {
  5109. throw new ForbiddenException();
  5110. }
  5111. if ($a->argc!==4) {
  5112. throw new BadRequestException("Invalid argument count");
  5113. }
  5114. $id = (!empty($_REQUEST['id']) ? intval($_REQUEST['id']) : 0);
  5115. $nm = DI::notify();
  5116. $note = $nm->getByID($id);
  5117. if (is_null($note)) {
  5118. throw new BadRequestException("Invalid argument");
  5119. }
  5120. $nm->setSeen($note);
  5121. if ($note['otype']=='item') {
  5122. // would be really better with an ItemsManager and $im->getByID() :-P
  5123. $item = Item::selectFirstForUser(api_user(), [], ['id' => $note['iid'], 'uid' => api_user()]);
  5124. if (DBA::isResult($item)) {
  5125. // we found the item, return it to the user
  5126. $ret = api_format_items([$item], $user_info, false, $type);
  5127. $data = ['status' => $ret];
  5128. return api_format_data("status", $type, $data);
  5129. }
  5130. // the item can't be found, but we set the note as seen, so we count this as a success
  5131. }
  5132. return api_format_data('result', $type, ['result' => "success"]);
  5133. }
  5134. /// @TODO move to top of file or somewhere better
  5135. api_register_func('api/friendica/notification/seen', 'api_friendica_notification_seen', true, API_METHOD_POST);
  5136. api_register_func('api/friendica/notification', 'api_friendica_notification', true, API_METHOD_GET);
  5137. /**
  5138. * @brief update a direct_message to seen state
  5139. *
  5140. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  5141. * @return string|array (success result=ok, error result=error with error message)
  5142. * @throws BadRequestException
  5143. * @throws ForbiddenException
  5144. * @throws ImagickException
  5145. * @throws InternalServerErrorException
  5146. * @throws UnauthorizedException
  5147. */
  5148. function api_friendica_direct_messages_setseen($type)
  5149. {
  5150. $a = DI::app();
  5151. if (api_user() === false) {
  5152. throw new ForbiddenException();
  5153. }
  5154. // params
  5155. $user_info = api_get_user($a);
  5156. $uid = $user_info['uid'];
  5157. $id = $_REQUEST['id'] ?? 0;
  5158. // return error if id is zero
  5159. if ($id == "") {
  5160. $answer = ['result' => 'error', 'message' => 'message id not specified'];
  5161. return api_format_data("direct_messages_setseen", $type, ['$result' => $answer]);
  5162. }
  5163. // error message if specified id is not in database
  5164. if (!DBA::exists('mail', ['id' => $id, 'uid' => $uid])) {
  5165. $answer = ['result' => 'error', 'message' => 'message id not in database'];
  5166. return api_format_data("direct_messages_setseen", $type, ['$result' => $answer]);
  5167. }
  5168. // update seen indicator
  5169. $result = DBA::update('mail', ['seen' => true], ['id' => $id]);
  5170. if ($result) {
  5171. // return success
  5172. $answer = ['result' => 'ok', 'message' => 'message set to seen'];
  5173. return api_format_data("direct_message_setseen", $type, ['$result' => $answer]);
  5174. } else {
  5175. $answer = ['result' => 'error', 'message' => 'unknown error'];
  5176. return api_format_data("direct_messages_setseen", $type, ['$result' => $answer]);
  5177. }
  5178. }
  5179. /// @TODO move to top of file or somewhere better
  5180. api_register_func('api/friendica/direct_messages_setseen', 'api_friendica_direct_messages_setseen', true);
  5181. /**
  5182. * @brief search for direct_messages containing a searchstring through api
  5183. *
  5184. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  5185. * @param string $box
  5186. * @return string|array (success: success=true if found and search_result contains found messages,
  5187. * success=false if nothing was found, search_result='nothing found',
  5188. * error: result=error with error message)
  5189. * @throws BadRequestException
  5190. * @throws ForbiddenException
  5191. * @throws ImagickException
  5192. * @throws InternalServerErrorException
  5193. * @throws UnauthorizedException
  5194. */
  5195. function api_friendica_direct_messages_search($type, $box = "")
  5196. {
  5197. $a = DI::app();
  5198. if (api_user() === false) {
  5199. throw new ForbiddenException();
  5200. }
  5201. // params
  5202. $user_info = api_get_user($a);
  5203. $searchstring = $_REQUEST['searchstring'] ?? '';
  5204. $uid = $user_info['uid'];
  5205. // error if no searchstring specified
  5206. if ($searchstring == "") {
  5207. $answer = ['result' => 'error', 'message' => 'searchstring not specified'];
  5208. return api_format_data("direct_messages_search", $type, ['$result' => $answer]);
  5209. }
  5210. // get data for the specified searchstring
  5211. $r = q(
  5212. "SELECT `mail`.*, `contact`.`nurl` AS `contact-url` FROM `mail`,`contact` WHERE `mail`.`contact-id` = `contact`.`id` AND `mail`.`uid`=%d AND `body` LIKE '%s' ORDER BY `mail`.`id` DESC",
  5213. intval($uid),
  5214. DBA::escape('%'.$searchstring.'%')
  5215. );
  5216. $profile_url = $user_info["url"];
  5217. // message if nothing was found
  5218. if (!DBA::isResult($r)) {
  5219. $success = ['success' => false, 'search_results' => 'problem with query'];
  5220. } elseif (count($r) == 0) {
  5221. $success = ['success' => false, 'search_results' => 'nothing found'];
  5222. } else {
  5223. $ret = [];
  5224. foreach ($r as $item) {
  5225. if ($box == "inbox" || $item['from-url'] != $profile_url) {
  5226. $recipient = $user_info;
  5227. $sender = api_get_user($a, Strings::normaliseLink($item['contact-url']));
  5228. } elseif ($box == "sentbox" || $item['from-url'] == $profile_url) {
  5229. $recipient = api_get_user($a, Strings::normaliseLink($item['contact-url']));
  5230. $sender = $user_info;
  5231. }
  5232. if (isset($recipient) && isset($sender)) {
  5233. $ret[] = api_format_messages($item, $recipient, $sender);
  5234. }
  5235. }
  5236. $success = ['success' => true, 'search_results' => $ret];
  5237. }
  5238. return api_format_data("direct_message_search", $type, ['$result' => $success]);
  5239. }
  5240. /// @TODO move to top of file or somewhere better
  5241. api_register_func('api/friendica/direct_messages_search', 'api_friendica_direct_messages_search', true);
  5242. /**
  5243. * @brief return data of all the profiles a user has to the client
  5244. *
  5245. * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
  5246. * @return string|array
  5247. * @throws BadRequestException
  5248. * @throws ForbiddenException
  5249. * @throws ImagickException
  5250. * @throws InternalServerErrorException
  5251. * @throws UnauthorizedException
  5252. */
  5253. function api_friendica_profile_show($type)
  5254. {
  5255. $a = DI::app();
  5256. if (api_user() === false) {
  5257. throw new ForbiddenException();
  5258. }
  5259. // input params
  5260. $profile_id = $_REQUEST['profile_id'] ?? 0;
  5261. // retrieve general information about profiles for user
  5262. $multi_profiles = Feature::isEnabled(api_user(), 'multi_profiles');
  5263. $directory = Config::get('system', 'directory');
  5264. // get data of the specified profile id or all profiles of the user if not specified
  5265. if ($profile_id != 0) {
  5266. $r = Profile::getById(api_user(), $profile_id);
  5267. // error message if specified gid is not in database
  5268. if (!DBA::isResult($r)) {
  5269. throw new BadRequestException("profile_id not available");
  5270. }
  5271. } else {
  5272. $r = Profile::getListByUser(api_user());
  5273. }
  5274. // loop through all returned profiles and retrieve data and users
  5275. $k = 0;
  5276. $profiles = [];
  5277. if (DBA::isResult($r)) {
  5278. foreach ($r as $rr) {
  5279. $profile = api_format_items_profiles($rr);
  5280. // select all users from contact table, loop and prepare standard return for user data
  5281. $users = [];
  5282. $nurls = Contact::selectToArray(['id', 'nurl'], ['uid' => api_user(), 'profile-id' => $rr['id']]);
  5283. foreach ($nurls as $nurl) {
  5284. $user = api_get_user($a, $nurl['nurl']);
  5285. ($type == "xml") ? $users[$k++ . ":user"] = $user : $users[] = $user;
  5286. }
  5287. $profile['users'] = $users;
  5288. // add prepared profile data to array for final return
  5289. if ($type == "xml") {
  5290. $profiles[$k++ . ":profile"] = $profile;
  5291. } else {
  5292. $profiles[] = $profile;
  5293. }
  5294. }
  5295. }
  5296. // return settings, authenticated user and profiles data
  5297. $self = DBA::selectFirst('contact', ['nurl'], ['uid' => api_user(), 'self' => true]);
  5298. $result = ['multi_profiles' => $multi_profiles ? true : false,
  5299. 'global_dir' => $directory,
  5300. 'friendica_owner' => api_get_user($a, $self['nurl']),
  5301. 'profiles' => $profiles];
  5302. return api_format_data("friendica_profiles", $type, ['$result' => $result]);
  5303. }
  5304. api_register_func('api/friendica/profile/show', 'api_friendica_profile_show', true, API_METHOD_GET);
  5305. /**
  5306. * Returns a list of saved searches.
  5307. *
  5308. * @see https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-saved_searches-list
  5309. *
  5310. * @param string $type Return format: json or xml
  5311. *
  5312. * @return string|array
  5313. * @throws Exception
  5314. */
  5315. function api_saved_searches_list($type)
  5316. {
  5317. $terms = DBA::select('search', ['id', 'term'], ['uid' => local_user()]);
  5318. $result = [];
  5319. while ($term = DBA::fetch($terms)) {
  5320. $result[] = [
  5321. 'created_at' => api_date(time()),
  5322. 'id' => intval($term['id']),
  5323. 'id_str' => $term['id'],
  5324. 'name' => $term['term'],
  5325. 'position' => null,
  5326. 'query' => $term['term']
  5327. ];
  5328. }
  5329. DBA::close($terms);
  5330. return api_format_data("terms", $type, ['terms' => $result]);
  5331. }
  5332. /// @TODO move to top of file or somewhere better
  5333. api_register_func('api/saved_searches/list', 'api_saved_searches_list', true);
  5334. /*
  5335. * Bind comment numbers(friendica_comments: Int) on each statuses page of *_timeline / favorites / search
  5336. *
  5337. * @brief Number of comments
  5338. *
  5339. * @param object $data [Status, Status]
  5340. *
  5341. * @return void
  5342. */
  5343. function bindComments(&$data)
  5344. {
  5345. if (count($data) == 0) {
  5346. return;
  5347. }
  5348. $ids = [];
  5349. $comments = [];
  5350. foreach ($data as $item) {
  5351. $ids[] = $item['id'];
  5352. }
  5353. $idStr = DBA::escape(implode(', ', $ids));
  5354. $sql = "SELECT `parent`, COUNT(*) as comments FROM `item` WHERE `parent` IN ($idStr) AND `deleted` = ? AND `gravity`= ? GROUP BY `parent`";
  5355. $items = DBA::p($sql, 0, GRAVITY_COMMENT);
  5356. $itemsData = DBA::toArray($items);
  5357. foreach ($itemsData as $item) {
  5358. $comments[$item['parent']] = $item['comments'];
  5359. }
  5360. foreach ($data as $idx => $item) {
  5361. $id = $item['id'];
  5362. $data[$idx]['friendica_comments'] = isset($comments[$id]) ? $comments[$id] : 0;
  5363. }
  5364. }
  5365. /*
  5366. @TODO Maybe open to implement?
  5367. To.Do:
  5368. [pagename] => api/1.1/statuses/lookup.json
  5369. [id] => 605138389168451584
  5370. [include_cards] => true
  5371. [cards_platform] => Android-12
  5372. [include_entities] => true
  5373. [include_my_retweet] => 1
  5374. [include_rts] => 1
  5375. [include_reply_count] => true
  5376. [include_descendent_reply_count] => true
  5377. (?)
  5378. Not implemented by now:
  5379. statuses/retweets_of_me
  5380. friendships/create
  5381. friendships/destroy
  5382. friendships/exists
  5383. friendships/show
  5384. account/update_location
  5385. account/update_profile_background_image
  5386. blocks/create
  5387. blocks/destroy
  5388. friendica/profile/update
  5389. friendica/profile/create
  5390. friendica/profile/delete
  5391. Not implemented in status.net:
  5392. statuses/retweeted_to_me
  5393. statuses/retweeted_by_me
  5394. direct_messages/destroy
  5395. account/end_session
  5396. account/update_delivery_device
  5397. notifications/follow
  5398. notifications/leave
  5399. blocks/exists
  5400. blocks/blocking
  5401. lists
  5402. */