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.

6079 lines
174KB

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