Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Facebook Broadcasts API coverage #174

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ crashlytics.properties
crashlytics-build.properties
fabric.properties


# Token for Graph API provided for testing
testdata.json

# composer
/vendor/
composer.phar
Expand Down
19 changes: 19 additions & 0 deletions Broadcasts/APIException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php


namespace pimax\Broadcasts;


use LogicException;
use Throwable;

class APIException extends LogicException
{
public $error = [];

public function __construct (array $fbError)
{
$this -> error = $fbError;
parent::__construct($fbError['message'], $fbError['error_subcode']);
}
}
230 changes: 230 additions & 0 deletions Broadcasts/Broadcasts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
<?php


namespace pimax\Broadcasts;


use pimax\Messages\Message;

trait Broadcasts
{
/**
* Broadcast API
* Send messages to groups of users marked by tags or all users
*
* @see https://developers.facebook.com/docs/messenger-platform/reference/broadcast-api/
* @see https://developers.facebook.com/docs/messenger-platform/send-messages/broadcast-messages/target-broadcasts
*
* TODO estimate broadcast reach
* TODO @see https://developers.facebook.com/docs/messenger-platform/send-messages/broadcast-messages/estimate-reach
*
* TODO broadcast metrics
* TODO @see https://developers.facebook.com/docs/messenger-platform/send-messages/broadcast-messages#metrics
*
* TODO Cancelling a Scheduled Broadcast etc
*/

/**
* LABELS are created to be bound to users. Broadcasts may be further filtered by labels or label predicates
*/

/**
* Create a label to be added to
* @param string $name
* @return array {"id":"1001200005002"}
*/
public function createLabel(string $name): array {
$label = $this -> call('me/custom_labels', compact('name'), self::TYPE_POST);
if (!empty($label['error'])) {
// Label with this name already exists
if ($label['error']['error_subcode'] == 2018210) {
// Is not supposed to fail. Not at all.
return $this -> getLabelByName($name);
} else throw new APIException($label['error']);
}
return $label;
}

/**
* Get label details (useless)
* @param string $id
* @return array Label {"name":"myLabel","id":"1001200005002"}
*/
public function getLabel(string $id): array {
return $this -> call($id, [
'fields' => 'name'
], self::TYPE_GET);
}

/**
* Returns the first label with the given name (Names are unique)
* @param string $name
* @return array Label {"name":"myLabel","id":"1001200005002"}
*/
public function getLabelByName(string $name): array {
$found = array_filter($this -> getAllLabels()['data'], function($label) use ($name) {
return $label['name'] == $name;
});
if (($c = count($found)) != 1)
throw new BroadcastsException("Found $c labels with a given name: $name");
$f = array_shift($found);
return [
'name' => $name,
'id' => (string) $f['id']
];
}

/**
* Get all labels
* @return array {"data": [Label]}
* TODO pagination. This doesn't include pagination and this worries me o_O Don't ask for dozens of labels with this
*/
public function getAllLabels(): array {
return $this -> call('me/custom_labels', [
'fields' => 'name'
], self::TYPE_GET);
}


/**
* Deletes a label and all bindings
* @param string $id
* @return array {"success": true}
*/
public function deleteLabel(string $id): array {
return $this -> call($id, [], self::TYPE_DELETE);
}

/**
* Binds the given user with the given label
* Binding a label to a user which is already bound to this level will NOT cause an error <3
* @param int|string $labelID
* @param int|string $PSID User ID
* @return array {"success": true}
*/
public function bindLabel($labelID, $PSID): array {
return $this -> call("$labelID/label", [
'user' => $PSID
], self::TYPE_POST);
}

/**
* Removes the binding of the given user to the given label
* @param int|string $labelID
* @param int|string $PSID User ID
* @return array {"success": true}
*/
public function unbindLabel($labelID, $PSID): array {
return $this -> call("$labelID/label", [
'user' => $PSID
], self::TYPE_DELETE);
}


/**
* Get all labels with a given bound PSID
* @param int|string $PSID
* @return array {"data": [Label]}
* TODO pagination. This doesn't include pagination and this worries me o_O Don't ask for dozens of labels with this
*/
public function getLabelsByPSID(string $PSID): array {
return $this -> call("$PSID/custom_labels", [
'fields' => 'name'
], self::TYPE_GET);
}


/**
* Message creatives are created and supplied into broadcasts.
* Message creative is like a draft for broadcasts
*/

/**
* Create message creative.
* For whatever reason the API requires a message array,
* but it's stated that no more than one message might be included
* @see https://developers.facebook.com/docs/messenger-platform/send-messages/broadcast-messages#creating
* @param Message $message
*
* <b>Please note. Unsupported templates:</b>
*
* Airline boarding pass template
* Airline check-in template
* Airline itinerary template
* Airline flight update template
* Receipt template
* Open graph template
*
* It isn't as if this lib supports them anyway...
*
* @return array
*/
public function createMessageCreative(Message $message): array {
return $this -> call('me/message_creatives', [
'messages' => [
$message -> getData()['message']
]
], self::TYPE_POST);
}


/**
* This sends a distribution over all users of the page
*
* Broadcast API reach is limited to 10,000 recipients per message.
* @see https://developers.facebook.com/docs/messenger-platform/reference/broadcast-api/#limits
*
* @param int|string $messageCreativeID
* @param string $notificationType
* @param int|string|null $customLabelID
* @param int|null $scheduleTimestamp seconds Unix Epoch
* @param array|null $targeting Targeting predicates
* @return array
*/
public function broadcast(
$messageCreativeID,
string $notificationType,
$customLabelID = NULL,
int $scheduleTimestamp = NULL,
array $targeting = NULL
): array {
$data = [
"message_creative_id" => $messageCreativeID,
"notification_type" => $notificationType,
"messaging_type" => "MESSAGE_TAG",
"tag" => "NON_PROMOTIONAL_SUBSCRIPTION" // They seriously had to include this. Always this value. Come on.
];
if ($scheduleTimestamp)
$data['scheduled_time'] = $scheduleTimestamp;
if ($customLabelID)
$data['custom_label_id'] = $customLabelID;
if ($targeting)
$data['targeting'] = $targeting;

return $this -> call('me/broadcast_messages', $data, self::TYPE_POST);
}


/**
* One-time message broadcast
*
* @param Message $message
* @param Targeting $predicates All Label IDs must be valid!
* @param int|NULL $scheduleTimestamp
* @param string $notificationType
* @return array
*/
public function broadcastMessage(
Message $message,
Targeting $predicates,
int $scheduleTimestamp = NULL,
string $notificationType = Message::NOTIFY_SILENT_PUSH
): array {
$messageCreativeID = $this -> createMessageCreative($message);
if (!$mID = $messageCreativeID['message_creative_id'])
throw new APIException($messageCreativeID);
return $this -> broadcast($mID, $notificationType, NULL, $scheduleTimestamp, $predicates -> getData());
}


}
12 changes: 12 additions & 0 deletions Broadcasts/BroadcastsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php


namespace pimax\Broadcasts;


use LogicException;

class BroadcastsException extends LogicException
{

}
34 changes: 34 additions & 0 deletions Broadcasts/Predicate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php


namespace pimax\Broadcasts;


class Predicate
{
const AND = 'AND';
const OR = 'OR';
const NOT = 'NOT';

private $operator;
private $values;

public function __construct (string $operator, array $values)
{
$this -> operator = $operator;
$this -> values = $values;
}

public function getData() {
return [
'operator' => $this -> operator,
'values' => array_map(function($x) {
if (is_object($x) && $x instanceof self)
return $x -> getData();
elseif (is_string($x))
return $x;
else throw new BroadcastsException('Invalid type of predicate: '.gettype($x));
}, $this -> values)
];
}
}
16 changes: 16 additions & 0 deletions Broadcasts/Predicates/AndPredicate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php


namespace pimax\Broadcasts\Predicates;


use pimax\Broadcasts\Predicate;

class AndPredicate extends TwoValuePredicate
{
public function __construct (...$values)
{
parent::__construct(self::AND, $values);
}

}
22 changes: 22 additions & 0 deletions Broadcasts/Predicates/NotPredicate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php


namespace pimax\Broadcasts\Predicates;


use pimax\Broadcasts\BroadcastsException;
use pimax\Broadcasts\Predicate;

class NotPredicate extends Predicate
{
public function __construct (...$values)
{
/**
* @see https://developers.facebook.com/docs/messenger-platform/send-messages/broadcast-messages/target-broadcasts#operators
*/
if (count($values) > 1)
throw new BroadcastsException('Not operator only supports one value, either string or another predicate');
parent::__construct(self::NOT, $values);
}

}
16 changes: 16 additions & 0 deletions Broadcasts/Predicates/OrPredicate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php


namespace pimax\Broadcasts\Predicates;


use pimax\Broadcasts\Predicate;

class OrPredicate extends TwoValuePredicate
{
public function __construct (...$values)
{
parent::__construct(self::OR, $values);
}

}
19 changes: 19 additions & 0 deletions Broadcasts/Predicates/TwoValuePredicate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php


namespace pimax\Broadcasts\Predicates;


use pimax\Broadcasts\BroadcastsException;
use pimax\Broadcasts\Predicate;

class TwoValuePredicate extends Predicate
{
public function __construct (string $operator, array $values)
{
if (count($values) < 2)
throw new BroadcastsException('This predicate requires 2 or more values');
parent::__construct($operator, $values);
}

}
Loading