1
0
Fork 0

Merge pull request #2216 from fabrixxm/feature-api-like

Works on API and a small refractor, updated docs
This commit is contained in:
Tobias Diekershoff 2015-12-28 18:39:21 +01:00
commit 99861b9fa6
10 changed files with 1436 additions and 850 deletions

6
.gitignore vendored
View file

@ -12,8 +12,8 @@ addon
*~
robots.txt
#ignore documentation, it should be newly built
doc/api
#ignore documentation, it should be newly built
doc/html
#ignore reports, should be generted with every build
report/
@ -23,7 +23,7 @@ report/
.buildpath
.externalToolBuilders
.settings
#ignore OSX .DS_Store files
#ignore OSX .DS_Store files
.DS_Store
/nbproject/private/

View file

@ -1,6 +1,6 @@
Implemented API calls
Friendica API
===
The Friendica API aims to be compatible to the [GNU Social API](http://skilledtests.com/wiki/Twitter-compatible_API) and the [Twitter API](https://dev.twitter.com/rest/public).
The Friendica API aims to be compatible to the [GNU Social API](http://skilledtests.com/wiki/Twitter-compatible_API) and the [Twitter API](https://dev.twitter.com/rest/public).
Please refer to the linked documentation for further information.
@ -24,13 +24,45 @@ Please refer to the linked documentation for further information.
* cid: Contact id of the user (important for "contact_allow" and "contact_deny")
* network: network of the user
#### Errors
When an error occour in API call, an HTTP error code is returned, with an error message
Usually:
- 400 Bad Request: if parameter are missing or items can't be found
- 403 Forbidden: if authenticated user is missing
- 405 Method Not Allowed: if API was called with invalid method, eg. GET when API require POST
- 501 Not Implemented: if requested API doesn't exists
- 500 Internal Server Error: on other error contitions
Error body is
json:
```
{
"error": "Specific error message",
"request": "API path requested",
"code": "HTTP error code"
}
```
xml:
```
<status>
<error>Specific error message</error>
<request>API path requested</request>
<code>HTTP error code</code>
</status>
```
---
### account/rate_limit_status
---
### account/verify_credentials
#### Parameters
* skip_status: Don't show the "status" field. (Default: false)
* include_entities: "true" shows entities for pictures and links (Default: false)
---
### conversation/show
Unofficial Twitter command. It shows all direct answers (excluding the original post) to a given id.
@ -43,10 +75,11 @@ Unofficial Twitter command. It shows all direct answers (excluding the original
* include_entities: "true" shows entities for pictures and links (Default: false)
#### Unsupported parameters
* include_rts
* trim_user
* contributor_details
* include_rts
* trim_user
* contributor_details
---
### direct_messages
#### Parameters
* count: Items per page (default: 20)
@ -57,8 +90,9 @@ Unofficial Twitter command. It shows all direct answers (excluding the original
* include_entities: "true" shows entities for pictures and links (Default: false)
#### Unsupported parameters
* skip_status
* skip_status
---
### direct_messages/all
#### Parameters
* count: Items per page (default: 20)
@ -67,6 +101,7 @@ Unofficial Twitter command. It shows all direct answers (excluding the original
* max_id: maximum id
* getText: Defines the format of the status field. Can be "html" or "plain"
---
### direct_messages/conversation
Shows all direct messages of a conversation
#### Parameters
@ -77,14 +112,16 @@ Shows all direct messages of a conversation
* getText: Defines the format of the status field. Can be "html" or "plain"
* uri: URI of the conversation
---
### direct_messages/new
#### Parameters
* user_id: id of the user
* user_id: id of the user
* screen_name: screen name (for technical reasons, this value is not unique!)
* text: The message
* replyto: ID of the replied direct message
* title: Title of the direct message
---
### direct_messages/sent
#### Parameters
* count: Items per page (default: 20)
@ -94,6 +131,7 @@ Shows all direct messages of a conversation
* getText: Defines the format of the status field. Can be "html" or "plain"
* include_entities: "true" shows entities for pictures and links (Default: false)
---
### favorites
#### Parameters
* count: Items per page (default: 20)
@ -108,16 +146,19 @@ Shows all direct messages of a conversation
Favorites aren't displayed to other users, so "user_id" and "screen_name". So setting this value will result in an empty array.
---
### favorites/create
#### Parameters
* id
* include_entities: "true" shows entities for pictures and links (Default: false)
---
### favorites/destroy
#### Parameters
* id
* include_entities: "true" shows entities for pictures and links (Default: false)
---
### followers/ids
#### Parameters
* stringify_ids: Should the id numbers be sent as text (true) or number (false)? (default: false)
@ -125,20 +166,143 @@ Favorites aren't displayed to other users, so "user_id" and "screen_name". So se
#### Unsupported parameters
* user_id
* screen_name
* cursor
* cursor
Friendica doesn't allow showing followers of other users.
---
### friendica/activity/<verb>
#### parameters
* id: item id
Add or remove an activity from an item.
'verb' can be one of:
- like
- dislike
- attendyes
- attendno
- attendmaybe
To remove an activity, prepend the verb with "un", eg. "unlike" or "undislike"
Attend verbs disable eachother: that means that if "attendyes" was added to an item, adding "attendno" remove previous "attendyes".
Attend verbs should be used only with event-related items (there is no check at the moment)
#### Return values
On success:
json
```"ok"```
xml
```<ok>true</ok>```
On error:
HTTP 400 BadRequest
---
### friendica/photo
#### Parameters
* photo_id: Resource id of a photo.
* scale: (optional) scale value of the photo
Returns data of a picture with the given resource.
If 'scale' isn't provided, returned data include full url to each scale of the photo.
If 'scale' is set, returned data include image data base64 encoded.
possibile scale value are:
0: original or max size by server settings
1: image with or height at <= 640
2: image with or height at <= 320
3: thumbnail 160x160
4: Profile image at 175x175
5: Profile image at 80x80
6: Profile image at 48x48
An image used as profile image has only scale 4-6, other images only 0-3
#### Return values
json
```
{
"id": "photo id"
"created": "date(YYYY-MM-GG HH:MM:SS)",
"edited": "date(YYYY-MM-GG HH:MM:SS)",
"title": "photo title",
"desc": "photo description",
"album": "album name",
"filename": "original file name",
"type": "mime type",
"height": "number",
"width": "number",
"profile": "1 if is profile photo",
"link": {
"<scale>": "url to image"
...
},
// if 'scale' is set
"datasize": "size in byte",
"data": "base64 encoded image data"
}
```
xml
```
<photo>
<id>photo id</id>
<created>date(YYYY-MM-GG HH:MM:SS)</created>
<edited>date(YYYY-MM-GG HH:MM:SS)</edited>
<title>photo title</title>
<desc>photo description</desc>
<album>album name</album>
<filename>original file name</filename>
<type>mime type</type>
<height>number</height>
<width>number</width>
<profile>1 if is profile photo</profile>
<links type="array">
<link type="mime type" scale="scale number" href="image url"/>
...
</links>
</photo>
```
---
### friendica/photos/list
Returns a list of all photo resources of the logged in user.
#### Return values
json
```
[
{
id: "resource_id",
album: "album name",
filename: "original file name",
type: "image mime type",
thumb: "url to thumb sized image"
},
...
]
```
xml
```
<photos type="array">
<photo id="resource_id"
album="album name"
filename="original file name"
type="image mime type">
"url to thumb sized image"
</photo>
...
</photos>
```
---
### friends/ids
#### Parameters
* stringify_ids: Should the id numbers be sent as text (true) or number (false)? (default: false)
@ -146,46 +310,54 @@ Returns a list of all photo resources of the logged in user.
#### Unsupported parameters
* user_id
* screen_name
* cursor
* cursor
Friendica doesn't allow showing friends of other users.
---
### help/test
---
### media/upload
#### Parameters
* media: image data
---
### oauth/request_token
#### Parameters
* oauth_callback
* oauth_callback
#### Unsupported parameters
* x_auth_access_type
* x_auth_access_type
---
### oauth/access_token
#### Parameters
* oauth_verifier
* oauth_verifier
#### Unsupported parameters
* x_auth_password
* x_auth_username
* x_auth_mode
* x_auth_password
* x_auth_username
* x_auth_mode
---
### statuses/destroy
#### Parameters
* id: message number
* include_entities: "true" shows entities for pictures and links (Default: false)
#### Unsupported parameters
* trim_user
* trim_user
---
### statuses/followers
* include_entities: "true" shows entities for pictures and links (Default: false)
---
### statuses/friends
* include_entities: "true" shows entities for pictures and links (Default: false)
---
### statuses/friends_timeline
#### Parameters
* count: Items per page (default: 20)
@ -197,10 +369,11 @@ Friendica doesn't allow showing friends of other users.
* include_entities: "true" shows entities for pictures and links (Default: false)
#### Unsupported parameters
* include_rts
* trim_user
* contributor_details
* include_rts
* trim_user
* contributor_details
---
### statuses/home_timeline
#### Parameters
* count: Items per page (default: 20)
@ -212,10 +385,11 @@ Friendica doesn't allow showing friends of other users.
* include_entities: "true" shows entities for pictures and links (Default: false)
#### Unsupported parameters
* include_rts
* trim_user
* contributor_details
* include_rts
* trim_user
* contributor_details
---
### statuses/mentions
#### Parameters
* count: Items per page (default: 20)
@ -225,10 +399,11 @@ Friendica doesn't allow showing friends of other users.
* include_entities: "true" shows entities for pictures and links (Default: false)
#### Unsupported parameters
* include_rts
* trim_user
* contributor_details
* include_rts
* trim_user
* contributor_details
---
### statuses/public_timeline
#### Parameters
* count: Items per page (default: 20)
@ -240,8 +415,9 @@ Friendica doesn't allow showing friends of other users.
* include_entities: "true" shows entities for pictures and links (Default: false)
#### Unsupported parameters
* trim_user
* trim_user
---
### statuses/replies
#### Parameters
* count: Items per page (default: 20)
@ -251,18 +427,20 @@ Friendica doesn't allow showing friends of other users.
* include_entities: "true" shows entities for pictures and links (Default: false)
#### Unsupported parameters
* include_rts
* trim_user
* contributor_details
* include_rts
* trim_user
* contributor_details
---
### statuses/retweet
#### Parameters
* id: message number
* include_entities: "true" shows entities for pictures and links (Default: false)
#### Unsupported parameters
* trim_user
* trim_user
---
### statuses/show
#### Parameters
* id: message number
@ -270,9 +448,10 @@ Friendica doesn't allow showing friends of other users.
* include_entities: "true" shows entities for pictures and links (Default: false)
#### Unsupported parameters
* include_my_retweet
* trim_user
* include_my_retweet
* trim_user
---
### statuses/update, statuses/update_with_media
#### Parameters
* title: Title of the status
@ -289,16 +468,17 @@ Friendica doesn't allow showing friends of other users.
* contact_deny
* network
* include_entities: "true" shows entities for pictures and links (Default: false)
* media_ids: (By now only a single value, no array)
* media_ids: (By now only a single value, no array)
#### Unsupported parameters
* trim_user
* place_id
* display_coordinates
---
### statuses/user_timeline
#### Parameters
* user_id: id of the user
* user_id: id of the user
* screen_name: screen name (for technical reasons, this value is not unique!)
* count: Items per page (default: 20)
* page: page number
@ -309,46 +489,51 @@ Friendica doesn't allow showing friends of other users.
* include_entities: "true" shows entities for pictures and links (Default: false)
#### Unsupported parameters
* include_rts
* trim_user
* contributor_details
* include_rts
* trim_user
* contributor_details
---
### statusnet/config
---
### statusnet/version
#### Unsupported parameters
* user_id
* screen_name
* cursor
* cursor
Friendica doesn't allow showing followers of other users.
---
### users/search
#### Parameters
* q: name of the user
* q: name of the user
#### Unsupported parameters
* page
* count
* include_entities
---
### users/show
#### Parameters
* user_id: id of the user
* user_id: id of the user
* screen_name: screen name (for technical reasons, this value is not unique!)
* include_entities: "true" shows entities for pictures and links (Default: false)
#### Unsupported parameters
* user_id
* screen_name
* cursor
* cursor
Friendica doesn't allow showing friends of other users.
## Implemented API calls (not compatible with other APIs)
---
### friendica/group_show
Return all or a specified group of the user with the containing contacts as array.
@ -362,12 +547,14 @@ Array of:
* user: array of group members (return from api_get_user() function for each member)
---
### friendica/group_delete
delete the specified group of contacts; API call need to include the correct gid AND name of the group to be deleted.
---
### Parameters
* gid: id of the group to be deleted
* name: name of the group to be deleted
* name: name of the group to be deleted
#### Return values
Array of:
@ -378,8 +565,9 @@ Array of:
* wrong users: empty array
---
### friendica/group_create
Create the group with the posted array of contacts as members.
Create the group with the posted array of contacts as members.
#### Parameters
* name: name of the group to be created
@ -395,11 +583,12 @@ Array of:
* gid: gid of the created group
* name: name of the created group
* status: „missing user“ | „reactivated“ | „ok“
* wrong users: array of users, which were not available in the contact table
* wrong users: array of users, which were not available in the contact table
---
### friendica/group_update
Update the group with the posted array of contacts as members (post all members of the group to the call; function will remove members not posted).
Update the group with the posted array of contacts as members (post all members of the group to the call; function will remove members not posted).
#### Parameters
* gid: id of the group to be changed
* name: name of the group to be changed
@ -416,9 +605,9 @@ Array of:
* gid: gid of the changed group
* name: name of the changed group
* status: „missing user“ | „ok“
* wrong users: array of users, which were not available in the contact table
* wrong users: array of users, which were not available in the contact table
---
## Not Implemented API calls
The following API calls are implemented in GNU Social but not in Friendica: (incomplete)
@ -505,6 +694,10 @@ The following API calls from the Twitter API aren't implemented neither in Frien
* trends/closest
* users/report_spam
---
---
## Usage Examples
### BASH / cURL
Betamax has documentated some example API usage from a [bash script](https://en.wikipedia.org/wiki/Bash_(Unix_shell) employing [curl](https://en.wikipedia.org/wiki/CURL) (see [his posting](https://betamax65.de/display/betamax65/43539)).

105
include/HTTPExceptions.php Normal file
View file

@ -0,0 +1,105 @@
<?php
/**
* Throwable exceptions to return HTTP status code
*
* This list of Exception has be extracted from
* here http://racksburg.com/choosing-an-http-status-code/
*/
class HTTPException extends Exception {
var $httpcode = 200;
var $httpdesc = "";
public function __construct($message="", $code = 0, Exception $previous = null) {
if ($this->httpdesc=="") {
$this->httpdesc = preg_replace("|([a-z])([A-Z])|",'$1 $2', str_replace("Exception","",get_class($this)));
}
parent::__construct($message, $code, $previous);
}
}
// 4xx
class TooManyRequestsException extends HTTPException {
var $httpcode = 429;
}
class UnauthorizedException extends HTTPException {
var $httpcode = 401;
}
class ForbiddenException extends HTTPException {
var $httpcode = 403;
}
class NotFoundException extends HTTPException {
var $httpcode = 404;
}
class GoneException extends HTTPException {
var $httpcode = 410;
}
class MethodNotAllowedException extends HTTPException {
var $httpcode = 405;
}
class NonAcceptableException extends HTTPException {
var $httpcode = 406;
}
class LenghtRequiredException extends HTTPException {
var $httpcode = 411;
}
class PreconditionFailedException extends HTTPException {
var $httpcode = 412;
}
class UnsupportedMediaTypeException extends HTTPException {
var $httpcode = 415;
}
class ExpetationFailesException extends HTTPException {
var $httpcode = 417;
}
class ConflictException extends HTTPException {
var $httpcode = 409;
}
class UnprocessableEntityException extends HTTPException {
var $httpcode = 422;
}
class ImATeapotException extends HTTPException {
var $httpcode = 418;
var $httpdesc = "I'm A Teapot";
}
class BadRequestException extends HTTPException {
var $httpcode = 400;
}
// 5xx
class ServiceUnavaiableException extends HTTPException {
var $httpcode = 503;
}
class BadGatewayException extends HTTPException {
var $httpcode = 502;
}
class GatewayTimeoutException extends HTTPException {
var $httpcode = 504;
}
class NotImplementedException extends HTTPException {
var $httpcode = 501;
}
class InternalServerErrorException extends HTTPException {
var $httpcode = 500;
}

View file

@ -1,66 +1,65 @@
<?php
/**
* @file include/api.php
*
* Friendica implementation of statusnet/twitter API
*
* @todo Automatically detect if incoming data is HTML or BBCode
*/
require_once('include/HTTPExceptions.php');
/* Contact details:
Gerhard Seeber Mail: gerhard@seeber.at Friendica: http://mozartweg.dyndns.org/friendica/gerhard
*/
/*
* Change history:
Gerhard Seeber 2015-NOV-25 Add API call /friendica/group_show to return all or a single group
with the containing contacts (necessary for Windows 10 Universal app)
Gerhard Seeber 2015-NOV-27 Add API call /friendica/group_delete to delete the specified group id
(necessary for Windows 10 Universal app)
Gerhard Seeber 2015-DEC-01 Add API call /friendica/group_create to create a group with the specified
name and the given list of contacts (necessary for Windows 10 Universal
app)
Gerhard Seeber 2015-DEC-07 Add API call /friendica/group_update to update a group with the given
list of contacts (necessary for Windows 10 Universal app)
*
*/
require_once("include/bbcode.php");
require_once("include/datetime.php");
require_once("include/conversation.php");
require_once("include/oauth.php");
require_once("include/html2plain.php");
require_once("mod/share.php");
require_once("include/Photo.php");
require_once("mod/item.php");
require_once('include/bbcode.php');
require_once('include/datetime.php');
require_once('include/conversation.php');
require_once('include/oauth.php');
require_once('include/html2plain.php');
require_once('mod/share.php');
require_once('include/Photo.php');
require_once('mod/item.php');
require_once('include/security.php');
require_once('include/contact_selectors.php');
require_once('include/html2bbcode.php');
require_once('mod/wall_upload.php');
require_once("mod/proxy.php");
require_once("include/message.php");
require_once("include/group.php");
require_once('mod/proxy.php');
require_once('include/message.php');
require_once('include/group.php');
require_once('include/like.php');
define('API_METHOD_ANY','*');
define('API_METHOD_GET','GET');
define('API_METHOD_POST','POST,PUT');
define('API_METHOD_DELETE','POST,DELETE');
/*
* Twitter-Like API
*
*/
$API = Array();
$called_api = Null;
/**
* @brief Auth API user
*
* It is not sufficient to use local_user() to check whether someone is allowed to use the API,
* because this will open CSRF holes (just embed an image with src=friendicasite.com/api/statuses/update?status=CSRF
* into a page, and visitors will post something without noticing it).
*/
function api_user() {
// It is not sufficient to use local_user() to check whether someone is allowed to use the API,
// because this will open CSRF holes (just embed an image with src=friendicasite.com/api/statuses/update?status=CSRF
// into a page, and visitors will post something without noticing it).
// Instead, use this function.
if ($_SESSION["allow_api"])
if ($_SESSION['allow_api'])
return local_user();
return false;
}
/**
* @brief Get source name from API client
*
* Clients can send 'source' parameter to be show in post metadata
* as "sent via <source>".
* Some clients doesn't send a source param, we support ones we know
* (only Twidere, atm)
*
* @return string
* Client source name, default to "api" if unset/unknown
*/
function api_source() {
if (requestdata('source'))
return (requestdata('source'));
@ -74,25 +73,63 @@
return ("api");
}
/**
* @brief Format date for API
*
* @param string $str Source date, as UTC
* @return string Date in UTC formatted as "D M d H:i:s +0000 Y"
*/
function api_date($str){
//Wed May 23 06:01:13 +0000 2007
return datetime_convert('UTC', 'UTC', $str, "D M d H:i:s +0000 Y" );
}
function api_register_func($path, $func, $auth=false){
/**
* @brief Register API endpoint
*
* Register a function to be the endpont for defined API path.
*
* @param string $path API URL path, relative to $a->get_baseurl()
* @param string $func Function name to call on path request
* @param bool $auth API need logged user
* @param string $method
* HTTP method reqiured to call this endpoint.
* One of API_METHOD_ANY, API_METHOD_GET, API_METHOD_POST.
* Default to API_METHOD_ANY
*/
function api_register_func($path, $func, $auth=false, $method=API_METHOD_ANY){
global $API;
$API[$path] = array('func'=>$func, 'auth'=>$auth);
$API[$path] = array(
'func'=>$func,
'auth'=>$auth,
'method'=> $method
);
// Workaround for hotot
$path = str_replace("api/", "api/1.1/", $path);
$API[$path] = array('func'=>$func, 'auth'=>$auth);
$API[$path] = array(
'func'=>$func,
'auth'=>$auth,
'method'=> $method
);
}
/**
* Simple HTTP Login
* @brief Login API user
*
* Log in user via OAuth1 or Simple HTTP Auth.
* Simple Auth allow username in form of <pre>user@server</pre>, ignoring server part
*
* @param App $a
* @hook 'authenticate'
* array $addon_auth
* 'username' => username from login form
* 'password' => password from login form
* 'authenticated' => return status,
* 'user_record' => return authenticated user record
* @hook 'logged_in'
* array $user logged user record
*/
function api_login(&$a){
// login with oauth
try{
@ -105,8 +142,7 @@
}
echo __file__.__line__.__function__."<pre>"; var_dump($consumer, $token); die();
}catch(Exception $e){
logger(__file__.__line__.__function__."\n".$e);
//die(__file__.__line__.__function__."<pre>".$e); die();
logger($e);
}
@ -189,104 +225,148 @@
}
/**************************
* MAIN API ENTRY POINT *
**************************/
/**
* @brief Check HTTP method of called API
*
* API endpoints can define which HTTP method to accept when called.
* This function check the current HTTP method agains endpoint
* registered method.
*
* @param string $method Required methods, uppercase, separated by comma
* @return bool
*/
function api_check_method($method) {
if ($method=="*") return True;
return strpos($method, $_SERVER['REQUEST_METHOD']) !== false;
}
/**
* @brief Main API entry point
*
* Authenticate user, call registered API function, set HTTP headers
*
* @param App $a
* @return string API call result
*/
function api_call(&$a){
GLOBAL $API, $called_api;
// preset
$type="json";
foreach ($API as $p=>$info){
if (strpos($a->query_string, $p)===0){
$called_api= explode("/",$p);
//unset($_SERVER['PHP_AUTH_USER']);
if ($info['auth']===true && api_user()===false) {
api_login($a);
if (strpos($a->query_string, ".xml")>0) $type="xml";
if (strpos($a->query_string, ".json")>0) $type="json";
if (strpos($a->query_string, ".rss")>0) $type="rss";
if (strpos($a->query_string, ".atom")>0) $type="atom";
if (strpos($a->query_string, ".as")>0) $type="as";
try {
foreach ($API as $p=>$info){
if (strpos($a->query_string, $p)===0){
if (!api_check_method($info['method'])){
throw new MethodNotAllowedException();
}
$called_api= explode("/",$p);
//unset($_SERVER['PHP_AUTH_USER']);
if ($info['auth']===true && api_user()===false) {
api_login($a);
}
load_contact_links(api_user());
logger('API call for ' . $a->user['username'] . ': ' . $a->query_string);
logger('API parameters: ' . print_r($_REQUEST,true));
$stamp = microtime(true);
$r = call_user_func($info['func'], $a, $type);
$duration = (float)(microtime(true)-$stamp);
logger("API call duration: ".round($duration, 2)."\t".$a->query_string, LOGGER_DEBUG);
if ($r===false) {
// api function returned false withour throw an
// exception. This should not happend, throw a 500
throw new InternalServerErrorException();
}
switch($type){
case "xml":
$r = mb_convert_encoding($r, "UTF-8",mb_detect_encoding($r));
header ("Content-Type: text/xml");
return '<?xml version="1.0" encoding="UTF-8"?>'."\n".$r;
break;
case "json":
header ("Content-Type: application/json");
foreach($r as $rr)
$json = json_encode($rr);
if ($_GET['callback'])
$json = $_GET['callback']."(".$json.")";
return $json;
break;
case "rss":
header ("Content-Type: application/rss+xml");
return '<?xml version="1.0" encoding="UTF-8"?>'."\n".$r;
break;
case "atom":
header ("Content-Type: application/atom+xml");
return '<?xml version="1.0" encoding="UTF-8"?>'."\n".$r;
break;
case "as":
//header ("Content-Type: application/json");
//foreach($r as $rr)
// return json_encode($rr);
return json_encode($r);
break;
}
}
load_contact_links(api_user());
logger('API call for ' . $a->user['username'] . ': ' . $a->query_string);
logger('API parameters: ' . print_r($_REQUEST,true));
$type="json";
if (strpos($a->query_string, ".xml")>0) $type="xml";
if (strpos($a->query_string, ".json")>0) $type="json";
if (strpos($a->query_string, ".rss")>0) $type="rss";
if (strpos($a->query_string, ".atom")>0) $type="atom";
if (strpos($a->query_string, ".as")>0) $type="as";
$stamp = microtime(true);
$r = call_user_func($info['func'], $a, $type);
$duration = (float)(microtime(true)-$stamp);
logger("API call duration: ".round($duration, 2)."\t".$a->query_string, LOGGER_DEBUG);
if ($r===false) return;
switch($type){
case "xml":
$r = mb_convert_encoding($r, "UTF-8",mb_detect_encoding($r));
header ("Content-Type: text/xml");
return '<?xml version="1.0" encoding="UTF-8"?>'."\n".$r;
break;
case "json":
header ("Content-Type: application/json");
foreach($r as $rr)
$json = json_encode($rr);
if ($_GET['callback'])
$json = $_GET['callback']."(".$json.")";
return $json;
break;
case "rss":
header ("Content-Type: application/rss+xml");
return '<?xml version="1.0" encoding="UTF-8"?>'."\n".$r;
break;
case "atom":
header ("Content-Type: application/atom+xml");
return '<?xml version="1.0" encoding="UTF-8"?>'."\n".$r;
break;
case "as":
//header ("Content-Type: application/json");
//foreach($r as $rr)
// return json_encode($rr);
return json_encode($r);
break;
}
//echo "<pre>"; var_dump($r); die();
}
throw new NotImplementedException();
} catch (HTTPException $e) {
header("HTTP/1.1 {$e->httpcode} {$e->httpdesc}");
return api_error($a, $type, $e);
}
header("HTTP/1.1 404 Not Found");
logger('API call not implemented: '.$a->query_string." - ".print_r($_REQUEST,true));
return(api_error($a, $type, "not implemented"));
}
function api_error(&$a, $type, $error) {
/// @TODO https://dev.twitter.com/overview/api/response-codes
$r = "<status><error>".$error."</error><request>".$a->query_string."</request></status>";
/**
* @brief Format API error string
*
* @param Api $a
* @param string $type Return type (xml, json, rss, as)
* @param string $error Error message
*/
function api_error(&$a, $type, $e) {
$error = ($e->getMessage()!==""?$e->getMessage():$e->httpdesc);
# TODO: https://dev.twitter.com/overview/api/response-codes
$xmlstr = "<status><error>{$error}</error><code>{$e->httpcode} {$e->httpdesc}</code><request>{$a->query_string}</request></status>";
switch($type){
case "xml":
header ("Content-Type: text/xml");
return '<?xml version="1.0" encoding="UTF-8"?>'."\n".$r;
return '<?xml version="1.0" encoding="UTF-8"?>'."\n".$xmlstr;
break;
case "json":
header ("Content-Type: application/json");
return json_encode(array('error' => $error, 'request' => $a->query_string));
return json_encode(array(
'error' => $error,
'request' => $a->query_string,
'code' => $e->httpcode." ".$e->httpdesc
));
break;
case "rss":
header ("Content-Type: application/rss+xml");
return '<?xml version="1.0" encoding="UTF-8"?>'."\n".$r;
return '<?xml version="1.0" encoding="UTF-8"?>'."\n".$xmlstr;
break;
case "atom":
header ("Content-Type: application/atom+xml");
return '<?xml version="1.0" encoding="UTF-8"?>'."\n".$r;
return '<?xml version="1.0" encoding="UTF-8"?>'."\n".$xmlstr;
break;
}
}
/**
* RSS extra info
* @brief Set values for RSS template
*
* @param App $a
* @param array $arr Array to be passed to template
* @param array $user_info
* @return array
*/
function api_rss_extra(&$a, $arr, $user_info){
if (is_null($user_info)) $user_info = api_get_user($a);
@ -306,7 +386,11 @@
/**
* Unique contact to contact url.
* @brief Unique contact to contact url.
*
* @param int $id Contact id
* @return bool|string
* Contact url or False if contact id is unknown
*/
function api_unique_id_to_url($id){
$r = q("SELECT `url` FROM `unique_contacts` WHERE `id`=%d LIMIT 1",
@ -318,7 +402,11 @@
}
/**
* Returns user info array.
* @brief Get user info array.
*
* @param Api $a
* @param int|string $contact_id Contact ID or URL
* @param string $type Return type (for errors)
*/
function api_get_user(&$a, $contact_id = Null, $type = "json"){
global $called_api;
@ -342,7 +430,7 @@
$user = dbesc(api_unique_id_to_url($contact_id));
if ($user == "")
die(api_error($a, $type, t("User not found.")));
throw new BadRequestException("User not found.");
$url = $user;
$extra_query = "AND `contact`.`nurl` = '%s' ";
@ -353,7 +441,7 @@
$user = dbesc(api_unique_id_to_url($_GET['user_id']));
if ($user == "")
die(api_error($a, $type, t("User not found.")));
throw new BadRequestException("User not found.");
$url = $user;
$extra_query = "AND `contact`.`nurl` = '%s' ";
@ -390,7 +478,8 @@
if (!$user) {
if (api_user()===false) {
api_login($a); return False;
api_login($a);
return False;
} else {
$user = $_SESSION['uid'];
$extra_query = "AND `contact`.`uid` = %d AND `contact`.`self` = 1 ";
@ -461,9 +550,9 @@
);
return $ret;
} else
die(api_error($a, $type, t("User not found.")));
} else {
throw new BadRequestException("User not found.");
}
}
if($uinfo[0]['self']) {
@ -672,7 +761,7 @@
* http://developer.twitter.com/doc/get/account/verify_credentials
*/
function api_account_verify_credentials(&$a, $type){
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
unset($_REQUEST["user_id"]);
unset($_GET["user_id"]);
@ -723,7 +812,7 @@
function api_statuses_mediap(&$a, $type) {
if (api_user()===false) {
logger('api_statuses_update: no user');
return false;
throw new ForbiddenException();
}
$user_info = api_get_user($a);
@ -757,14 +846,14 @@
// this should output the last post (the one we just posted).
return api_status_show($a,$type);
}
api_register_func('api/statuses/mediap','api_statuses_mediap', true);
api_register_func('api/statuses/mediap','api_statuses_mediap', true, API_METHOD_POST);
/*Waitman Gobble Mod*/
function api_statuses_update(&$a, $type) {
if (api_user()===false) {
logger('api_statuses_update: no user');
return false;
throw new ForbiddenException();
}
$user_info = api_get_user($a);
@ -882,7 +971,7 @@
$_REQUEST['body'] .= "\n\n".$media;
}
/// @TODO Multiple IDs
// To-Do: Multiple IDs
if (requestdata('media_ids')) {
$r = q("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",
intval(requestdata('media_ids')), api_user());
@ -908,27 +997,27 @@
// this should output the last post (the one we just posted).
return api_status_show($a,$type);
}
api_register_func('api/statuses/update','api_statuses_update', true);
api_register_func('api/statuses/update_with_media','api_statuses_update', true);
api_register_func('api/statuses/update','api_statuses_update', true, API_METHOD_POST);
api_register_func('api/statuses/update_with_media','api_statuses_update', true, API_METHOD_POST);
function api_media_upload(&$a, $type) {
if (api_user()===false) {
logger('no user');
return false;
throw new ForbiddenException();
}
$user_info = api_get_user($a);
if(!x($_FILES,'media')) {
// Output error
return false;
throw new BadRequestException("No media.");
}
$media = wall_upload_post($a, false);
if(!$media) {
// Output error
return false;
throw new InternalServerErrorException();
}
$returndata = array();
@ -943,8 +1032,7 @@
return array("media" => $returndata);
}
api_register_func('api/media/upload','api_media_upload', true);
api_register_func('api/media/upload','api_media_upload', true, API_METHOD_POST);
function api_status_show(&$a, $type){
$user_info = api_get_user($a);
@ -1180,11 +1268,12 @@
$userlist[] = $userdata["user"];
}
$userlist = array("users" => $userlist);
} else
die(api_error($a, $type, t("User not found.")));
} else
die(api_error($a, $type, t("User not found.")));
} else {
throw new BadRequestException("User not found.");
}
} else {
throw new BadRequestException("User not found.");
}
return ($userlist);
}
@ -1194,11 +1283,11 @@
*
* http://developer.twitter.com/doc/get/statuses/home_timeline
*
* @TODO Optional parameters
* @TODO Add reply info
* TODO: Optional parameters
* TODO: Add reply info
*/
function api_statuses_home_timeline(&$a, $type){
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
unset($_REQUEST["user_id"]);
unset($_GET["user_id"]);
@ -1281,7 +1370,7 @@
api_register_func('api/statuses/friends_timeline','api_statuses_home_timeline', true);
function api_statuses_public_timeline(&$a, $type){
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
$user_info = api_get_user($a);
// get last newtork messages
@ -1351,7 +1440,7 @@
*
*/
function api_statuses_show(&$a, $type){
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
$user_info = api_get_user($a);
@ -1389,8 +1478,9 @@
intval($id)
);
if (!$r)
die(api_error($a, $type, t("There is no status with this id.")));
if (!$r) {
throw new BadRequestException("There is no status with this id.");
}
$ret = api_format_items($r,$user_info);
@ -1414,7 +1504,7 @@
*
*/
function api_conversation_show(&$a, $type){
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
$user_info = api_get_user($a);
@ -1464,7 +1554,7 @@
);
if (!$r)
die(api_error($a, $type, t("There is no conversation with this id.")));
throw new BadRequestException("There is no conversation with this id.");
$ret = api_format_items($r,$user_info);
@ -1480,7 +1570,7 @@
function api_statuses_repeat(&$a, $type){
global $called_api;
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
$user_info = api_get_user($a);
@ -1538,13 +1628,13 @@
$called_api = null;
return(api_status_show($a,$type));
}
api_register_func('api/statuses/retweet','api_statuses_repeat', true);
api_register_func('api/statuses/retweet','api_statuses_repeat', true, API_METHOD_POST);
/**
*
*/
function api_statuses_destroy(&$a, $type){
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
$user_info = api_get_user($a);
@ -1566,7 +1656,7 @@
return($ret);
}
api_register_func('api/statuses/destroy','api_statuses_destroy', true);
api_register_func('api/statuses/destroy','api_statuses_destroy', true, API_METHOD_DELETE);
/**
*
@ -1574,7 +1664,7 @@
*
*/
function api_statuses_mentions(&$a, $type){
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
unset($_REQUEST["user_id"]);
unset($_GET["user_id"]);
@ -1653,7 +1743,7 @@
function api_statuses_user_timeline(&$a, $type){
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
$user_info = api_get_user($a);
// get last network messages
@ -1714,7 +1804,6 @@
return api_apply_template("timeline", $type, $data);
}
api_register_func('api/statuses/user_timeline','api_statuses_user_timeline', true);
@ -1725,7 +1814,7 @@
* api v1 : https://web.archive.org/web/20131019055350/https://dev.twitter.com/docs/api/1/post/favorites/create/%3Aid
*/
function api_favorites_create_destroy(&$a, $type){
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
// for versioned api.
/// @TODO We need a better global soluton
@ -1743,7 +1832,8 @@
$item = q("SELECT * FROM item WHERE id=%d AND uid=%d",
$itemid, api_user());
if ($item===false || count($item)==0) die(api_error($a, $type, t("Invalid item.")));
if ($item===false || count($item)==0)
throw new BadRequestException("Invalid item.");
switch($action){
case "create":
@ -1753,7 +1843,7 @@
$item[0]['starred']=0;
break;
default:
die(api_error($a, $type, t("Invalid action. ".$action)));
throw new BadRequestException("Invalid action ".$action);
}
$r = q("UPDATE item SET starred=%d WHERE id=%d AND uid=%d",
$item[0]['starred'], $itemid, api_user());
@ -1761,7 +1851,8 @@
q("UPDATE thread SET starred=%d WHERE iid=%d AND uid=%d",
$item[0]['starred'], $itemid, api_user());
if ($r===false) die(api_error($a, $type, t("DB error")));
if ($r===false)
throw InternalServerErrorException("DB error");
$user_info = api_get_user($a);
@ -1777,14 +1868,13 @@
return api_apply_template("status", $type, $data);
}
api_register_func('api/favorites/create', 'api_favorites_create_destroy', true);
api_register_func('api/favorites/destroy', 'api_favorites_create_destroy', true);
api_register_func('api/favorites/create', 'api_favorites_create_destroy', true, API_METHOD_POST);
api_register_func('api/favorites/destroy', 'api_favorites_create_destroy', true, API_METHOD_DELETE);
function api_favorites(&$a, $type){
global $called_api;
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
$called_api= array();
@ -1842,14 +1932,12 @@
return api_apply_template("timeline", $type, $data);
}
api_register_func('api/favorites','api_favorites', true);
function api_format_as($a, $ret, $user_info) {
$as = array();
$as['title'] = $a->config['sitename']." Public Timeline";
$items = array();
@ -2015,8 +2103,10 @@
}
function api_get_entitities(&$text, $bbcode) {
/// @todo
/// Links at the first character of the post
/*
To-Do:
* Links at the first character of the post
*/
$a = get_app();
@ -2166,7 +2256,7 @@
return($entities);
}
function api_format_items_embeded_images($item, $text){
function api_format_items_embeded_images(&$item, $text){
$a = get_app();
$text = preg_replace_callback(
"|data:image/([^;]+)[^=]+=*|m",
@ -2177,7 +2267,42 @@
return $text;
}
function api_format_items($r,$user_info, $filter_user = false) {
/**
* @brief return likes, dislikes and attend status for item
*
* @param array $item
* @return array
* likes => int count
* dislikes => int count
*/
function api_format_items_likes(&$item) {
$activities = array(
ACTIVITY_LIKE => 'like',
ACTIVITY_DISLIKE => 'dislike',
ACTIVITY_ATTEND => 'attendyes',
ACTIVITY_ATTENDNO => 'attendno',
ACTIVITY_ATTENDMAYBE => 'attendmaybe'
);
$r = q("SELECT verb, count(verb) as n FROM item WHERE parent=%d GROUP BY verb",
intval($item['id']));
$res = array();
foreach($r as $row) {
if (x($activities, $row['verb'])) {
$res[$activities[$row['verb']]] = $row['n'];
}
}
return $res;
}
/**
* @brief format items to be returned by api
*
* @param array $r array of items
* @param array $user_info
* @param bool $filter_user filter items by $user_info
*/
function api_format_items(&$r,$user_info, $filter_user = false) {
$a = get_app();
$ret = Array();
@ -2250,6 +2375,7 @@
//'entities' => NULL,
'statusnet_html' => $converted["html"],
'statusnet_conversation_id' => $item['parent'],
'friendica_activities' => api_format_items_likes($item),
);
if (count($converted["attachments"]) > 0)
@ -2297,7 +2423,6 @@
function api_account_rate_limit_status(&$a,$type) {
$hash = array(
'reset_time_in_seconds' => strtotime('now + 1 hour'),
'remaining_hits' => (string) 150,
@ -2308,31 +2433,26 @@
$hash['resettime_in_seconds'] = $hash['reset_time_in_seconds'];
return api_apply_template('ratelimit', $type, array('$hash' => $hash));
}
api_register_func('api/account/rate_limit_status','api_account_rate_limit_status',true);
function api_help_test(&$a,$type) {
if ($type == 'xml')
$ok = "true";
else
$ok = "ok";
return api_apply_template('test', $type, array("$ok" => $ok));
}
api_register_func('api/help/test','api_help_test',false);
function api_lists(&$a,$type) {
$ret = array();
return array($ret);
}
api_register_func('api/lists','api_lists',true);
function api_lists_list(&$a,$type) {
$ret = array();
return array($ret);
}
@ -2344,7 +2464,7 @@
* returns: json, xml
**/
function api_statuses_f(&$a, $type, $qtype) {
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
$user_info = api_get_user($a);
if (x($_GET,'cursor') && $_GET['cursor']=='undefined'){
@ -2437,26 +2557,27 @@
api_register_func('api/statusnet/config','api_statusnet_config',false);
function api_statusnet_version(&$a,$type) {
// liar
$fake_statusnet_version = "0.9.7";
if($type === 'xml') {
header("Content-type: application/xml");
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n" . '<version>0.9.7</version>' . "\r\n";
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n" . '<version>'.$fake_statusnet_version.'</version>' . "\r\n";
killme();
}
elseif($type === 'json') {
header("Content-type: application/json");
echo '"0.9.7"';
echo '"'.$fake_statusnet_version.'"';
killme();
}
}
api_register_func('api/statusnet/version','api_statusnet_version',false);
/**
* @todo use api_apply_template() to return data
*/
function api_ff_ids(&$a,$type,$qtype) {
if(! api_user())
return false;
if(! api_user()) throw new ForbiddenException();
$user_info = api_get_user($a);
@ -2510,7 +2631,7 @@
function api_direct_messages_new(&$a, $type) {
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
if (!x($_POST, "text") OR (!x($_POST,"screen_name") AND !x($_POST,"user_id"))) return;
@ -2567,11 +2688,10 @@
return api_apply_template("direct_messages", $type, $data);
}
api_register_func('api/direct_messages/new','api_direct_messages_new',true);
api_register_func('api/direct_messages/new','api_direct_messages_new',true, API_METHOD_POST);
function api_direct_messages_box(&$a, $type, $box) {
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
// params
$count = (x($_GET,'count')?$_GET['count']:20);
@ -2701,36 +2821,75 @@
function api_fr_photos_list(&$a,$type) {
if (api_user()===false) return false;
$r = q("select distinct `resource-id` from photo where uid = %d and album != 'Contact Photos' ",
if (api_user()===false) throw new ForbiddenException();
$r = q("select `resource-id`, max(scale) as scale, album, filename, type from photo
where uid = %d and album != 'Contact Photos' group by `resource-id`",
intval(local_user())
);
$typetoext = array(
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif'
);
$data = array('photos'=>array());
if($r) {
$ret = array();
foreach($r as $rr)
$ret[] = $rr['resource-id'];
header("Content-type: application/json");
echo json_encode($ret);
foreach($r as $rr) {
$photo = array();
$photo['id'] = $rr['resource-id'];
$photo['album'] = $rr['album'];
$photo['filename'] = $rr['filename'];
$photo['type'] = $rr['type'];
$photo['thumb'] = $a->get_baseurl()."/photo/".$rr['resource-id']."-".$rr['scale'].".".$typetoext[$rr['type']];
$data['photos'][] = $photo;
}
}
killme();
return api_apply_template("photos_list", $type, $data);
}
function api_fr_photo_detail(&$a,$type) {
if (api_user()===false) return false;
if(! $_REQUEST['photo_id']) return false;
$scale = ((array_key_exists('scale',$_REQUEST)) ? intval($_REQUEST['scale']) : 0);
$r = q("select * from photo where uid = %d and `resource-id` = '%s' and scale = %d limit 1",
if (api_user()===false) throw new ForbiddenException();
if(!x($_REQUEST,'photo_id')) throw new BadRequestException("No photo id.");
$scale = (x($_REQUEST, 'scale') ? intval($_REQUEST['scale']) : false);
$scale_sql = ($scale === false ? "" : sprintf("and scale=%d",intval($scale)));
$data_sql = ($scale === false ? "" : "data, ");
$r = q("select %s `resource-id`, `created`, `edited`, `title`, `desc`, `album`, `filename`,
`type`, `height`, `width`, `datasize`, `profile`, min(`scale`) as minscale, max(`scale`) as maxscale
from photo where `uid` = %d and `resource-id` = '%s' %s group by `resource-id`",
$data_sql,
intval(local_user()),
dbesc($_REQUEST['photo_id']),
intval($scale)
$scale_sql
);
if($r) {
header("Content-type: application/json");
$r[0]['data'] = base64_encode($r[0]['data']);
echo json_encode($r[0]);
$typetoext = array(
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif'
);
if ($r) {
$data = array('photo' => $r[0]);
if ($scale !== false) {
$data['photo']['data'] = base64_encode($data['photo']['data']);
} else {
unset($data['photo']['datasize']); //needed only with scale param
}
$data['photo']['link'] = array();
for($k=intval($data['photo']['minscale']); $k<=intval($data['photo']['maxscale']); $k++) {
$data['photo']['link'][$k] = $a->get_baseurl()."/photo/".$data['photo']['resource-id']."-".$k.".".$typetoext[$data['photo']['type']];
}
$data['photo']['id'] = $data['photo']['resource-id'];
unset($data['photo']['resource-id']);
unset($data['photo']['minscale']);
unset($data['photo']['maxscale']);
} else {
throw new NotFoundException();
}
killme();
return api_apply_template("photo_detail", $type, $data);
}
api_register_func('api/friendica/photos/list', 'api_fr_photos_list', true);
@ -2754,7 +2913,7 @@
$c_url = ((x($_GET,'c_url')) ? $_GET['c_url'] : '');
if ($url === '' || $c_url === '')
die((api_error($a, 'json', "Wrong parameters")));
throw new BadRequestException("Wrong parameters.");
$c_url = normalise_link($c_url);
@ -2766,7 +2925,7 @@
);
if ((! count($r)) || ($r[0]['network'] !== NETWORK_DFRN))
die((api_error($a, 'json', "Unknown contact")));
throw new BadRequestException("Unknown contact");
$cid = $r[0]['id'];
@ -2801,259 +2960,260 @@
api_register_func('api/friendica/remoteauth', 'api_friendica_remoteauth', true);
function api_share_as_retweet(&$item) {
$body = trim($item["body"]);
function api_share_as_retweet(&$item) {
$body = trim($item["body"]);
// Skip if it isn't a pure repeated messages
// Does it start with a share?
if (strpos($body, "[share") > 0)
return(false);
// Skip if it isn't a pure repeated messages
// Does it start with a share?
if (strpos($body, "[share") > 0)
return(false);
// Does it end with a share?
if (strlen($body) > (strrpos($body, "[/share]") + 8))
return(false);
// Does it end with a share?
if (strlen($body) > (strrpos($body, "[/share]") + 8))
return(false);
$attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$1",$body);
// Skip if there is no shared message in there
if ($body == $attributes)
return(false);
$attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$1",$body);
// Skip if there is no shared message in there
if ($body == $attributes)
return(false);
$author = "";
preg_match("/author='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "")
$author = html_entity_decode($matches[1],ENT_QUOTES,'UTF-8');
$author = "";
preg_match("/author='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "")
$author = html_entity_decode($matches[1],ENT_QUOTES,'UTF-8');
preg_match('/author="(.*?)"/ism', $attributes, $matches);
if ($matches[1] != "")
$author = $matches[1];
preg_match('/author="(.*?)"/ism', $attributes, $matches);
if ($matches[1] != "")
$author = $matches[1];
$profile = "";
preg_match("/profile='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "")
$profile = $matches[1];
$profile = "";
preg_match("/profile='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "")
$profile = $matches[1];
preg_match('/profile="(.*?)"/ism', $attributes, $matches);
if ($matches[1] != "")
$profile = $matches[1];
preg_match('/profile="(.*?)"/ism', $attributes, $matches);
if ($matches[1] != "")
$profile = $matches[1];
$avatar = "";
preg_match("/avatar='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "")
$avatar = $matches[1];
$avatar = "";
preg_match("/avatar='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "")
$avatar = $matches[1];
preg_match('/avatar="(.*?)"/ism', $attributes, $matches);
if ($matches[1] != "")
$avatar = $matches[1];
preg_match('/avatar="(.*?)"/ism', $attributes, $matches);
if ($matches[1] != "")
$avatar = $matches[1];
$link = "";
preg_match("/link='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "")
$link = $matches[1];
$link = "";
preg_match("/link='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "")
$link = $matches[1];
preg_match('/link="(.*?)"/ism', $attributes, $matches);
if ($matches[1] != "")
$link = $matches[1];
preg_match('/link="(.*?)"/ism', $attributes, $matches);
if ($matches[1] != "")
$link = $matches[1];
$shared_body = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$2",$body);
$shared_body = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$2",$body);
if (($shared_body == "") OR ($profile == "") OR ($author == "") OR ($avatar == ""))
return(false);
if (($shared_body == "") OR ($profile == "") OR ($author == "") OR ($avatar == ""))
return(false);
$item["body"] = $shared_body;
$item["author-name"] = $author;
$item["author-link"] = $profile;
$item["author-avatar"] = $avatar;
$item["plink"] = $link;
$item["body"] = $shared_body;
$item["author-name"] = $author;
$item["author-link"] = $profile;
$item["author-avatar"] = $avatar;
$item["plink"] = $link;
return(true);
return(true);
}
}
function api_get_nick($profile) {
/* To-Do:
- remove trailing junk from profile url
- pump.io check has to check the website
*/
function api_get_nick($profile) {
/// @TODO Remove trailing junk from profile url
/// @TODO pump.io check has to check the website
$nick = "";
$nick = "";
$r = q("SELECT `nick` FROM `gcontact` WHERE `nurl` = '%s'",
dbesc(normalise_link($profile)));
if ($r)
$nick = $r[0]["nick"];
if (!$nick == "") {
$r = q("SELECT `nick` FROM `contact` WHERE `uid` = 0 AND `nurl` = '%s'",
$r = q("SELECT `nick` FROM `gcontact` WHERE `nurl` = '%s'",
dbesc(normalise_link($profile)));
if ($r)
$nick = $r[0]["nick"];
}
if (!$nick == "") {
$friendica = preg_replace("=https?://(.*)/profile/(.*)=ism", "$2", $profile);
if ($friendica != $profile)
$nick = $friendica;
}
if (!$nick == "") {
$r = q("SELECT `nick` FROM `contact` WHERE `uid` = 0 AND `nurl` = '%s'",
dbesc(normalise_link($profile)));
if ($r)
$nick = $r[0]["nick"];
}
if (!$nick == "") {
$diaspora = preg_replace("=https?://(.*)/u/(.*)=ism", "$2", $profile);
if ($diaspora != $profile)
$nick = $diaspora;
}
if (!$nick == "") {
$friendica = preg_replace("=https?://(.*)/profile/(.*)=ism", "$2", $profile);
if ($friendica != $profile)
$nick = $friendica;
}
if (!$nick == "") {
$twitter = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $profile);
if ($twitter != $profile)
$nick = $twitter;
}
if (!$nick == "") {
$diaspora = preg_replace("=https?://(.*)/u/(.*)=ism", "$2", $profile);
if ($diaspora != $profile)
$nick = $diaspora;
}
if (!$nick == "") {
$twitter = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $profile);
if ($twitter != $profile)
$nick = $twitter;
}
if (!$nick == "") {
$StatusnetHost = preg_replace("=https?://(.*)/user/(.*)=ism", "$1", $profile);
if ($StatusnetHost != $profile) {
$StatusnetUser = preg_replace("=https?://(.*)/user/(.*)=ism", "$2", $profile);
if ($StatusnetUser != $profile) {
$UserData = fetch_url("http://".$StatusnetHost."/api/users/show.json?user_id=".$StatusnetUser);
$user = json_decode($UserData);
if ($user)
$nick = $user->screen_name;
if (!$nick == "") {
$StatusnetHost = preg_replace("=https?://(.*)/user/(.*)=ism", "$1", $profile);
if ($StatusnetHost != $profile) {
$StatusnetUser = preg_replace("=https?://(.*)/user/(.*)=ism", "$2", $profile);
if ($StatusnetUser != $profile) {
$UserData = fetch_url("http://".$StatusnetHost."/api/users/show.json?user_id=".$StatusnetUser);
$user = json_decode($UserData);
if ($user)
$nick = $user->screen_name;
}
}
}
}
/// @TODO Look at the page if its really a pumpio site
//if (!$nick == "") {
// $pumpio = preg_replace("=https?://(.*)/(.*)/=ism", "$2", $profile."/");
// if ($pumpio != $profile)
// $nick = $pumpio;
// <div class="media" id="profile-block" data-profile-id="acct:kabniel@microca.st">
// To-Do: look at the page if its really a pumpio site
//if (!$nick == "") {
// $pumpio = preg_replace("=https?://(.*)/(.*)/=ism", "$2", $profile."/");
// if ($pumpio != $profile)
// $nick = $pumpio;
// <div class="media" id="profile-block" data-profile-id="acct:kabniel@microca.st">
//}
//}
if ($nick != "") {
q("UPDATE `unique_contacts` SET `nick` = '%s' WHERE `nick` != '%s' AND url = '%s'",
dbesc($nick), dbesc($nick), dbesc(normalise_link($profile)));
return($nick);
}
return(false);
}
function api_clean_plain_items($Text) {
$include_entities = strtolower(x($_REQUEST,'include_entities')?$_REQUEST['include_entities']:"false");
$Text = bb_CleanPictureLinks($Text);
$URLSearchString = "^\[\]";
$Text = preg_replace("/([!#@])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",'$1$3',$Text);
if ($include_entities == "true") {
$Text = preg_replace("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",'[url=$1]$1[/url]',$Text);
}
$Text = preg_replace_callback("((.*?)\[class=(.*?)\](.*?)\[\/class\])ism","api_cleanup_share",$Text);
return($Text);
}
function api_cleanup_share($shared) {
if ($shared[2] != "type-link")
return($shared[0]);
if (!preg_match_all("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism",$shared[3], $bookmark))
return($shared[0]);
$title = "";
$link = "";
if (isset($bookmark[2][0]))
$title = $bookmark[2][0];
if (isset($bookmark[1][0]))
$link = $bookmark[1][0];
if (strpos($shared[1],$title) !== false)
$title = "";
if (strpos($shared[1],$link) !== false)
$link = "";
$text = trim($shared[1]);
//if (strlen($text) < strlen($title))
if (($text == "") AND ($title != ""))
$text .= "\n\n".trim($title);
if ($link != "")
$text .= "\n".trim($link);
return(trim($text));
}
function api_best_nickname(&$contacts) {
$best_contact = array();
if (count($contact) == 0)
return;
foreach ($contacts AS $contact)
if ($contact["network"] == "") {
$contact["network"] = "dfrn";
$best_contact = array($contact);
if ($nick != "") {
q("UPDATE `unique_contacts` SET `nick` = '%s' WHERE `nick` != '%s' AND url = '%s'",
dbesc($nick), dbesc($nick), dbesc(normalise_link($profile)));
return($nick);
}
if (sizeof($best_contact) == 0)
foreach ($contacts AS $contact)
if ($contact["network"] == "dfrn")
$best_contact = array($contact);
return(false);
}
if (sizeof($best_contact) == 0)
foreach ($contacts AS $contact)
if ($contact["network"] == "dspr")
$best_contact = array($contact);
function api_clean_plain_items($Text) {
$include_entities = strtolower(x($_REQUEST,'include_entities')?$_REQUEST['include_entities']:"false");
if (sizeof($best_contact) == 0)
foreach ($contacts AS $contact)
if ($contact["network"] == "stat")
$best_contact = array($contact);
$Text = bb_CleanPictureLinks($Text);
if (sizeof($best_contact) == 0)
foreach ($contacts AS $contact)
if ($contact["network"] == "pump")
$best_contact = array($contact);
$URLSearchString = "^\[\]";
if (sizeof($best_contact) == 0)
foreach ($contacts AS $contact)
if ($contact["network"] == "twit")
$best_contact = array($contact);
$Text = preg_replace("/([!#@])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",'$1$3',$Text);
if (sizeof($best_contact) == 1)
$contacts = $best_contact;
else
$contacts = array($contacts[0]);
}
if ($include_entities == "true") {
$Text = preg_replace("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",'[url=$1]$1[/url]',$Text);
}
$Text = preg_replace_callback("((.*?)\[class=(.*?)\](.*?)\[\/class\])ism","api_cleanup_share",$Text);
return($Text);
}
function api_cleanup_share($shared) {
if ($shared[2] != "type-link")
return($shared[0]);
if (!preg_match_all("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism",$shared[3], $bookmark))
return($shared[0]);
$title = "";
$link = "";
if (isset($bookmark[2][0]))
$title = $bookmark[2][0];
if (isset($bookmark[1][0]))
$link = $bookmark[1][0];
if (strpos($shared[1],$title) !== false)
$title = "";
if (strpos($shared[1],$link) !== false)
$link = "";
$text = trim($shared[1]);
//if (strlen($text) < strlen($title))
if (($text == "") AND ($title != ""))
$text .= "\n\n".trim($title);
if ($link != "")
$text .= "\n".trim($link);
return(trim($text));
}
function api_best_nickname(&$contacts) {
$best_contact = array();
if (count($contact) == 0)
return;
foreach ($contacts AS $contact)
if ($contact["network"] == "") {
$contact["network"] = "dfrn";
$best_contact = array($contact);
}
if (sizeof($best_contact) == 0)
foreach ($contacts AS $contact)
if ($contact["network"] == "dfrn")
$best_contact = array($contact);
if (sizeof($best_contact) == 0)
foreach ($contacts AS $contact)
if ($contact["network"] == "dspr")
$best_contact = array($contact);
if (sizeof($best_contact) == 0)
foreach ($contacts AS $contact)
if ($contact["network"] == "stat")
$best_contact = array($contact);
if (sizeof($best_contact) == 0)
foreach ($contacts AS $contact)
if ($contact["network"] == "pump")
$best_contact = array($contact);
if (sizeof($best_contact) == 0)
foreach ($contacts AS $contact)
if ($contact["network"] == "twit")
$best_contact = array($contact);
if (sizeof($best_contact) == 1)
$contacts = $best_contact;
else
$contacts = array($contacts[0]);
}
// return all or a specified group of the user with the containing contacts
function api_friendica_group_show(&$a, $type) {
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
// params
$user_info = api_get_user($a);
$gid = (x($_REQUEST,'gid') ? $_REQUEST['gid'] : 0);
$uid = $user_info['uid'];
// get data of the specified group id or all groups if not specified
if ($gid != 0) {
$r = q("SELECT * FROM `group` WHERE `deleted` = 0 AND `uid` = %d AND `id` = %d",
intval($uid),
intval($uid),
intval($gid));
// error message if specified gid is not in database
if (count($r) == 0)
die(api_error($a, $type, 'gid not available'));
if (count($r) == 0)
throw new BadRequestException("gid not available");
}
else
else
$r = q("SELECT * FROM `group` WHERE `deleted` = 0 AND `uid` = %d",
intval($uid));
// loop through all groups and retrieve all members for adding data in the user array
foreach ($r as $rr) {
$members = group_get_members($rr['id']);
@ -3071,34 +3231,34 @@ function api_best_nickname(&$contacts) {
// delete the specified group of the user
function api_friendica_group_delete(&$a, $type) {
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
// params
$user_info = api_get_user($a);
$gid = (x($_REQUEST,'gid') ? $_REQUEST['gid'] : 0);
$name = (x($_REQUEST, 'name') ? $_REQUEST['name'] : "");
$uid = $user_info['uid'];
// error if no gid specified
if ($gid == 0 || $name == "")
die(api_error($a, $type, 'gid or name not specified'));
throw new BadRequestException('gid or name not specified');
// get data of the specified group id
$r = q("SELECT * FROM `group` WHERE `uid` = %d AND `id` = %d",
intval($uid),
intval($uid),
intval($gid));
// error message if specified gid is not in database
if (count($r) == 0)
die(api_error($a, $type, 'gid not available'));
if (count($r) == 0)
throw new BadRequestException('gid not available');
// get data of the specified group id and group name
$rname = q("SELECT * FROM `group` WHERE `uid` = %d AND `id` = %d AND `name` = '%s'",
intval($uid),
intval($uid),
intval($gid),
dbesc($name));
// error message if specified gid is not in database
if (count($rname) == 0)
die(api_error($a, $type, 'wrong group name'));
if (count($rname) == 0)
throw new BadRequestException('wrong group name');
// delete group
$ret = group_rmv($uid, $name);
@ -3108,14 +3268,14 @@ function api_best_nickname(&$contacts) {
return api_apply_template("group_delete", $type, array('$result' => $success));
}
else
die(api_error($a, $type, 'other API error'));
throw new BadRequestException('other API error');
}
api_register_func('api/friendica/group_delete', 'api_friendica_group_delete', true);
api_register_func('api/friendica/group_delete', 'api_friendica_group_delete', true, API_METHOD_DELETE);
// create the specified group with the posted array of contacts
// create the specified group with the posted array of contacts
function api_friendica_group_create(&$a, $type) {
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
// params
$user_info = api_get_user($a);
@ -3126,38 +3286,38 @@ function api_best_nickname(&$contacts) {
// error if no name specified
if ($name == "")
die(api_error($a, $type, 'group name not specified'));
throw new BadRequestException('group name not specified');
// get data of the specified group name
$rname = q("SELECT * FROM `group` WHERE `uid` = %d AND `name` = '%s' AND `deleted` = 0",
intval($uid),
intval($uid),
dbesc($name));
// error message if specified group name already exists
if (count($rname) != 0)
die(api_error($a, $type, 'group name already exists'));
if (count($rname) != 0)
throw new BadRequestException('group name already exists');
// check if specified group name is a deleted group
$rname = q("SELECT * FROM `group` WHERE `uid` = %d AND `name` = '%s' AND `deleted` = 1",
intval($uid),
intval($uid),
dbesc($name));
// error message if specified group name already exists
if (count($rname) != 0)
if (count($rname) != 0)
$reactivate_group = true;
// create group
$ret = group_add($uid, $name);
if ($ret)
if ($ret)
$gid = group_byname($uid, $name);
else
die(api_error($a, $type, 'other API error'));
throw new BadRequestException('other API error');
// add members
$erroraddinguser = false;
$errorusers = array();
foreach ($users as $user) {
$cid = $user['cid'];
// check if user really exists as contact
$contact = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d",
$contact = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d",
intval($cid),
intval($uid));
if (count($contact))
@ -3171,14 +3331,14 @@ function api_best_nickname(&$contacts) {
// return success message incl. missing users in array
$status = ($erroraddinguser ? "missing user" : ($reactivate_group ? "reactivated" : "ok"));
$success = array('success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers);
return api_apply_template("group_create", $type, array('result' => $success));
return api_apply_template("group_create", $type, array('result' => $success));
}
api_register_func('api/friendica/group_create', 'api_friendica_group_create', true);
api_register_func('api/friendica/group_create', 'api_friendica_group_create', true, API_METHOD_POST);
// update the specified group with the posted array of contacts
// update the specified group with the posted array of contacts
function api_friendica_group_update(&$a, $type) {
if (api_user()===false) return false;
if (api_user()===false) throw new ForbiddenException();
// params
$user_info = api_get_user($a);
@ -3190,11 +3350,11 @@ function api_best_nickname(&$contacts) {
// error if no name specified
if ($name == "")
die(api_error($a, $type, 'group name not specified'));
throw new BadRequestException('group name not specified');
// error if no gid specified
if ($gid == "")
die(api_error($a, $type, 'gid not specified'));
throw new BadRequestException('gid not specified');
// remove members
$members = group_get_members($gid);
@ -3214,7 +3374,7 @@ function api_best_nickname(&$contacts) {
foreach ($users as $user) {
$cid = $user['cid'];
// check if user really exists as contact
$contact = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d",
$contact = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d",
intval($cid),
intval($uid));
if (count($contact))
@ -3224,13 +3384,44 @@ function api_best_nickname(&$contacts) {
$errorusers[] = $cid;
}
}
// return success message incl. missing users in array
$status = ($erroraddinguser ? "missing user" : "ok");
$success = array('success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers);
return api_apply_template("group_update", $type, array('result' => $success));
return api_apply_template("group_update", $type, array('result' => $success));
}
api_register_func('api/friendica/group_update', 'api_friendica_group_update', true);
api_register_func('api/friendica/group_update', 'api_friendica_group_update', true, API_METHOD_POST);
function api_friendica_activity(&$a, $type) {
if (api_user()===false) throw new ForbiddenException();
#$verb = (x($_REQUEST, 'verb') ? strtolower($_REQUEST['verb']) : '');
$verb = strtolower($a->argv[3]);
$id = (x($_REQUEST, 'id') ? $_REQUEST['id'] : 0);
$res = do_like($id, $verb);
if ($res) {
if ($type == 'xml')
$ok = "true";
else
$ok = "ok";
return api_apply_template('test', $type, array("$ok" => $ok));
} else {
throw new BadRequestException('Error adding activity');
}
}
api_register_func('api/friendica/activity/like', 'api_friendica_activity', true, API_METHOD_POST);
api_register_func('api/friendica/activity/dislike', 'api_friendica_activity', true, API_METHOD_POST);
api_register_func('api/friendica/activity/attendyes', 'api_friendica_activity', true, API_METHOD_POST);
api_register_func('api/friendica/activity/attendno', 'api_friendica_activity', true, API_METHOD_POST);
api_register_func('api/friendica/activity/attendmaybe', 'api_friendica_activity', true, API_METHOD_POST);
api_register_func('api/friendica/activity/unlike', 'api_friendica_activity', true, API_METHOD_POST);
api_register_func('api/friendica/activity/undislike', 'api_friendica_activity', true, API_METHOD_POST);
api_register_func('api/friendica/activity/unattendyes', 'api_friendica_activity', true, API_METHOD_POST);
api_register_func('api/friendica/activity/unattendno', 'api_friendica_activity', true, API_METHOD_POST);
api_register_func('api/friendica/activity/unattendmaybe', 'api_friendica_activity', true, API_METHOD_POST);
/*
To.Do:
@ -3243,7 +3434,7 @@ To.Do:
[include_rts] => 1
[include_reply_count] => true
[include_descendent_reply_count] => true
(?)
Not implemented by now:

374
include/like.php Normal file
View file

@ -0,0 +1,374 @@
<?php
/**
* @brief add/remove activity to an item
*
* Toggle activities as like,dislike,attend of an item
*
* @param string $item_id
* @param string $verb
* Activity verb. One of
* like, unlike, dislike, undislike, attendyes, unattendyes,
* attendno, unattendno, attendmaybe, unattendmaybe
* @hook 'post_local_end'
* array $arr
* 'post_id' => ID of posted item
*/
function do_like($item_id, $verb) {
$a = get_app();
if(! local_user() && ! remote_user()) {
return false;
}
switch($verb) {
case 'like':
case 'unlike':
$activity = ACTIVITY_LIKE;
break;
case 'dislike':
case 'undislike':
$activity = ACTIVITY_DISLIKE;
break;
case 'attendyes':
case 'unattendyes':
$activity = ACTIVITY_ATTEND;
break;
case 'attendno':
case 'unattendno':
$activity = ACTIVITY_ATTENDNO;
break;
case 'attendmaybe':
case 'unattendmaybe':
$activity = ACTIVITY_ATTENDMAYBE;
break;
default:
return false;
break;
}
logger('like: verb ' . $verb . ' item ' . $item_id);
$r = q("SELECT * FROM `item` WHERE `id` = '%s' OR `uri` = '%s' LIMIT 1",
dbesc($item_id),
dbesc($item_id)
);
if(! $item_id || (! count($r))) {
logger('like: no item ' . $item_id);
return false;
}
$item = $r[0];
$owner_uid = $item['uid'];
if(! can_write_wall($a,$owner_uid)) {
return false;
}
$remote_owner = null;
if(! $item['wall']) {
// The top level post may have been written by somebody on another system
$r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
intval($item['contact-id']),
intval($item['uid'])
);
if(! count($r))
return false;
if(! $r[0]['self'])
$remote_owner = $r[0];
}
// this represents the post owner on this system.
$r = q("SELECT `contact`.*, `user`.`nickname` FROM `contact` LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
WHERE `contact`.`self` = 1 AND `contact`.`uid` = %d LIMIT 1",
intval($owner_uid)
);
if(count($r))
$owner = $r[0];
if(! $owner) {
logger('like: no owner');
return false;
}
if(! $remote_owner)
$remote_owner = $owner;
// This represents the person posting
if((local_user()) && (local_user() == $owner_uid)) {
$contact = $owner;
}
else {
$r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
intval($_SESSION['visitor_id']),
intval($owner_uid)
);
if(count($r))
$contact = $r[0];
}
if(! $contact) {
return false;
}
$verbs = " '".dbesc($activity)."' ";
// event participation are essentially radio toggles. If you make a subsequent choice,
// we need to eradicate your first choice.
if($activity === ACTIVITY_ATTEND || $activity === ACTIVITY_ATTENDNO || $activity === ACTIVITY_ATTENDMAYBE) {
$verbs = " '" . dbesc(ACTIVITY_ATTEND) . "','" . dbesc(ACTIVITY_ATTENDNO) . "','" . dbesc(ACTIVITY_ATTENDMAYBE) . "' ";
}
$r = q("SELECT `id`, `guid` FROM `item` WHERE `verb` IN ( $verbs ) AND `deleted` = 0
AND `contact-id` = %d AND `uid` = %d
AND (`parent` = '%s' OR `parent-uri` = '%s' OR `thr-parent` = '%s') LIMIT 1",
intval($contact['id']), intval($owner_uid),
dbesc($item_id), dbesc($item_id), dbesc($item['uri'])
);
if(count($r)) {
$like_item = $r[0];
// Already voted, undo it
$r = q("UPDATE `item` SET `deleted` = 1, `unseen` = 1, `changed` = '%s' WHERE `id` = %d",
dbesc(datetime_convert()),
intval($like_item['id'])
);
// Clean up the Diaspora signatures for this like
// Go ahead and do it even if Diaspora support is disabled. We still want to clean up
// if it had been enabled in the past
$r = q("DELETE FROM `sign` WHERE `iid` = %d",
intval($like_item['id'])
);
// Save the author information for the unlike in case we need to relay to Diaspora
store_diaspora_like_retract_sig($activity, $item, $like_item, $contact);
$like_item_id = $like_item['id'];
proc_run('php',"include/notifier.php","like","$like_item_id");
return true;
}
$uri = item_new_uri($a->get_hostname(),$owner_uid);
$post_type = (($item['resource-id']) ? t('photo') : t('status'));
if($item['obj_type'] === ACTIVITY_OBJ_EVENT)
$post_type = t('event');
$objtype = (($item['resource-id']) ? ACTIVITY_OBJ_PHOTO : ACTIVITY_OBJ_NOTE );
$link = xmlify('<link rel="alternate" type="text/html" href="' . $a->get_baseurl() . '/display/' . $owner['nickname'] . '/' . $item['id'] . '" />' . "\n") ;
$body = $item['body'];
$obj = <<< EOT
<object>
<type>$objtype</type>
<local>1</local>
<id>{$item['uri']}</id>
<link>$link</link>
<title></title>
<content>$body</content>
</object>
EOT;
if($verb === 'like')
$bodyverb = t('%1$s likes %2$s\'s %3$s');
if($verb === 'dislike')
$bodyverb = t('%1$s doesn\'t like %2$s\'s %3$s');
if($verb === 'attendyes')
$bodyverb = t('%1$s is attending %2$s\'s %3$s');
if($verb === 'attendno')
$bodyverb = t('%1$s is not attending %2$s\'s %3$s');
if($verb === 'attendmaybe')
$bodyverb = t('%1$s may attend %2$s\'s %3$s');
if(! isset($bodyverb))
return false;
$arr = array();
$arr['uri'] = $uri;
$arr['uid'] = $owner_uid;
$arr['contact-id'] = $contact['id'];
$arr['type'] = 'activity';
$arr['wall'] = $item['wall'];
$arr['origin'] = 1;
$arr['gravity'] = GRAVITY_LIKE;
$arr['parent'] = $item['id'];
$arr['parent-uri'] = $item['uri'];
$arr['thr-parent'] = $item['uri'];
$arr['owner-name'] = $remote_owner['name'];
$arr['owner-link'] = $remote_owner['url'];
$arr['owner-avatar'] = $remote_owner['thumb'];
$arr['author-name'] = $contact['name'];
$arr['author-link'] = $contact['url'];
$arr['author-avatar'] = $contact['thumb'];
$ulink = '[url=' . $contact['url'] . ']' . $contact['name'] . '[/url]';
$alink = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]';
$plink = '[url=' . $a->get_baseurl() . '/display/' . $owner['nickname'] . '/' . $item['id'] . ']' . $post_type . '[/url]';
$arr['body'] = sprintf( $bodyverb, $ulink, $alink, $plink );
$arr['verb'] = $activity;
$arr['object-type'] = $objtype;
$arr['object'] = $obj;
$arr['allow_cid'] = $item['allow_cid'];
$arr['allow_gid'] = $item['allow_gid'];
$arr['deny_cid'] = $item['deny_cid'];
$arr['deny_gid'] = $item['deny_gid'];
$arr['visible'] = 1;
$arr['unseen'] = 1;
$arr['last-child'] = 0;
$post_id = item_store($arr);
if(! $item['visible']) {
$r = q("UPDATE `item` SET `visible` = 1 WHERE `id` = %d AND `uid` = %d",
intval($item['id']),
intval($owner_uid)
);
}
// Save the author information for the like in case we need to relay to Diaspora
store_diaspora_like_sig($activity, $post_type, $contact, $post_id);
$arr['id'] = $post_id;
call_hooks('post_local_end', $arr);
proc_run('php',"include/notifier.php","like","$post_id");
return true;
}
function store_diaspora_like_retract_sig($activity, $item, $like_item, $contact) {
// Note that we can only create a signature for a user of the local server. We don't have
// a key for remote users. That is ok, because if a remote user is "unlike"ing a post, it
// means we are the relay, and for relayable_retractions, Diaspora
// only checks the parent_author_signature if it doesn't have to relay further
//
// If $item['resource-id'] exists, it means the item is a photo. Diaspora doesn't support
// likes on photos, so don't bother.
$enabled = intval(get_config('system','diaspora_enabled'));
if(! $enabled) {
logger('mod_like: diaspora support disabled, not storing like retraction signature', LOGGER_DEBUG);
return;
}
logger('mod_like: storing diaspora like retraction signature');
if(($activity === ACTIVITY_LIKE) && (! $item['resource-id'])) {
$signed_text = $like_item['guid'] . ';' . 'Like';
// Only works for NETWORK_DFRN
$contact_baseurl_start = strpos($contact['url'],'://') + 3;
$contact_baseurl_length = strpos($contact['url'],'/profile') - $contact_baseurl_start;
$contact_baseurl = substr($contact['url'], $contact_baseurl_start, $contact_baseurl_length);
$diaspora_handle = $contact['nick'] . '@' . $contact_baseurl;
// Get contact's private key if he's a user of the local Friendica server
$r = q("SELECT `contact`.`uid` FROM `contact` WHERE `url` = '%s' AND `self` = 1 LIMIT 1",
dbesc($contact['url'])
);
if( $r) {
$contact_uid = $r['uid'];
$r = q("SELECT prvkey FROM user WHERE uid = %d LIMIT 1",
intval($contact_uid)
);
if( $r)
$authorsig = base64_encode(rsa_sign($signed_text,$r['prvkey'],'sha256'));
}
if(! isset($authorsig))
$authorsig = '';
q("insert into sign (`retract_iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
intval($like_item['id']),
dbesc($signed_text),
dbesc($authorsig),
dbesc($diaspora_handle)
);
}
return;
}
function store_diaspora_like_sig($activity, $post_type, $contact, $post_id) {
// Note that we can only create a signature for a user of the local server. We don't have
// a key for remote users. That is ok, because if a remote user is "unlike"ing a post, it
// means we are the relay, and for relayable_retractions, Diaspora
// only checks the parent_author_signature if it doesn't have to relay further
$enabled = intval(get_config('system','diaspora_enabled'));
if(! $enabled) {
logger('mod_like: diaspora support disabled, not storing like signature', LOGGER_DEBUG);
return;
}
logger('mod_like: storing diaspora like signature');
if(($activity === ACTIVITY_LIKE) && ($post_type === t('status'))) {
// Only works for NETWORK_DFRN
$contact_baseurl_start = strpos($contact['url'],'://') + 3;
$contact_baseurl_length = strpos($contact['url'],'/profile') - $contact_baseurl_start;
$contact_baseurl = substr($contact['url'], $contact_baseurl_start, $contact_baseurl_length);
$diaspora_handle = $contact['nick'] . '@' . $contact_baseurl;
// Get contact's private key if he's a user of the local Friendica server
$r = q("SELECT `contact`.`uid` FROM `contact` WHERE `url` = '%s' AND `self` = 1 LIMIT 1",
dbesc($contact['url'])
);
if( $r) {
$contact_uid = $r['uid'];
$r = q("SELECT prvkey FROM user WHERE uid = %d LIMIT 1",
intval($contact_uid)
);
if( $r)
$contact_uprvkey = $r['prvkey'];
}
$r = q("SELECT guid, parent FROM `item` WHERE id = %d LIMIT 1",
intval($post_id)
);
if( $r) {
$p = q("SELECT guid FROM `item` WHERE id = %d AND parent = %d LIMIT 1",
intval($r[0]['parent']),
intval($r[0]['parent'])
);
if( $p) {
$signed_text = $r[0]['guid'] . ';Post;' . $p[0]['guid'] . ';true;' . $diaspora_handle;
if(isset($contact_uprvkey))
$authorsig = base64_encode(rsa_sign($signed_text,$contact_uprvkey,'sha256'));
else
$authorsig = '';
q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
intval($post_id),
dbesc($signed_text),
dbesc($authorsig),
dbesc($diaspora_handle)
);
}
}
}
return;
}

View file

@ -36,13 +36,14 @@ function help_content(&$a) {
$path .= argv($x);
}
$title = basename($path);
$filename = $path;
$text = load_doc_file('doc/' . $path . '.md');
$a->page['title'] = t('Help:') . ' ' . str_replace('-', ' ', notags($title));
}
$home = load_doc_file('doc/Home.md');
if (!$text) {
$text = $home;
$filename = "Home";
$a->page['title'] = t('Help');
} else {
$a->page['aside'] = Markdown($home);
@ -57,7 +58,45 @@ function help_content(&$a) {
}
$html = Markdown($text);
$html = "<style>.md_warning { padding: 1em; border: #ff0000 solid 2px; background-color: #f9a3a3; color: #ffffff;</style>".$html;
if ($filename !== "Home") {
// create TOC but not for home
$lines = explode("\n", $html);
$toc="<style>aside ul {padding-left: 1em;}</style><h2>TOC</h2><ul id='toc'>";
$lastlevel=1;
$idnum = array(0,0,0,0,0,0,0);
foreach($lines as &$line){
if (substr($line,0,2)=="<h") {
$level = substr($line,2,1);
if ($level!="r") {
$level = intval($level);
if ($level<$lastlevel) {
for($k=$level;$k<$lastlevel; $k++) $toc.="</ul>";
for($k=$level+1;$k<count($idnum);$k++) $idnum[$k]=0;
}
if ($level>$lastlevel) $toc.="<ul>";
$idnum[$level]++;
$id = implode("_", array_slice($idnum,1,$level));
$href = $a->get_baseurl()."/help/{$filename}#{$id}";
$toc .= "<li><a href='{$href}'>".strip_tags($line)."</a></li>";
$line = "<a name='{$id}'></a>".$line;
$lastlevel = $level;
}
}
}
for($k=1;$k<$lastlevel; $k++) $toc.="</ul>";
$html = implode("\n",$lines);
$a->page['aside'] = $toc.$a->page['aside'];
}
$html = "
<style>
.md_warning {
padding: 1em; border: #ff0000 solid 2px;
background-color: #f9a3a3; color: #ffffff;
}
</style>".$html;
return $html;
}

View file

@ -3,254 +3,27 @@
require_once('include/security.php');
require_once('include/bbcode.php');
require_once('include/items.php');
require_once('include/like.php');
function like_content(&$a) {
if(! local_user() && ! remote_user()) {
return;
return false;
}
$verb = notags(trim($_GET['verb']));
if(! $verb)
$verb = 'like';
switch($verb) {
case 'like':
case 'unlike':
$activity = ACTIVITY_LIKE;
break;
case 'dislike':
case 'undislike':
$activity = ACTIVITY_DISLIKE;
break;
case 'attendyes':
case 'unattendyes':
$activity = ACTIVITY_ATTEND;
break;
case 'attendno':
case 'unattendno':
$activity = ACTIVITY_ATTENDNO;
break;
case 'attendmaybe':
case 'unattendmaybe':
$activity = ACTIVITY_ATTENDMAYBE;
break;
default:
return;
break;
}
$item_id = (($a->argc > 1) ? notags(trim($a->argv[1])) : 0);
logger('like: verb ' . $verb . ' item ' . $item_id);
$r = q("SELECT * FROM `item` WHERE `id` = '%s' OR `uri` = '%s' LIMIT 1",
dbesc($item_id),
dbesc($item_id)
);
if(! $item_id || (! count($r))) {
logger('like: no item ' . $item_id);
return;
}
$item = $r[0];
$owner_uid = $item['uid'];
if(! can_write_wall($a,$owner_uid)) {
return;
}
$remote_owner = null;
if(! $item['wall']) {
// The top level post may have been written by somebody on another system
$r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
intval($item['contact-id']),
intval($item['uid'])
);
if(! count($r))
return;
if(! $r[0]['self'])
$remote_owner = $r[0];
}
// this represents the post owner on this system.
$r = q("SELECT `contact`.*, `user`.`nickname` FROM `contact` LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
WHERE `contact`.`self` = 1 AND `contact`.`uid` = %d LIMIT 1",
intval($owner_uid)
);
if(count($r))
$owner = $r[0];
if(! $owner) {
logger('like: no owner');
return;
}
if(! $remote_owner)
$remote_owner = $owner;
// This represents the person posting
if((local_user()) && (local_user() == $owner_uid)) {
$contact = $owner;
}
else {
$r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
intval($_SESSION['visitor_id']),
intval($owner_uid)
);
if(count($r))
$contact = $r[0];
}
if(! $contact) {
return;
}
$r = do_like($item_id, $verb);
if (!$r) return;
// See if we've been passed a return path to redirect to
$return_path = ((x($_REQUEST,'return')) ? $_REQUEST['return'] : '');
$verbs = " '".dbesc($activity)."' ";
// event participation are essentially radio toggles. If you make a subsequent choice,
// we need to eradicate your first choice.
if($activity === ACTIVITY_ATTEND || $activity === ACTIVITY_ATTENDNO || $activity === ACTIVITY_ATTENDMAYBE) {
$verbs = " '" . dbesc(ACTIVITY_ATTEND) . "','" . dbesc(ACTIVITY_ATTENDNO) . "','" . dbesc(ACTIVITY_ATTENDMAYBE) . "' ";
}
$r = q("SELECT `id`, `guid` FROM `item` WHERE `verb` IN ( $verbs ) AND `deleted` = 0
AND `contact-id` = %d AND `uid` = %d
AND (`parent` = '%s' OR `parent-uri` = '%s' OR `thr-parent` = '%s') LIMIT 1",
intval($contact['id']), intval($owner_uid),
dbesc($item_id), dbesc($item_id), dbesc($item['uri'])
);
if(count($r)) {
$like_item = $r[0];
// Already voted, undo it
$r = q("UPDATE `item` SET `deleted` = 1, `unseen` = 1, `changed` = '%s' WHERE `id` = %d",
dbesc(datetime_convert()),
intval($like_item['id'])
);
// Clean up the Diaspora signatures for this like
// Go ahead and do it even if Diaspora support is disabled. We still want to clean up
// if it had been enabled in the past
$r = q("DELETE FROM `sign` WHERE `iid` = %d",
intval($like_item['id'])
);
// Save the author information for the unlike in case we need to relay to Diaspora
store_diaspora_like_retract_sig($activity, $item, $like_item, $contact);
// proc_run('php',"include/notifier.php","like","$post_id"); // $post_id isn't defined here!
$like_item_id = $like_item['id'];
proc_run('php',"include/notifier.php","like","$like_item_id");
like_content_return($a->get_baseurl(), $return_path);
return; // NOTREACHED
}
$uri = item_new_uri($a->get_hostname(),$owner_uid);
$post_type = (($item['resource-id']) ? t('photo') : t('status'));
if($item['obj_type'] === ACTIVITY_OBJ_EVENT)
$post_type = t('event');
$objtype = (($item['resource-id']) ? ACTIVITY_OBJ_PHOTO : ACTIVITY_OBJ_NOTE );
$link = xmlify('<link rel="alternate" type="text/html" href="' . $a->get_baseurl() . '/display/' . $owner['nickname'] . '/' . $item['id'] . '" />' . "\n") ;
$body = $item['body'];
$obj = <<< EOT
<object>
<type>$objtype</type>
<local>1</local>
<id>{$item['uri']}</id>
<link>$link</link>
<title></title>
<content>$body</content>
</object>
EOT;
if($verb === 'like')
$bodyverb = t('%1$s likes %2$s\'s %3$s');
if($verb === 'dislike')
$bodyverb = t('%1$s doesn\'t like %2$s\'s %3$s');
if($verb === 'attendyes')
$bodyverb = t('%1$s is attending %2$s\'s %3$s');
if($verb === 'attendno')
$bodyverb = t('%1$s is not attending %2$s\'s %3$s');
if($verb === 'attendmaybe')
$bodyverb = t('%1$s may attend %2$s\'s %3$s');
if(! isset($bodyverb))
return;
$arr = array();
$arr['uri'] = $uri;
$arr['uid'] = $owner_uid;
$arr['contact-id'] = $contact['id'];
$arr['type'] = 'activity';
$arr['wall'] = $item['wall'];
$arr['origin'] = 1;
$arr['gravity'] = GRAVITY_LIKE;
$arr['parent'] = $item['id'];
$arr['parent-uri'] = $item['uri'];
$arr['thr-parent'] = $item['uri'];
$arr['owner-name'] = $remote_owner['name'];
$arr['owner-link'] = $remote_owner['url'];
$arr['owner-avatar'] = $remote_owner['thumb'];
$arr['author-name'] = $contact['name'];
$arr['author-link'] = $contact['url'];
$arr['author-avatar'] = $contact['thumb'];
$ulink = '[url=' . $contact['url'] . ']' . $contact['name'] . '[/url]';
$alink = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]';
$plink = '[url=' . $a->get_baseurl() . '/display/' . $owner['nickname'] . '/' . $item['id'] . ']' . $post_type . '[/url]';
$arr['body'] = sprintf( $bodyverb, $ulink, $alink, $plink );
$arr['verb'] = $activity;
$arr['object-type'] = $objtype;
$arr['object'] = $obj;
$arr['allow_cid'] = $item['allow_cid'];
$arr['allow_gid'] = $item['allow_gid'];
$arr['deny_cid'] = $item['deny_cid'];
$arr['deny_gid'] = $item['deny_gid'];
$arr['visible'] = 1;
$arr['unseen'] = 1;
$arr['last-child'] = 0;
$post_id = item_store($arr);
if(! $item['visible']) {
$r = q("UPDATE `item` SET `visible` = 1 WHERE `id` = %d AND `uid` = %d",
intval($item['id']),
intval($owner_uid)
);
}
// Save the author information for the like in case we need to relay to Diaspora
store_diaspora_like_sig($activity, $post_type, $contact, $post_id);
$arr['id'] = $post_id;
call_hooks('post_local_end', $arr);
proc_run('php',"include/notifier.php","like","$post_id");
like_content_return($a->get_baseurl(), $return_path);
killme(); // NOTREACHED
// return; // NOTREACHED
@ -273,123 +46,3 @@ function like_content_return($baseurl, $return_path) {
killme();
}
function store_diaspora_like_retract_sig($activity, $item, $like_item, $contact) {
// Note that we can only create a signature for a user of the local server. We don't have
// a key for remote users. That is ok, because if a remote user is "unlike"ing a post, it
// means we are the relay, and for relayable_retractions, Diaspora
// only checks the parent_author_signature if it doesn't have to relay further
//
// If $item['resource-id'] exists, it means the item is a photo. Diaspora doesn't support
// likes on photos, so don't bother.
$enabled = intval(get_config('system','diaspora_enabled'));
if(! $enabled) {
logger('mod_like: diaspora support disabled, not storing like retraction signature', LOGGER_DEBUG);
return;
}
logger('mod_like: storing diaspora like retraction signature');
if(($activity === ACTIVITY_LIKE) && (! $item['resource-id'])) {
$signed_text = $like_item['guid'] . ';' . 'Like';
// Only works for NETWORK_DFRN
$contact_baseurl_start = strpos($contact['url'],'://') + 3;
$contact_baseurl_length = strpos($contact['url'],'/profile') - $contact_baseurl_start;
$contact_baseurl = substr($contact['url'], $contact_baseurl_start, $contact_baseurl_length);
$diaspora_handle = $contact['nick'] . '@' . $contact_baseurl;
// Get contact's private key if he's a user of the local Friendica server
$r = q("SELECT `contact`.`uid` FROM `contact` WHERE `url` = '%s' AND `self` = 1 LIMIT 1",
dbesc($contact['url'])
);
if( $r) {
$contact_uid = $r['uid'];
$r = q("SELECT prvkey FROM user WHERE uid = %d LIMIT 1",
intval($contact_uid)
);
if( $r)
$authorsig = base64_encode(rsa_sign($signed_text,$r['prvkey'],'sha256'));
}
if(! isset($authorsig))
$authorsig = '';
q("insert into sign (`retract_iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
intval($like_item['id']),
dbesc($signed_text),
dbesc($authorsig),
dbesc($diaspora_handle)
);
}
return;
}
function store_diaspora_like_sig($activity, $post_type, $contact, $post_id) {
// Note that we can only create a signature for a user of the local server. We don't have
// a key for remote users. That is ok, because if a remote user is "unlike"ing a post, it
// means we are the relay, and for relayable_retractions, Diaspora
// only checks the parent_author_signature if it doesn't have to relay further
$enabled = intval(get_config('system','diaspora_enabled'));
if(! $enabled) {
logger('mod_like: diaspora support disabled, not storing like signature', LOGGER_DEBUG);
return;
}
logger('mod_like: storing diaspora like signature');
if(($activity === ACTIVITY_LIKE) && ($post_type === t('status'))) {
// Only works for NETWORK_DFRN
$contact_baseurl_start = strpos($contact['url'],'://') + 3;
$contact_baseurl_length = strpos($contact['url'],'/profile') - $contact_baseurl_start;
$contact_baseurl = substr($contact['url'], $contact_baseurl_start, $contact_baseurl_length);
$diaspora_handle = $contact['nick'] . '@' . $contact_baseurl;
// Get contact's private key if he's a user of the local Friendica server
$r = q("SELECT `contact`.`uid` FROM `contact` WHERE `url` = '%s' AND `self` = 1 LIMIT 1",
dbesc($contact['url'])
);
if( $r) {
$contact_uid = $r['uid'];
$r = q("SELECT prvkey FROM user WHERE uid = %d LIMIT 1",
intval($contact_uid)
);
if( $r)
$contact_uprvkey = $r['prvkey'];
}
$r = q("SELECT guid, parent FROM `item` WHERE id = %d LIMIT 1",
intval($post_id)
);
if( $r) {
$p = q("SELECT guid FROM `item` WHERE id = %d AND parent = %d LIMIT 1",
intval($r[0]['parent']),
intval($r[0]['parent'])
);
if( $p) {
$signed_text = $r[0]['guid'] . ';Post;' . $p[0]['guid'] . ';true;' . $diaspora_handle;
if(isset($contact_uprvkey))
$authorsig = base64_encode(rsa_sign($signed_text,$contact_uprvkey,'sha256'));
else
$authorsig = '';
q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
intval($post_id),
dbesc($signed_text),
dbesc($authorsig),
dbesc($diaspora_handle)
);
}
}
}
return;
}

View file

@ -0,0 +1,21 @@
<photo>
<id>{{$photo.id}}</id>
<created>{{$photo.created}}</created>
<edited>{{$photo.edited}}</edited>
<title>{{$photo.title}}</title>
<desc>{{$photo.desc}}</desc>
<album>{{$photo.album}}</album>
<filename>{{$photo.filename}}</filename>
<type>{{$photo.type}}</type>
<height>{{$photo.height}}</height>
<width>{{$photo.width}}</width>
<datasize>{{$photo.datasize}}</datasize>
<profile>1</profile>
<links type="array">{{foreach $photo.link as $scale => $url}}
<link type="{{$photo.type}}" scale="{{$scale}}" href="{{$url}}" />
{{/foreach}}</links>
{{if $photo.data}}
<data encode="base64">{{$photo.data}}</data>
{{/if}}
</photo>

View file

@ -0,0 +1,5 @@
<photos type="array">
{{foreach $photos as $photo}}
<photo id="{{$photo.id}}" album="{{$photo.album}}" filename="{{$photo.filename}}" type="{{$photo.type}}">{{$photo.thumb}}</photo>
{{/foreach}}</photos>

View file

@ -1,5 +1,7 @@
<statuses type="array" xmlns:statusnet="http://status.net/schema/api/1/">
<statuses type="array"
xmlns:statusnet="http://status.net/schema/api/1/"
xmlns:friendica="http://friendi.ca/schema/api/1/">
{{foreach $statuses as $status}} <status>
<text>{{$status.text}}</text>
<truncated>{{$status.truncated}}</truncated>
@ -17,5 +19,8 @@
<coordinates>{{$status.coordinates}}</coordinates>
<place>{{$status.place}}</place>
<contributors>{{$status.contributors}}</contributors>
<friendica:activities>{{foreach $status.friendica_activities as $k=>$v}}
<friendica:{{$k}}>{{$v}}</friendica:{{$k}}>
{{/foreach}}</friendica:activities>
</status>
{{/foreach}}</statuses>