forked from grumpydevelop/federator
initial support for actually sending NewContent
- integrated functionality to actually send new content to federated recipients and followers (IT WORKS!!) - changed the way we remove a follow to return the removed followId (used in order to build the undo follow activity)
This commit is contained in:
parent
7a5870de95
commit
5c90b4cfc9
5 changed files with 229 additions and 41 deletions
|
@ -140,9 +140,6 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
&& (filter_var($receiver, FILTER_VALIDATE_URL) !== false);
|
||||
});
|
||||
|
||||
if (!in_array($posterName, $receivers, true)) {
|
||||
$receivers[] = $posterName;
|
||||
}
|
||||
foreach ($receivers as $receiver) {
|
||||
if ($receiver === '' || !is_string($receiver)) {
|
||||
continue;
|
||||
|
@ -173,7 +170,6 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
$domain = parse_url($receiver, PHP_URL_HOST);
|
||||
if ($receiverName === null || $domain === null) {
|
||||
if ($receiver === $posterName) {
|
||||
$users[] = $receiver;
|
||||
continue;
|
||||
}
|
||||
error_log("NewContent::post no receiverName or domain found for receiver: " . $receiver);
|
||||
|
@ -232,12 +228,13 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
* connector to fetch use with
|
||||
* @param \Federator\Cache\Cache|null $cache
|
||||
* optional caching service
|
||||
* @param string $host host url of our server (e.g. https://federator.com)
|
||||
* @param string $_user user that triggered the post
|
||||
* @param string $_recipientId recipient of the post
|
||||
* @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received
|
||||
* @return boolean response
|
||||
*/
|
||||
public static function postForUser($dbh, $connector, $cache, $_user, $_recipientId, $newActivity)
|
||||
public static function postForUser($dbh, $connector, $cache, $host, $_user, $_recipientId, $newActivity)
|
||||
{
|
||||
if (!isset($_user)) {
|
||||
error_log("NewContent::postForUser no user given");
|
||||
|
@ -284,13 +281,15 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
if ($actor !== '') {
|
||||
$followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
|
||||
$followerDomain = parse_url($actor, PHP_URL_HOST);
|
||||
$newIdUrl = \Federator\DIO\Followers::generateNewFollowId($dbh, $host);
|
||||
$newActivity->setID($newIdUrl);
|
||||
if (is_string($followerDomain)) {
|
||||
$followerId = "{$followerUsername}@{$followerDomain}";
|
||||
$success = \Federator\DIO\Followers::addFollow($dbh, $followerId, $user->id, $followerDomain);
|
||||
// $success = \Federator\DIO\Followers::sendFollowRequest($dbh, $connector, $cache, $user->id, $followerId, $followerDomain);
|
||||
}
|
||||
}
|
||||
if ($success === false) {
|
||||
error_log("NewContent::postForUser: Failed to add follower for user $user->id");
|
||||
error_log("NewContent::postForUser Failed to add follower for user $user->id");
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -318,12 +317,19 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
$followerDomain = parse_url($actor, PHP_URL_HOST);
|
||||
if (is_string($followerDomain)) {
|
||||
$followerId = "{$followerUsername}@{$followerDomain}";
|
||||
$success = \Federator\DIO\Followers::removeFollow($dbh, $followerId, $user->id);
|
||||
$removedId = \Federator\DIO\Followers::removeFollow($dbh, $followerId, $user->id);
|
||||
if ($removedId !== false) {
|
||||
$object->setID($removedId);
|
||||
$newActivity->setObject($object);
|
||||
$success = true;
|
||||
} else {
|
||||
error_log("NewContent::postForUser Failed to remove follow for user $user->id");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($success === false) {
|
||||
error_log("NewContent::postForUser: Failed to remove follower for user $user->id");
|
||||
error_log("NewContent::postForUser Failed to remove follower for user $user->id");
|
||||
}
|
||||
break;
|
||||
case 'Like':
|
||||
|
@ -334,7 +340,7 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId);
|
||||
\Federator\DIO\Posts::deletePost($dbh, $targetId);
|
||||
} else {
|
||||
error_log("NewContent::postForUser: Error in Undo Like/Dislike for user $user->id, targetId is not a string");
|
||||
error_log("NewContent::postForUser Error in Undo Like/Dislike for user $user->id, targetId is not a string");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -358,7 +364,7 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
} else if (is_string($object)) {
|
||||
\Federator\DIO\Posts::deletePost($dbh, $object);
|
||||
} else {
|
||||
error_log("NewContent::postForUser: Error in Undo for user $user->id, object is not a string or object");
|
||||
error_log("NewContent::postForUser Error in Undo for user $user->id, object is not a string or object");
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -370,7 +376,7 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
|
||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
||||
} else {
|
||||
error_log("NewContent::postForUser: Error in Add Like/Dislike for user $user->id, targetId is not a string");
|
||||
error_log("NewContent::postForUser Error in Add Like/Dislike for user $user->id, targetId is not a string");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
@ -391,13 +397,118 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
break;
|
||||
|
||||
default:
|
||||
error_log("NewContent::postForUser: Unhandled activity type $type for user $user->id");
|
||||
error_log("NewContent::postForUser Unhandled activity type $type for user $user->id");
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = self::sendActivity($dbh, $host, $user, $recipient, $newActivity);
|
||||
} catch (\Exception $e) {
|
||||
error_log("NewContent::postForUser Failed to send activity: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
if (empty($response)) {
|
||||
error_log("NewContent::postForUser Sent activity to $recipient->id");
|
||||
} else {
|
||||
error_log("NewContent::postForUser Sent activity to $recipient->id with response: " . json_encode($response, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* send activity to federated server
|
||||
*
|
||||
* @param \mysqli $dbh database handle
|
||||
* @param string $host host url of our server (e.g. federator)
|
||||
* @param \Federator\Data\User $sender source user
|
||||
* @param \Federator\Data\FedUser $target federated target user
|
||||
* @param \Federator\Data\ActivityPub\Common\Activity $activity activity to send
|
||||
* @return string|true the generated follow ID on success, false on failure
|
||||
*/
|
||||
public static function sendActivity($dbh, $host, $sender, $target, $activity)
|
||||
{
|
||||
if ($dbh === false) {
|
||||
throw new \Federator\Exceptions\ServerError("NewContent::sendActivity Failed to get database handle");
|
||||
}
|
||||
|
||||
$inboxUrl = $target->inboxURL;
|
||||
|
||||
$json = json_encode($activity, JSON_UNESCAPED_SLASHES);
|
||||
|
||||
if ($json === false) {
|
||||
throw new \Exception('Failed to encode JSON: ' . json_last_error_msg());
|
||||
}
|
||||
$digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true));
|
||||
$date = gmdate('D, d M Y H:i:s') . ' GMT';
|
||||
$parsed = parse_url($inboxUrl);
|
||||
if ($parsed === false) {
|
||||
throw new \Exception('Failed to parse URL: ' . $inboxUrl);
|
||||
}
|
||||
|
||||
if (!isset($parsed['host']) || !isset($parsed['path'])) {
|
||||
throw new \Exception('Invalid inbox URL: missing host or path');
|
||||
}
|
||||
$extHost = $parsed['host'];
|
||||
$path = $parsed['path'];
|
||||
|
||||
// Build the signature string
|
||||
$signatureString = "(request-target): post {$path}\n" .
|
||||
"host: {$extHost}\n" .
|
||||
"date: {$date}\n" .
|
||||
"digest: {$digest}";
|
||||
|
||||
// Get rsa private key
|
||||
$privateKey = \Federator\DIO\User::getrsaprivate($dbh, $sender->id); // OR from DB
|
||||
if ($privateKey === false) {
|
||||
throw new \Exception('Failed to get private key');
|
||||
}
|
||||
$pkeyId = openssl_pkey_get_private($privateKey);
|
||||
|
||||
if ($pkeyId === false) {
|
||||
throw new \Exception('Invalid private key');
|
||||
}
|
||||
|
||||
openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
|
||||
$signature_b64 = base64_encode($signature);
|
||||
|
||||
// Build keyId (public key ID from your actor object)
|
||||
$keyId = $host . '/' . $sender->id . '#main-key';
|
||||
|
||||
$signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
|
||||
|
||||
$ch = curl_init($inboxUrl);
|
||||
if ($ch === false) {
|
||||
throw new \Exception('Failed to initialize cURL');
|
||||
}
|
||||
$headers = [
|
||||
'Host: ' . $extHost,
|
||||
'Date: ' . $date,
|
||||
'Digest: ' . $digest,
|
||||
'Content-Type: application/activity+json',
|
||||
'Signature: ' . $signatureHeader,
|
||||
'Accept: application/activity+json',
|
||||
];
|
||||
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
// Log the response for debugging if needed
|
||||
if ($response === false) {
|
||||
throw new \Exception("Failed to send activity: " . curl_error($ch));
|
||||
} else {
|
||||
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if ($httpcode != 200 && $httpcode != 202) {
|
||||
throw new \Exception("Unexpected HTTP code $httpcode: $response");
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* get internal represenation as json string
|
||||
* @return string json string or html
|
||||
|
|
|
@ -388,25 +388,97 @@ class Followers
|
|||
return $idurl; // Return the generated follow ID
|
||||
}
|
||||
|
||||
/**
|
||||
* generate new follow id
|
||||
*
|
||||
* @param \mysqli $dbh database handle
|
||||
* @param string $hostUrl the host URL (e.g. federator URL)
|
||||
* @return string the new follow id
|
||||
*/
|
||||
public static function generateNewFollowId($dbh, $hostUrl)
|
||||
{
|
||||
// Generate a new unique follow ID
|
||||
do {
|
||||
$newId = bin2hex(openssl_random_pseudo_bytes(16));
|
||||
$newIdUrl = $hostUrl . '/' . $newId;
|
||||
|
||||
// Check if the generated ID is unique
|
||||
$sql = 'select id from follows where id = ?';
|
||||
$stmt = $dbh->prepare($sql);
|
||||
if ($stmt === false) {
|
||||
throw new \Federator\Exceptions\ServerError("Followers::generateNewFollowId Failed to prepare id-check statement");
|
||||
}
|
||||
$stmt->bind_param("s", $newIdUrl);
|
||||
$foundId = 0;
|
||||
$ret = $stmt->bind_result($foundId);
|
||||
$stmt->execute();
|
||||
if ($ret) {
|
||||
$stmt->fetch();
|
||||
}
|
||||
$stmt->close();
|
||||
} while ($foundId > 0);
|
||||
|
||||
return $newIdUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* remove follow
|
||||
*
|
||||
* @param \mysqli $dbh database handle
|
||||
* @param string $sourceUser source user id
|
||||
* @param string $targetUserId target user id
|
||||
* @return bool true on success
|
||||
* @return string|false removed followId on success, false on failure
|
||||
*/
|
||||
public static function removeFollow($dbh, $sourceUser, $targetUserId)
|
||||
{
|
||||
$sql = 'delete from follows where source_user = ? and target_user = ?';
|
||||
// Combine retrieval and removal in one query using MySQL's RETURNING (if supported)
|
||||
$sql = 'delete from follows where source_user = ? and target_user = ? RETURNING id';
|
||||
$stmt = $dbh->prepare($sql);
|
||||
if ($stmt === false) {
|
||||
throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare statement");
|
||||
if ($stmt !== false) {
|
||||
$stmt->bind_param("ss", $sourceUser, $targetUserId);
|
||||
if ($stmt->execute()) {
|
||||
$stmt->bind_result($followId);
|
||||
if ($stmt->fetch() === true) {
|
||||
$stmt->close();
|
||||
if (!empty($followId)) {
|
||||
return $followId;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
// Fallback for MySQL versions that do not support RETURNING
|
||||
// First, fetch the id of the follow to be removed
|
||||
$sql = 'select id from follows where source_user = ? and target_user = ?';
|
||||
$stmt = $dbh->prepare($sql);
|
||||
if ($stmt === false) {
|
||||
throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare select statement");
|
||||
}
|
||||
$stmt->bind_param("ss", $sourceUser, $targetUserId);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($followId);
|
||||
$found = $stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
if ($found === false || empty($followId)) {
|
||||
return false; // No such follow found
|
||||
}
|
||||
|
||||
// Now, delete the row
|
||||
$sql = 'delete from follows where source_user = ? and target_user = ?';
|
||||
$stmt = $dbh->prepare($sql);
|
||||
if ($stmt === false) {
|
||||
throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare delete statement");
|
||||
}
|
||||
$stmt->bind_param("ss", $sourceUser, $targetUserId);
|
||||
$stmt->execute();
|
||||
$affectedRows = $stmt->affected_rows;
|
||||
$stmt->close();
|
||||
|
||||
return $affectedRows > 0 ? $followId : false;
|
||||
}
|
||||
$stmt->bind_param("ss", $sourceUser, $targetUserId);
|
||||
$stmt->execute();
|
||||
$affectedRows = $stmt->affected_rows;
|
||||
$stmt->close();
|
||||
return $affectedRows > 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ class InboxJob extends \Federator\Api
|
|||
*/
|
||||
public function perform(): bool
|
||||
{
|
||||
error_log("InboxJob: Starting inbox job");
|
||||
error_log("InboxJob: Starting job");
|
||||
$user = $this->args['user'];
|
||||
$recipientId = $this->args['recipientId'];
|
||||
$activity = $this->args['activity'];
|
||||
|
|
|
@ -52,7 +52,7 @@ class NewContentJob extends \Federator\Api
|
|||
*/
|
||||
public function perform(): bool
|
||||
{
|
||||
error_log("NewContentJob: Starting inbox job");
|
||||
error_log("NewContentJob: Starting job");
|
||||
$user = $this->args['user'];
|
||||
$recipientId = $this->args['recipientId'];
|
||||
$activity = $this->args['activity'];
|
||||
|
@ -63,8 +63,11 @@ class NewContentJob extends \Federator\Api
|
|||
error_log("NewContentJob: Failed to create activity from JSON");
|
||||
return false;
|
||||
}
|
||||
$domain = $this->config['generic']['externaldomain'];
|
||||
|
||||
\Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $user, $recipientId, $activity);
|
||||
$ourUrl = 'https://' . $domain;
|
||||
|
||||
\Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $ourUrl, $user, $recipientId, $activity);
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -139,23 +139,25 @@ class ContentNation implements Connector
|
|||
$activities = $r['activities'];
|
||||
$config = $this->main->getConfig();
|
||||
$domain = $config['generic']['externaldomain'];
|
||||
$ourUrl = 'https://' . $domain;
|
||||
|
||||
$imgpath = $this->config['userdata']['path'];
|
||||
$userdata = $this->config['userdata']['url'];
|
||||
foreach ($activities as $activity) {
|
||||
switch ($activity['type']) {
|
||||
case 'Article':
|
||||
$create = new \Federator\Data\ActivityPub\Common\Create();
|
||||
$create->setAActor('https://' . $domain . '/' . $userId);
|
||||
$create->setAActor($ourUrl . '/' . $userId);
|
||||
$create->setID($activity['id'])
|
||||
->setPublished($activity['published'] ?? $activity['timestamp'])
|
||||
->addTo('https://' . $domain . '/' . $userId . '/followers')
|
||||
->addTo($ourUrl . '/' . $userId . '/followers')
|
||||
->addCC("https://www.w3.org/ns/activitystreams#Public");
|
||||
$create->setURL('https://' . $domain . '/' . $activity['profilename'] . '/' . $activity['name']);
|
||||
$create->setID('https://' . $domain . '/' . $activity['profilename'] . '/' . $activity['id']);
|
||||
$create->setURL($ourUrl . '/' . $activity['profilename'] . '/' . $activity['name']);
|
||||
$create->setID($ourUrl . '/' . $activity['profilename'] . '/' . $activity['id']);
|
||||
$apArticle = new \Federator\Data\ActivityPub\Common\Article();
|
||||
if (array_key_exists('tags', $activity)) {
|
||||
foreach ($activity['tags'] as $tag) {
|
||||
$href = 'https://' . $domain . '/search.htm?tagsearch=' . urlencode($tag);
|
||||
$href = $ourUrl . '/search.htm?tagsearch=' . urlencode($tag);
|
||||
$tagObj = new \Federator\Data\ActivityPub\Common\Tag();
|
||||
$tagObj->setHref($href)
|
||||
->setName('#' . urlencode(str_replace(' ', '', $tag)))
|
||||
|
@ -165,7 +167,7 @@ class ContentNation implements Connector
|
|||
}
|
||||
$apArticle->setPublished($activity['published'])
|
||||
->setName($activity['title'])
|
||||
->setAttributedTo('https://' . $domain . '/' . $activity['profilename'])
|
||||
->setAttributedTo($ourUrl . '/' . $activity['profilename'])
|
||||
->setContent(
|
||||
$activity['teaser'] ??
|
||||
$this->main->translate(
|
||||
|
@ -175,10 +177,10 @@ class ContentNation implements Connector
|
|||
)
|
||||
)
|
||||
->addTo("https://www.w3.org/ns/activitystreams#Public")
|
||||
->addCC('https://' . $domain . '/' . $userId . '/followers.json');
|
||||
->addCC($ourUrl . '/' . $userId . '/followers.json');
|
||||
$articleimage = $activity['imagealt'] ??
|
||||
$this->main->translate($activity['language'], 'article', 'image');
|
||||
$idurl = 'https://' . $domain . '/' . $userId . '/' . $activity['name'];
|
||||
$idurl = $ourUrl . '/' . $userId . '/' . $activity['name'];
|
||||
$apArticle->setID($idurl)
|
||||
->setURL($idurl);
|
||||
$image = $activity['image'] ?? $activity['profileimg'];
|
||||
|
@ -200,15 +202,15 @@ class ContentNation implements Connector
|
|||
|
||||
case 'Comment':
|
||||
$create = new \Federator\Data\ActivityPub\Common\Create();
|
||||
$create->setAActor('https://' . $domain . '/' . $userId);
|
||||
$create->setAActor($ourUrl . '/' . $userId);
|
||||
$create->setID($activity['id'])
|
||||
->setPublished($activity['published'] ?? $activity['timestamp'])
|
||||
->addTo('https://' . $domain . '/' . $userId . '/followers')
|
||||
->addTo($ourUrl . '/' . $userId . '/followers')
|
||||
->addCC("https://www.w3.org/ns/activitystreams#Public");
|
||||
$commentJson = $activity;
|
||||
$commentJson['type'] = 'Note';
|
||||
$commentJson['summary'] = $activity['subject'];
|
||||
$commentJson['id'] = 'https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id'];
|
||||
$commentJson['id'] = $ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id'];
|
||||
$note = \Federator\Data\ActivityPub\Factory::newFromJson($commentJson, "");
|
||||
if ($note === null) {
|
||||
error_log("ContentNation::getRemotePostsByUser couldn't create comment");
|
||||
|
@ -218,11 +220,11 @@ class ContentNation implements Connector
|
|||
}
|
||||
$note->setID($commentJson['id']);
|
||||
if (!isset($commentJson['parent']) || $commentJson['parent'] === null) {
|
||||
$note->setInReplyTo('https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName']);
|
||||
$note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName']);
|
||||
} else {
|
||||
$note->setInReplyTo('https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . "#" . $commentJson['parent']);
|
||||
$note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . "#" . $commentJson['parent']);
|
||||
}
|
||||
$url = 'https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id'];
|
||||
$url = $ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id'];
|
||||
$create->setURL($url);
|
||||
$create->setID($url);
|
||||
$create->setObject($note);
|
||||
|
@ -233,7 +235,7 @@ class ContentNation implements Connector
|
|||
// Build Like/Dislike as top-level activity
|
||||
$likeType = $activity['upvote'] === true ? 'Like' : 'Dislike';
|
||||
$like = new \Federator\Data\ActivityPub\Common\Activity($likeType);
|
||||
$like->setAActor('https://' . $domain . '/' . $userId);
|
||||
$like->setAActor($ourUrl . '/' . $userId);
|
||||
$like->setID($activity['id'])
|
||||
->setPublished($activity['published'] ?? $activity['timestamp']);
|
||||
// $like->addTo("https://www.w3.org/ns/activitystreams#Public")
|
||||
|
@ -246,7 +248,7 @@ class ContentNation implements Connector
|
|||
[$activity['username']]
|
||||
)
|
||||
);
|
||||
$objectUrl = 'https://' . $domain . '/' . $userId . '/' . $activity['articlename'];
|
||||
$objectUrl = $ourUrl . '/' . $userId . '/' . $activity['articlename'];
|
||||
$like->setURL($objectUrl . '#' . $activity['id']);
|
||||
$like->setID($objectUrl . '#' . $activity['id']);
|
||||
$like->setObject($objectUrl);
|
||||
|
|
Loading…
Add table
Reference in a new issue