Merge remote-tracking branch 'upstream/develop' into fetch-usage

This commit is contained in:
Michael 2022-03-19 11:21:38 +00:00
commit 4e45987f74
196 changed files with 19366 additions and 17273 deletions

View file

@ -64,6 +64,7 @@ Options
-s|--savedb Save the DB credentials to the file (if environment variables is used)
-H|--dbhost <host> The host of the mysql/mariadb database (env MYSQL_HOST)
-p|--dbport <port> The port of the mysql/mariadb database (env MYSQL_PORT)
-s|--dbsocket <socket> The socket of the mysql/mariadb database (env MYSQL_SOCKET)
-d|--dbdata <database> The name of the mysql/mariadb database (env MYSQL_DATABASE)
-u|--dbuser <username> The username of the mysql/mariadb database login (env MYSQL_USER or MYSQL_USERNAME)
-P|--dbpass <password> The password of the mysql/mariadb database login (env MYSQL_PASSWORD)
@ -76,6 +77,7 @@ Options
Environment variables
MYSQL_HOST The host of the mysql/mariadb database (mandatory if mysql and environment is used)
MYSQL_PORT The port of the mysql/mariadb database
MYSQL_SOCKET The socket of the mysql/mariadb database
MYSQL_USERNAME|MYSQL_USER The username of the mysql/mariadb database login (MYSQL_USERNAME is for mysql, MYSQL_USER for mariadb)
MYSQL_PASSWORD The password of the mysql/mariadb database login
MYSQL_DATABASE The name of the mysql/mariadb database
@ -157,6 +159,7 @@ HELP;
$db_host = $this->getOption(['H', 'dbhost'], ($save_db) ? (getenv('MYSQL_HOST')) : Installer::DEFAULT_HOST);
$db_port = $this->getOption(['p', 'dbport'], ($save_db) ? getenv('MYSQL_PORT') : null);
$db_socket = $this->getOption(['s', 'dbsocket'], ($save_db) ? getenv('MYSQL_SOCKET') : null);
$configCache->set('database', 'hostname', $db_host . (!empty($db_port) ? ':' . $db_port : ''));
$configCache->set('database', 'database',
$this->getOption(['d', 'dbdata'],

View file

@ -151,7 +151,7 @@ HELP;
$this->out("{$cat}.{$key}[{$k}] => " . (is_array($v) ? implode(', ', $v) : $v));
}
} else {
$this->out("{$cat}.{$key} => " . $value);
$this->out("{$cat}.{$key} => " . ($value ?? 'NULL'));
}
}

View file

@ -199,19 +199,18 @@ HELP;
throw new RuntimeException('Contact not found');
}
$user = UserModel::getById($contact['uid']);
if (empty($contact['uid'])) {
throw new RuntimeException('Contact must be user-specific (uid != 0)');
}
try {
$result = ContactModel::terminateFriendship($user, $contact);
if ($result === false) {
throw new RuntimeException('Unable to unfollow this contact, please retry in a few minutes or check the logs.');
}
ContactModel::unfollow($contact);
$this->out('Contact was successfully unfollowed');
return true;
} catch (\Exception $e) {
DI::logger()->error($e->getMessage(), ['owner' => $user, 'contact' => $contact]);
DI::logger()->error($e->getMessage(), ['contact' => $contact]);
throw new RuntimeException('Unable to unfollow this contact, please check the log');
}
}

View file

@ -154,7 +154,7 @@ class Conversation
// Skip when the causer of the parent is the same than the author of the announce
if (($verb == Activity::ANNOUNCE) && Post::exists(['uri-id' => $activity['thr-parent-id'],
'uid' => $activity['uid'], 'causer-id' => $activity['author-id'], 'gravity' => GRAVITY_PARENT])) {
'uid' => $activity['uid'], 'causer-id' => $activity['author-id'], 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]])) {
continue;
}
@ -843,7 +843,7 @@ class Conversation
$row['owner-name'] = $row['causer-name'];
}
if (($row['gravity'] == GRAVITY_PARENT) && !empty($row['causer-id'])) {
if (in_array($row['gravity'], [GRAVITY_PARENT, GRAVITY_COMMENT]) && !empty($row['causer-id'])) {
$causer = ['uid' => 0, 'id' => $row['causer-id'], 'network' => $row['causer-network'], 'url' => $row['causer-link']];
$row['reshared'] = $this->l10n->t('%s reshared this.', '<a href="'. htmlentities(Contact::magicLinkByContact($causer)) .'">' . htmlentities($row['causer-name']) . '</a>');

View file

@ -104,6 +104,7 @@ class Feature
DI::l10n()->t('Post Composition Features'),
['aclautomention', DI::l10n()->t('Auto-mention Forums'), DI::l10n()->t('Add/remove mention when a forum page is selected/deselected in ACL window.'), false, DI::config()->get('feature_lock', 'aclautomention', false)],
['explicit_mentions', DI::l10n()->t('Explicit Mentions'), DI::l10n()->t('Add explicit mentions to comment box for manual control over who gets mentioned in replies.'), false, DI::config()->get('feature_lock', 'explicit_mentions', false)],
['add_abstract', DI::l10n()->t('Add an abstract from ActivityPub content warnings'), DI::l10n()->t('Add an abstract when commenting on ActivityPub posts with a content warning. Abstracts are displayed as content warning on systems like Mastodon or Pleroma.'), false, DI::config()->get('feature_lock', 'add_abstract', false)],
],
// Item tools

View file

@ -21,12 +21,15 @@
namespace Friendica\Content;
use Friendica\Content\Text\BBCode;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\Session;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Model\Group;
use Friendica\Model\Item as ModelItem;
use Friendica\Model\Tag;
use Friendica\Model\Post;
@ -53,7 +56,7 @@ class Item
$this->activity = $activity;
$this->l10n = $l10n;
}
/**
* Return array with details for categories and folders for an item
*
@ -479,7 +482,7 @@ class Item
if (empty($item['verb']) || $this->activity->isHidden($item['verb'])) {
return false;
}
// @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
if ($this->activity->match($item['verb'], Activity::FOLLOW) &&
$item['object-type'] === Activity\ObjectType::NOTE &&
@ -487,7 +490,91 @@ class Item
$item['uid'] == local_user()) {
return false;
}
return true;
}
public function expandTags(array $item, bool $setPermissions = false)
{
// Look for any tags and linkify them
$item['inform'] = '';
$private_forum = false;
$private_id = null;
$only_to_forum = false;
$forum_contact = [];
$receivers = [];
// Convert mentions in the body to a unified format
$item['body'] = BBCode::setMentions($item['body'], $item['uid'], $item['network']);
// Search for forum mentions
foreach (Tag::getFromBody($item['body'], Tag::TAG_CHARACTER[Tag::MENTION] . Tag::TAG_CHARACTER[Tag::EXCLUSIVE_MENTION]) as $tag) {
$contact = Contact::getByURLForUser($tag[2], $item['uid']);
$receivers[] = $contact['id'];
if (!empty($item['inform'])) {
$item['inform'] .= ',';
}
$item['inform'] .= 'cid:' . $contact['id'];
if (($item['gravity'] == GRAVITY_COMMENT) || empty($contact['cid']) || ($contact['contact-type'] != Contact::TYPE_COMMUNITY)) {
continue;
}
if (!empty($contact['prv']) || ($tag[1] == Tag::TAG_CHARACTER[Tag::EXCLUSIVE_MENTION])) {
$private_forum = $contact['prv'];
$only_to_forum = ($tag[1] == Tag::TAG_CHARACTER[Tag::EXCLUSIVE_MENTION]);
$private_id = $contact['id'];
$forum_contact = $contact;
Logger::info('Private forum or exclusive mention', ['url' => $tag[2], 'mention' => $tag[1]]);
} elseif ($item['allow_cid'] == '<' . $contact['id'] . '>') {
$private_forum = false;
$only_to_forum = true;
$private_id = $contact['id'];
$forum_contact = $contact;
Logger::info('Public forum', ['url' => $tag[2], 'mention' => $tag[1]]);
} else {
Logger::info('Post with forum mention will not be converted to a forum post', ['url' => $tag[2], 'mention' => $tag[1]]);
}
}
Logger::info('Got inform', ['inform' => $item['inform']]);
if (($item['gravity'] == GRAVITY_PARENT) && !empty($forum_contact) && ($private_forum || $only_to_forum)) {
// we tagged a forum in a top level post. Now we change the post
$item['private'] = $private_forum ? ModelItem::PRIVATE : ModelItem::UNLISTED;
if ($only_to_forum) {
$item['postopts'] = '';
}
$item['deny_cid'] = '';
$item['deny_gid'] = '';
if ($private_forum) {
$item['allow_cid'] = '<' . $private_id . '>';
$item['allow_gid'] = '<' . Group::getIdForForum($forum_contact['id']) . '>';
} else {
$item['allow_cid'] = '';
$item['allow_gid'] = '';
}
} elseif ($setPermissions && ($item['gravity'] == GRAVITY_PARENT)) {
if (empty($receivers)) {
// For security reasons direct posts without any receiver will be posts to yourself
$self = Contact::selectFirst(['id'], ['uid' => $item['uid'], 'self' => true]);
$receivers[] = $self['id'];
}
$item['private'] = ModelItem::PRIVATE;
$item['allow_cid'] = '';
$item['allow_gid'] = '';
$item['deny_cid'] = '';
$item['deny_gid'] = '';
foreach ($receivers as $receiver) {
$item['allow_cid'] .= '<' . $receiver . '>';
}
}
return $item;
}
}

View file

@ -2086,8 +2086,8 @@ class BBCode
public static function stripAbstract($text)
{
DI::profiler()->startRecording('rendering');
$text = preg_replace("/[\s|\n]*\[abstract\].*?\[\/abstract\][\s|\n]*/ism", '', $text);
$text = preg_replace("/[\s|\n]*\[abstract=.*?\].*?\[\/abstract][\s|\n]*/ism", '', $text);
$text = preg_replace("/[\s|\n]*\[abstract\].*?\[\/abstract\][\s|\n]*/ism", ' ', $text);
$text = preg_replace("/[\s|\n]*\[abstract=.*?\].*?\[\/abstract][\s|\n]*/ism", ' ', $text);
DI::profiler()->stopRecording();
return $text;

View file

@ -318,23 +318,20 @@ class Widget
/**
* Return categories widget
*
* @param string $baseurl baseurl
* @param string $selected optional, default empty
* @param int $uid Id of the user owning the categories
* @param string $baseurl Base page URL
* @param string $selected Selected category
* @return string|void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function categories($baseurl, $selected = '')
public static function categories(int $uid, string $baseurl, string $selected = '')
{
$a = DI::app();
$uid = intval($a->getProfileOwner());
if (!Feature::isEnabled($uid, 'categories')) {
return '';
}
$terms = array();
foreach (Post\Category::getArray(local_user(), Post\Category::CATEGORY) as $savedFolderName) {
foreach (Post\Category::getArray($uid, Post\Category::CATEGORY) as $savedFolderName) {
$terms[] = ['ref' => $savedFolderName, 'name' => $savedFolderName];
}

View file

@ -99,7 +99,7 @@ class VCard
'$network_link' => $network_link,
'$network_avatar' => $network_avatar,
'$network' => DI::l10n()->t('Network:'),
'$account_type' => Contact::getAccountType($contact),
'$account_type' => Contact::getAccountType($contact['contact-type']),
'$follow' => DI::l10n()->t('Follow'),
'$follow_link' => $follow_link,
'$unfollow' => DI::l10n()->t('Unfollow'),

View file

@ -51,7 +51,7 @@ class ACL
* @return string
* @throws \Exception
*/
public static function getMessageContactSelectHTML(int $selected = null)
public static function getMessageContactSelectHTML(int $selected = null): string
{
$o = '';
@ -62,25 +62,7 @@ class ACL
$page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.css'));
$page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput-typeahead.css'));
$condition = [
'uid' => local_user(),
'self' => false,
'blocked' => false,
'pending' => false,
'archive' => false,
'deleted' => false,
'rel' => [Contact::FOLLOWER, Contact::SHARING, Contact::FRIEND],
'network' => Protocol::SUPPORT_PRIVATE,
];
$contacts = Contact::selectToArray(
['id', 'name', 'addr', 'micro'],
DBA::mergeConditions($condition, ["`notify` != ''"])
);
$arr = ['contact' => $contacts, 'entry' => $o];
Hook::callAll(DI::args()->getModuleName() . '_pre_recipient', $arr);
$contacts = self::getValidMessageRecipientsForUser(local_user());
$tpl = Renderer::getMarkupTemplate('acl/message_recipient.tpl');
$o = Renderer::replaceMacros($tpl, [
@ -93,6 +75,25 @@ class ACL
return $o;
}
public static function getValidMessageRecipientsForUser(int $uid): array
{
$condition = [
'uid' => $uid,
'self' => false,
'blocked' => false,
'pending' => false,
'archive' => false,
'deleted' => false,
'rel' => [Contact::FOLLOWER, Contact::SHARING, Contact::FRIEND],
'network' => Protocol::SUPPORT_PRIVATE,
];
return Contact::selectToArray(
['id', 'name', 'addr', 'micro', 'url', 'nick'],
DBA::mergeConditions($condition, ["`notify` != ''"])
);
}
/**
* Returns a minimal ACL block for self-only permissions
*

View file

@ -59,13 +59,13 @@ class RedisCache extends AbstractCache implements ICanCacheInMemory
$redis_pw = $config->get('system', 'redis_password');
$redis_db = $config->get('system', 'redis_db', 0);
if (isset($redis_port) && !@$this->redis->connect($redis_host, $redis_port)) {
if (!empty($redis_port) && !@$this->redis->connect($redis_host, $redis_port)) {
throw new CachePersistenceException('Expected Redis server at ' . $redis_host . ':' . $redis_port . ' isn\'t available');
} elseif (!@$this->redis->connect($redis_host)) {
throw new CachePersistenceException('Expected Redis server at ' . $redis_host . ' isn\'t available');
}
if (isset($redis_pw) && !$this->redis->auth($redis_pw)) {
if (!empty($redis_pw) && !$this->redis->auth($redis_pw)) {
throw new CachePersistenceException('Cannot authenticate redis server at ' . $redis_host . ':' . $redis_port);
}

View file

@ -22,7 +22,6 @@
namespace Friendica\Core;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\User;
use Friendica\Network\HTTPException;
use Friendica\Protocol\Activity;
@ -171,15 +170,15 @@ class Protocol
}
/**
* Sends an unfriend message. Does not remove the contact
* Sends an unfollow message. Does not remove the contact
*
* @param array $user User unfriending
* @param array $contact Contact unfriended
* @param array $contact Target public contact (uid = 0) array
* @param array $user Source local user array
* @return bool|null true if successful, false if not, null if no remote action was performed
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function terminateFriendship(array $user, array $contact): ?bool
public static function unfollow(array $contact, array $user): ?bool
{
if (empty($contact['network'])) {
throw new \InvalidArgumentException('Missing network key in contact array');
@ -216,7 +215,8 @@ class Protocol
// Catch-all hook for connector addons
$hook_data = [
'contact' => $contact,
'result' => null
'uid' => $user['uid'],
'result' => null,
];
Hook::callAll('unfollow', $hook_data);
@ -226,12 +226,13 @@ class Protocol
/**
* Revoke an incoming follow from the provided contact
*
* @param array $contact Private contact (uid != 0) array
* @param array $contact Target public contact (uid == 0) array
* @param int $uid Source local user id
* @return bool|null true if successful, false if not, null if no action was performed
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function revokeFollow(array $contact): ?bool
public static function revokeFollow(array $contact, int $uid): ?bool
{
if (empty($contact['network'])) {
throw new \InvalidArgumentException('Missing network key in contact array');
@ -243,13 +244,14 @@ class Protocol
}
if ($protocol == Protocol::ACTIVITYPUB) {
return ActivityPub\Transmitter::sendContactReject($contact['url'], $contact['hub-verify'], $contact['uid']);
return ActivityPub\Transmitter::sendContactReject($contact['url'], $contact['hub-verify'], $uid);
}
// Catch-all hook for connector addons
$hook_data = [
'contact' => $contact,
'result' => null,
'uid' => $uid,
'result' => null,
];
Hook::callAll('revoke_follow', $hook_data);

View file

@ -334,9 +334,10 @@ class System
* and adds an application/json HTTP header to the output.
* After finishing the process is getting killed.
*
* @param mixed $x The input content.
* @param string $content_type Type of the input (Default: 'application/json').
* @param integer $options JSON options
* @param mixed $x The input content
* @param string $content_type Type of the input (Default: 'application/json')
* @param integer $options JSON options
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function jsonExit($x, $content_type = 'application/json', int $options = 0) {
DI::apiResponse()->setType(Response::TYPE_JSON, $content_type);

View file

@ -1378,8 +1378,9 @@ class Worker
* Defers the current worker entry
*
* @return boolean had the entry been deferred?
* @throws \Exception
*/
public static function defer()
public static function defer(): bool
{
$queue = DI::app()->getQueue();
@ -1387,7 +1388,6 @@ class Worker
return false;
}
$retrial = $queue['retrial'];
$id = $queue['id'];
$priority = $queue['priority'];

View file

@ -487,6 +487,11 @@ abstract class DI
return self::$dice->create(Contact\Introduction\Factory\Introduction::class);
}
public static function localRelationship(): Contact\LocalRelationship\Repository\LocalRelationship
{
return self::$dice->create(Contact\LocalRelationship\Repository\LocalRelationship::class);
}
public static function permissionSet(): Security\PermissionSet\Repository\PermissionSet
{
return self::$dice->create(Security\PermissionSet\Repository\PermissionSet::class);
@ -527,9 +532,14 @@ abstract class DI
return self::$dice->create(Navigation\Notifications\Factory\Notify::class);
}
public static function formattedNotificationFactory(): Navigation\Notifications\Factory\FormattedNotification
public static function formattedNotificationFactory(): Navigation\Notifications\Factory\FormattedNotify
{
return self::$dice->create(Navigation\Notifications\Factory\FormattedNotification::class);
return self::$dice->create(Navigation\Notifications\Factory\FormattedNotify::class);
}
public static function formattedNavNotificationFactory(): Navigation\Notifications\Factory\FormattedNavNotification
{
return self::$dice->create(Navigation\Notifications\Factory\FormattedNavNotification::class);
}
//

View file

@ -114,6 +114,7 @@ class Database
$pass = trim($this->configCache->get('database', 'password'));
$db = trim($this->configCache->get('database', 'database'));
$charset = trim($this->configCache->get('database', 'charset'));
$socket = trim($this->configCache->get('database', 'socket'));
if (!(strlen($server) && strlen($user))) {
return false;
@ -135,9 +136,14 @@ class Database
$connect .= ";charset=" . $charset;
}
if ($socket) {
$connect .= ";$unix_socket=" . $socket;
}
try {
$this->connection = @new PDO($connect, $user, $pass, [PDO::ATTR_PERSISTENT => $persistent]);
$this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->pdo_emulate_prepares);
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$this->connected = true;
} catch (PDOException $e) {
$this->connected = false;
@ -159,6 +165,11 @@ class Database
if ($charset) {
$this->connection->set_charset($charset);
}
if ($socket) {
$this->connection->set_socket($socket);
}
}
}

View file

@ -25,6 +25,7 @@ use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Conversation;
use Friendica\Model\GServer;
use Friendica\Model\Item;
use Friendica\Model\ItemURI;
@ -33,6 +34,9 @@ use Friendica\Model\Post;
use Friendica\Model\Post\Category;
use Friendica\Model\Tag;
use Friendica\Model\Verb;
use Friendica\Protocol\ActivityPub\Processor;
use Friendica\Protocol\ActivityPub\Receiver;
use Friendica\Util\JsonLD;
use Friendica\Util\Strings;
/**
@ -46,7 +50,7 @@ class PostUpdate
// Needed for the helper function to read from the legacy term table
const OBJECT_TYPE_POST = 1;
const VERSION = 1427;
const VERSION = 1452;
/**
* Calls the post update functions
@ -104,6 +108,9 @@ class PostUpdate
if (!self::update1427()) {
return false;
}
if (!self::update1452()) {
return false;
}
return true;
}
@ -1012,4 +1019,70 @@ class PostUpdate
return false;
}
/**
* Fill the receivers of the post via the raw source
*
* @return bool "true" when the job is done
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function update1452()
{
// Was the script completed?
if (DI::config()->get('system', 'post_update_version') >= 1452) {
return true;
}
$id = DI::config()->get('system', 'post_update_version_1452_id', 0);
Logger::info('Start', ['uri-id' => $id]);
$rows = 0;
$received = '';
$conversations = DBA::p("SELECT `post-view`.`uri-id`, `conversation`.`source`, `conversation`.`received` FROM `conversation`
INNER JOIN `post-view` ON `post-view`.`uri` = `conversation`.`item-uri`
WHERE NOT `source` IS NULL AND `conversation`.`protocol` = ? AND `uri-id` > ? LIMIT ?",
Conversation::PARCEL_ACTIVITYPUB, $id, 1000);
if (DBA::errorNo() != 0) {
Logger::error('Database error', ['no' => DBA::errorNo(), 'message' => DBA::errorMessage()]);
return false;
}
while ($conversation = DBA::fetch($conversations)) {
$id = $conversation['uri-id'];
$received = $conversation['received'];
$raw = json_decode($conversation['source'], true);
if (empty($raw)) {
continue;
}
$activity = JsonLD::compact($raw);
$urls = Receiver::getReceiverURL($activity);
Processor::storeReceivers($conversation['uri-id'], $urls);
if (!empty($activity['as:object'])) {
$urls = array_merge($urls, Receiver::getReceiverURL($activity['as:object']));
Processor::storeReceivers($conversation['uri-id'], $urls);
}
++$rows;
}
DBA::close($conversations);
DI::config()->set('system', 'post_update_version_1452_id', $id);
Logger::info('Processed', ['rows' => $rows, 'last' => $id, 'last-received' => $received]);
if ($rows <= 100) {
DI::config()->set('system', 'post_update_version', 1452);
Logger::info('Done');
return true;
}
return false;
}
}

View file

@ -27,6 +27,7 @@ use Friendica\Content\Text\BBCode;
use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\Model\Post;
use Friendica\Model\Tag as TagModel;
use Friendica\Model\Verb;
use Friendica\Network\HTTPException;
use Friendica\Protocol\Activity;
@ -76,8 +77,8 @@ class Status extends BaseFactory
*/
public function createFromUriId(int $uriId, $uid = 0): \Friendica\Object\Api\Mastodon\Status
{
$fields = ['uri-id', 'uid', 'author-id', 'author-link', 'starred', 'app', 'title', 'body', 'raw-body', 'created', 'network',
'thr-parent-id', 'parent-author-id', 'language', 'uri', 'plink', 'private', 'vid', 'gravity'];
$fields = ['uri-id', 'uid', 'author-id', 'author-link', 'starred', 'app', 'title', 'body', 'raw-body', 'content-warning',
'created', 'network', 'thr-parent-id', 'parent-author-id', 'language', 'uri', 'plink', 'private', 'vid', 'gravity'];
$item = Post::selectFirst($fields, ['uri-id' => $uriId, 'uid' => [0, $uid]], ['order' => ['uid' => true]]);
if (!$item) {
$mail = DBA::selectFirst('mail', ['id'], ['uri-id' => $uriId, 'uid' => $uid]);
@ -127,7 +128,7 @@ class Status extends BaseFactory
Post\ThreadUser::getPinned($uriId, $uid)
);
$sensitive = $this->dba->exists('tag-view', ['uri-id' => $uriId, 'name' => 'nsfw']);
$sensitive = $this->dba->exists('tag-view', ['uri-id' => $uriId, 'name' => 'nsfw', 'type' => TagModel::HASHTAG]);
$application = new \Friendica\Object\Api\Mastodon\Application($item['app'] ?: ContactSelector::networkToName($item['network'], $item['author-link']));
$mentions = $this->mstdnMentionFactory->createFromUriId($uriId)->getArrayCopy();

View file

@ -27,6 +27,7 @@ use Friendica\Content\Text\HTML;
use Friendica\Database\Database;
use Friendica\Factory\Api\Friendica\Activities;
use Friendica\Factory\Api\Twitter\User as TwitterUser;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Model\Verb;
use Friendica\Network\HTTPException;
@ -70,14 +71,15 @@ class Status extends BaseFactory
* @param int $uriId Uri-ID of the item
* @param int $uid Item user
*
* @return \Friendica\Object\Api\Mastodon\Status
* @return \Friendica\Object\Api\Twitter\Status
* @throws HTTPException\InternalServerErrorException
* @throws ImagickException|HTTPException\NotFoundException
*/
public function createFromItemId(int $id, int $uid, bool $include_entities = false): \Friendica\Object\Api\Twitter\Status
{
$fields = ['id', 'parent', 'uri-id', 'uid', 'author-id', 'author-link', 'author-network', 'owner-id', 'starred', 'app', 'title', 'body', 'raw-body', 'created', 'network',
'thr-parent-id', 'parent-author-id', 'parent-author-nick', 'language', 'uri', 'plink', 'private', 'vid', 'gravity', 'coord'];
$fields = ['parent-uri-id', 'uri-id', 'uid', 'author-id', 'author-link', 'author-network', 'owner-id', 'causer-id',
'starred', 'app', 'title', 'body', 'raw-body', 'created', 'network','post-reason', 'language', 'gravity',
'thr-parent-id', 'parent-author-id', 'parent-author-nick', 'uri', 'plink', 'private', 'vid', 'coord'];
$item = Post::selectFirst($fields, ['id' => $id], ['order' => ['uid' => true]]);
if (!$item) {
throw new HTTPException\NotFoundException('Item with ID ' . $id . ' not found.');
@ -89,14 +91,15 @@ class Status extends BaseFactory
* @param int $uriId Uri-ID of the item
* @param int $uid Item user
*
* @return \Friendica\Object\Api\Mastodon\Status
* @return \Friendica\Object\Api\Twitter\Status
* @throws HTTPException\InternalServerErrorException
* @throws ImagickException|HTTPException\NotFoundException
*/
public function createFromUriId(int $uriId, $uid = 0, $include_entities = false): \Friendica\Object\Api\Twitter\Status
{
$fields = ['id', 'parent', 'uri-id', 'uid', 'author-id', 'author-link', 'author-network', 'owner-id', 'starred', 'app', 'title', 'body', 'raw-body', 'created', 'network',
'thr-parent-id', 'parent-author-id', 'parent-author-nick', 'language', 'uri', 'plink', 'private', 'vid', 'gravity', 'coord'];
$fields = ['parent-uri-id', 'uri-id', 'uid', 'author-id', 'author-link', 'author-network', 'owner-id', 'causer-id',
'starred', 'app', 'title', 'body', 'raw-body', 'created', 'network','post-reason', 'language', 'gravity',
'thr-parent-id', 'parent-author-id', 'parent-author-nick', 'uri', 'plink', 'private', 'vid', 'coord'];
$item = Post::selectFirst($fields, ['uri-id' => $uriId, 'uid' => [0, $uid]], ['order' => ['uid' => true]]);
if (!$item) {
throw new HTTPException\NotFoundException('Item with URI ID ' . $uriId . ' not found' . ($uid ? ' for user ' . $uid : '.'));
@ -108,14 +111,19 @@ class Status extends BaseFactory
* @param array $item item array
* @param int $uid Item user
*
* @return \Friendica\Object\Api\Mastodon\Status
* @return \Friendica\Object\Api\Twitter\Status
* @throws HTTPException\InternalServerErrorException
* @throws ImagickException|HTTPException\NotFoundException
*/
private function createFromArray(array $item, int $uid, bool $include_entities): \Friendica\Object\Api\Twitter\Status
{
$author = $this->twitterUser->createFromContactId($item['author-id'], $uid, true);
$owner = $this->twitterUser->createFromContactId($item['owner-id'], $uid, true);
if (!empty($item['causer-id']) && ($item['post-reason'] == Item::PR_ANNOUNCEMENT)) {
$owner = $this->twitterUser->createFromContactId($item['causer-id'], $uid, true);
} else {
$owner = $this->twitterUser->createFromContactId($item['owner-id'], $uid, true);
}
$friendica_comments = Post::countPosts(['thr-parent-id' => $item['uri-id'], 'deleted' => false, 'gravity' => GRAVITY_COMMENT]);

View file

@ -168,7 +168,7 @@ class APContact
// Detect multiple fast repeating request to the same address
// See https://github.com/friendica/friendica/issues/9303
$cachekey = 'apcontact:getByURL:' . $url;
$cachekey = 'apcontact:' . ItemURI::getIdByURI($url);
$result = DI::cache()->get($cachekey);
if (!is_null($result)) {
Logger::notice('Multiple requests for the address', ['url' => $url, 'update' => $update, 'callstack' => System::callstack(20), 'result' => $result]);

View file

@ -685,7 +685,7 @@ class Contact
*/
public static function updateSelfFromUserID($uid, $update_avatar = false)
{
$fields = ['id', 'name', 'nick', 'location', 'about', 'keywords', 'avatar', 'prvkey', 'pubkey',
$fields = ['id', 'name', 'nick', 'location', 'about', 'keywords', 'avatar', 'prvkey', 'pubkey', 'manually-approve',
'xmpp', 'matrix', 'contact-type', 'forum', 'prv', 'avatar-date', 'url', 'nurl', 'unsearchable',
'photo', 'thumb', 'micro', 'header', 'addr', 'request', 'notify', 'poll', 'confirm', 'poco', 'network'];
$self = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]);
@ -757,6 +757,7 @@ class Contact
$fields['forum'] = $user['page-flags'] == User::PAGE_FLAGS_COMMUNITY;
$fields['prv'] = $user['page-flags'] == User::PAGE_FLAGS_PRVGROUP;
$fields['unsearchable'] = !$profile['net-publish'];
$fields['manually-approve'] = in_array($user['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP]);
$update = false;
@ -812,37 +813,13 @@ class Contact
}
/**
* Sends an unfriend message. Removes the contact for two-way unfriending or sharing only protocols (feed an mail)
* Unfollow the remote contact
*
* @param array $user User unfriending
* @param array $contact Contact (uid != 0) unfriended
* @param boolean $two_way Revoke eventual inbound follow as well
* @return bool|null true if successful, false if not, null if no remote action was performed
* @param array $contact Target user-specific contact (uid != 0) array
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function terminateFriendship(array $user, array $contact): ?bool
{
$result = Protocol::terminateFriendship($user, $contact);
if ($contact['rel'] == Contact::SHARING || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) {
self::remove($contact['id']);
} else {
self::update(['rel' => Contact::FOLLOWER], ['id' => $contact['id']]);
}
return $result;
}
/**
* Revoke follow privileges of the remote user contact
*
* @param array $contact Contact unfriended
* @return bool|null Whether the remote operation is successful or null if no remote operation was performed
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function revokeFollow(array $contact): ?bool
public static function unfollow(array $contact): void
{
if (empty($contact['network'])) {
throw new \InvalidArgumentException('Empty network in contact array');
@ -852,19 +829,69 @@ class Contact
throw new \InvalidArgumentException('Unexpected public contact record');
}
$result = Protocol::revokeFollow($contact);
// A null value here means the remote network doesn't support explicit follow revocation, we can still
// break the locally recorded relationship
if ($result !== false) {
if ($contact['rel'] == self::FRIEND) {
self::update(['rel' => self::SHARING], ['id' => $contact['id']]);
} else {
self::remove($contact['id']);
}
if (in_array($contact['rel'], [self::SHARING, self::FRIEND])) {
$cdata = Contact::getPublicAndUserContactID($contact['id'], $contact['uid']);
Worker::add(PRIORITY_HIGH, 'Contact\Unfollow', $cdata['public'], $contact['uid']);
}
return $result;
self::removeSharer($contact);
}
/**
* Revoke follow privileges of the remote user contact
*
* The local relationship is updated immediately, the eventual remote server is messaged in the background.
*
* @param array $contact User-specific contact array (uid != 0) to revoke the follow from
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function revokeFollow(array $contact): void
{
if (empty($contact['network'])) {
throw new \InvalidArgumentException('Empty network in contact array');
}
if (empty($contact['uid'])) {
throw new \InvalidArgumentException('Unexpected public contact record');
}
if (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND])) {
$cdata = Contact::getPublicAndUserContactID($contact['id'], $contact['uid']);
Worker::add(PRIORITY_HIGH, 'Contact\RevokeFollow', $cdata['public'], $contact['uid']);
}
self::removeFollower($contact);
}
/**
* Completely severs a relationship with a contact
*
* @param array $contact User-specific contact (uid != 0) array
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function terminateFriendship(array $contact)
{
if (empty($contact['network'])) {
throw new \InvalidArgumentException('Empty network in contact array');
}
if (empty($contact['uid'])) {
throw new \InvalidArgumentException('Unexpected public contact record');
}
$cdata = Contact::getPublicAndUserContactID($contact['id'], $contact['uid']);
if (in_array($contact['rel'], [self::SHARING, self::FRIEND])) {
Worker::add(PRIORITY_HIGH, 'Contact\Unfollow', $cdata['public'], $contact['uid']);
}
if (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND])) {
Worker::add(PRIORITY_HIGH, 'Contact\RevokeFollow', $cdata['public'], $contact['uid']);
}
self::remove($contact['id']);
}
@ -1457,34 +1484,11 @@ class Contact
*
* The function can be called with either the user or the contact array
*
* @param array $contact contact or user array
* @param int $type type of contact or account
* @return string
*/
public static function getAccountType(array $contact)
public static function getAccountType(int $type)
{
// There are several fields that indicate that the contact or user is a forum
// "page-flags" is a field in the user table,
// "forum" and "prv" are used in the contact table. They stand for User::PAGE_FLAGS_COMMUNITY and User::PAGE_FLAGS_PRVGROUP.
if ((isset($contact['page-flags']) && (intval($contact['page-flags']) == User::PAGE_FLAGS_COMMUNITY))
|| (isset($contact['page-flags']) && (intval($contact['page-flags']) == User::PAGE_FLAGS_PRVGROUP))
|| (isset($contact['forum']) && intval($contact['forum']))
|| (isset($contact['prv']) && intval($contact['prv']))
|| (isset($contact['community']) && intval($contact['community']))
) {
$type = self::TYPE_COMMUNITY;
} else {
$type = self::TYPE_PERSON;
}
// The "contact-type" (contact table) and "account-type" (user table) are more general then the chaos from above.
if (isset($contact["contact-type"])) {
$type = $contact["contact-type"];
}
if (isset($contact["account-type"])) {
$type = $contact["account-type"];
}
switch ($type) {
case self::TYPE_ORGANISATION:
$account_type = DI::l10n()->t("Organisation");
@ -2596,28 +2600,6 @@ class Contact
return $result;
}
/**
* Unfollow a contact
*
* @param int $cid Public contact id
* @param int $uid User ID
*
* @return bool "true" if unfollowing had been successful
*/
public static function unfollow(int $cid, int $uid)
{
$cdata = self::getPublicAndUserContactID($cid, $uid);
if (empty($cdata['user'])) {
return false;
}
$contact = self::getById($cdata['user']);
self::removeSharer([], $contact);
return true;
}
/**
* @param array $importer Owner (local user) data
* @param array $contact Existing owner-specific contact data we want to expand the relationship with. Optional.
@ -2635,7 +2617,7 @@ class Contact
return false;
}
$fields = ['url', 'name', 'nick', 'avatar', 'photo', 'network', 'blocked'];
$fields = ['id', 'url', 'name', 'nick', 'avatar', 'photo', 'network', 'blocked'];
$pub_contact = DBA::selectFirst('contact', $fields, ['id' => $datarray['author-id']]);
if (!DBA::isResult($pub_contact)) {
// Should never happen
@ -2683,7 +2665,7 @@ class Contact
// Ensure to always have the correct network type, independent from the connection request method
self::updateFromProbe($contact['id']);
Post\UserNotification::insertNotification($contact['id'], Activity::FOLLOW, $importer['uid']);
Post\UserNotification::insertNotification($pub_contact['id'], Activity::FOLLOW, $importer['uid']);
return true;
} else {
@ -2714,7 +2696,7 @@ class Contact
self::updateAvatar($contact_id, $photo, true);
Post\UserNotification::insertNotification($contact_id, Activity::FOLLOW, $importer['uid']);
Post\UserNotification::insertNotification($pub_contact['id'], Activity::FOLLOW, $importer['uid']);
$contact_record = DBA::selectFirst('contact', ['id', 'network', 'name', 'url', 'photo'], ['id' => $contact_id]);
@ -2734,9 +2716,7 @@ class Contact
Group::addMember(User::getDefaultGroup($importer['uid']), $contact_record['id']);
if (($user['notify-flags'] & Notification\Type::INTRO) &&
in_array($user['page-flags'], [User::PAGE_FLAGS_NORMAL])) {
if (($user['notify-flags'] & Notification\Type::INTRO) && $user['page-flags'] == User::PAGE_FLAGS_NORMAL) {
DI::notify()->createFromArray([
'type' => Notification\Type::INTRO,
'otype' => Notification\ObjectType::INTRO,
@ -2766,23 +2746,41 @@ class Contact
return null;
}
/**
* Update the local relationship when a local user loses a follower
*
* @param array $contact User-specific contact (uid != 0) array
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function removeFollower(array $contact)
{
if (in_array($contact['rel'] ?? [], [self::FRIEND, self::SHARING])) {
DBA::update('contact', ['rel' => self::SHARING], ['id' => $contact['id']]);
self::update(['rel' => self::SHARING], ['id' => $contact['id']]);
} elseif (!empty($contact['id'])) {
self::remove($contact['id']);
} else {
DI::logger()->info('Couldn\'t remove follower because of invalid contact array', ['contact' => $contact, 'callstack' => System::callstack()]);
}
$cdata = Contact::getPublicAndUserContactID($contact['id'], $contact['uid']);
DI::notification()->deleteForUserByVerb($contact['uid'], Activity::FOLLOW, ['actor-id' => $cdata['public']]);
}
public static function removeSharer($importer, $contact)
/**
* Update the local relationship when a local user unfollow a contact.
* Removes the contact for sharing-only protocols (feed and mail).
*
* @param array $contact User-specific contact (uid != 0) array
* @throws HTTPException\InternalServerErrorException
*/
public static function removeSharer(array $contact)
{
if (($contact['rel'] == self::FRIEND) || ($contact['rel'] == self::FOLLOWER)) {
self::update(['rel' => self::FOLLOWER], ['id' => $contact['id']]);
} else {
if ($contact['rel'] == self::SHARING || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) {
self::remove($contact['id']);
} else {
self::update(['rel' => self::FOLLOWER], ['id' => $contact['id']]);
}
}
@ -2947,7 +2945,7 @@ class Contact
*/
public static function isForum($contactid)
{
$fields = ['contact-type', 'forum', 'prv'];
$fields = ['contact-type'];
$condition = ['id' => $contactid];
$contact = DBA::selectFirst('contact', $fields, $condition);
if (!DBA::isResult($contact)) {
@ -2955,7 +2953,7 @@ class Contact
}
// Is it a forum?
return (($contact['contact-type'] == self::TYPE_COMMUNITY) || $contact['forum'] || $contact['prv']);
return ($contact['contact-type'] == self::TYPE_COMMUNITY);
}
/**

View file

@ -546,6 +546,22 @@ class GServer
Logger::info('Update registered users', ['id' => $id, 'url' => $serverdata['nurl'], 'registered-users' => $max_users]);
DBA::update('gserver', ['registered-users' => $max_users], ['id' => $id]);
}
if (empty($serverdata['active-month-users'])) {
$contacts = DBA::count('contact', ["`uid` = ? AND `gsid` = ? AND NOT `failed` AND `last-item` > ?", 0, $id, DateTimeFormat::utc('now - 30 days')]);
if ($contacts > 0) {
Logger::info('Update monthly users', ['id' => $id, 'url' => $serverdata['nurl'], 'monthly-users' => $contacts]);
DBA::update('gserver', ['active-month-users' => $contacts], ['id' => $id]);
}
}
if (empty($serverdata['active-halfyear-users'])) {
$contacts = DBA::count('contact', ["`uid` = ? AND `gsid` = ? AND NOT `failed` AND `last-item` > ?", 0, $id, DateTimeFormat::utc('now - 180 days')]);
if ($contacts > 0) {
Logger::info('Update halfyear users', ['id' => $id, 'url' => $serverdata['nurl'], 'halfyear-users' => $contacts]);
DBA::update('gserver', ['active-halfyear-users' => $contacts], ['id' => $id]);
}
}
}
if (!empty($serverdata['network']) && in_array($serverdata['network'], [Protocol::DFRN, Protocol::DIASPORA])) {

View file

@ -29,6 +29,7 @@ use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Network\HTTPException;
use Friendica\Protocol\ActivityPub;
/**
* functions for interacting with the group database table
@ -40,7 +41,7 @@ class Group
public static function getByUserId($uid, $includesDeleted = false)
{
$conditions = ['uid' => $uid];
$conditions = ['uid' => $uid, 'cid' => null];
if (!$includesDeleted) {
$conditions['deleted'] = false;
@ -309,6 +310,68 @@ class Group
return DBA::delete('group_member', ['gid' => $gid, 'contact-id' => $cid]);
}
/**
* Adds contacts to a group
*
* @param int $gid
* @param array $contacts
* @throws \Exception
*/
public static function addMembers(int $gid, array $contacts)
{
if (!$gid || !$contacts) {
return false;
}
// @TODO Backward compatibility with user contacts, remove by version 2022.03
$group = DBA::selectFirst('group', ['uid'], ['id' => $gid]);
if (empty($group)) {
throw new HTTPException\NotFoundException('Group not found.');
}
foreach ($contacts as $cid) {
$cdata = Contact::getPublicAndUserContactID($cid, $group['uid']);
if (empty($cdata['user'])) {
throw new HTTPException\NotFoundException('Invalid contact.');
}
DBA::insert('group_member', ['gid' => $gid, 'contact-id' => $cdata['user']], Database::INSERT_IGNORE);
}
}
/**
* Removes contacts from a group
*
* @param int $gid
* @param array $contacts
* @throws \Exception
*/
public static function removeMembers(int $gid, array $contacts)
{
if (!$gid || !$contacts) {
return false;
}
// @TODO Backward compatibility with user contacts, remove by version 2022.03
$group = DBA::selectFirst('group', ['uid'], ['id' => $gid]);
if (empty($group)) {
throw new HTTPException\NotFoundException('Group not found.');
}
$contactIds = [];
foreach ($contacts as $cid) {
$cdata = Contact::getPublicAndUserContactID($cid, $group['uid']);
if (empty($cdata['user'])) {
throw new HTTPException\NotFoundException('Invalid contact.');
}
$contactIds[] = $cdata['user'];
}
DBA::delete('group_member', ['gid' => $gid, 'contact-id' => $contactIds]);
}
/**
* Returns the combined list of contact ids from a group id list
*
@ -407,7 +470,7 @@ class Group
]
];
$stmt = DBA::select('group', [], ['deleted' => 0, 'uid' => $uid], ['order' => ['name']]);
$stmt = DBA::select('group', [], ['deleted' => false, 'uid' => $uid, 'cid' => null], ['order' => ['name']]);
while ($group = DBA::fetch($stmt)) {
$display_groups[] = [
'name' => $group['name'],
@ -464,7 +527,7 @@ class Group
$member_of = self::getIdsByContactId($cid);
}
$stmt = DBA::select('group', [], ['deleted' => 0, 'uid' => local_user()], ['order' => ['name']]);
$stmt = DBA::select('group', [], ['deleted' => false, 'uid' => local_user(), 'cid' => null], ['order' => ['name']]);
while ($group = DBA::fetch($stmt)) {
$selected = (($group_id == $group['id']) ? ' group-selected' : '');
@ -519,4 +582,79 @@ class Group
return $o;
}
/**
* Fetch the group id for the given contact id
*
* @param integer $id Contact ID
* @return integer Group IO
*/
public static function getIdForForum(int $id)
{
Logger::info('Get id for forum id', ['id' => $id]);
$contact = Contact::getById($id, ['uid', 'name', 'contact-type', 'manually-approve']);
if (empty($contact) || ($contact['contact-type'] != Contact::TYPE_COMMUNITY) || !$contact['manually-approve']) {
return 0;
}
$group = DBA::selectFirst('group', ['id'], ['uid' => $contact['uid'], 'cid' => $id]);
if (empty($group)) {
$fields = [
'uid' => $contact['uid'],
'name' => $contact['name'],
'cid' => $id,
];
DBA::insert('group', $fields);
$gid = DBA::lastInsertId();
} else {
$gid = $group['id'];
}
return $gid;
}
/**
* Fetch the followers of a given contact id and store them as group members
*
* @param integer $id Contact ID
*/
public static function updateMembersForForum(int $id)
{
Logger::info('Update forum members', ['id' => $id]);
$contact = Contact::getById($id, ['uid', 'url']);
if (empty($contact)) {
return;
}
$apcontact = APContact::getByURL($contact['url']);
if (empty($apcontact['followers'])) {
return;
}
$gid = self::getIdForForum($id);
if (empty($gid)) {
return;
}
$group_members = DBA::selectToArray('group_member', ['contact-id'], ['gid' => $gid]);
if (!empty($group_members)) {
$current = array_unique(array_column($group_members, 'contact-id'));
} else {
$current = [];
}
foreach (ActivityPub::fetchItems($apcontact['followers'], $contact['uid']) as $follower) {
$id = Contact::getIdForURL($follower);
if (!in_array($id, $current)) {
DBA::insert('group_member', ['gid' => $gid, 'contact-id' => $id]);
} else {
$key = array_search($id, $current);
unset($current[$key]);
}
}
DBA::delete('group_member', ['gid' => $gid, 'contact-id' => $current]);
Logger::info('Updated forum members', ['id' => $id, 'count' => DBA::count('group_member', ['gid' => $gid])]);
}
}

View file

@ -74,6 +74,11 @@ class Item
const PR_RELAY = 74;
const PR_FETCHED = 75;
// system.accept_only_sharer setting values
const COMPLETION_NONE = 1;
const COMPLETION_COMMENT = 0;
const COMPLETION_LIKE = 2;
// Field list that is used to display the items
const DISPLAY_FIELDLIST = [
'uid', 'id', 'parent', 'guid', 'network', 'gravity',
@ -100,7 +105,7 @@ class Item
'inform', 'deleted', 'extid', 'post-type', 'post-reason', 'gravity',
'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
'author-id', 'author-link', 'author-name', 'author-avatar', 'owner-id', 'owner-link', 'contact-uid',
'signed_text', 'network', 'wall', 'contact-id', 'plink', 'forum_mode', 'origin',
'signed_text', 'network', 'wall', 'contact-id', 'plink', 'origin',
'thr-parent-id', 'parent-uri-id', 'postopts', 'pubmail',
'event-created', 'event-edited', 'event-start', 'event-finish',
'event-summary', 'event-desc', 'event-location', 'event-type',
@ -114,7 +119,7 @@ class Item
'postopts', 'plink', 'resource-id', 'event-id', 'inform',
'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'post-type', 'post-reason',
'private', 'pubmail', 'visible', 'starred',
'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global', 'network',
'unseen', 'deleted', 'origin', 'mention', 'global', 'network',
'title', 'content-warning', 'body', 'location', 'coord', 'app',
'rendered-hash', 'rendered-html', 'object-type', 'object', 'target-type', 'target',
'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network',
@ -655,7 +660,7 @@ class Item
$fields = ['uid', 'uri', 'parent-uri', 'id', 'deleted',
'uri-id', 'parent-uri-id',
'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
'wall', 'private', 'forum_mode', 'origin', 'author-id'];
'wall', 'private', 'origin', 'author-id'];
$condition = ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']];
$params = ['order' => ['id' => false]];
$parent = Post::selectFirst($fields, $condition, $params);
@ -818,6 +823,15 @@ class Item
$item['inform'] = trim($item['inform'] ?? '');
$item['file'] = trim($item['file'] ?? '');
// Communities aren't working with the Diaspora protoccol
if (($uid != 0) && ($item['network'] == Protocol::DIASPORA)) {
$user = User::getById($uid, ['account-type']);
if ($user['account-type'] == Contact::TYPE_COMMUNITY) {
Logger::info('Community posts are not supported via Diaspora');
return 0;
}
}
// Items cannot be stored before they happen ...
if ($item['created'] > DateTimeFormat::utcNow()) {
$item['created'] = DateTimeFormat::utcNow();
@ -881,10 +895,15 @@ class Item
$item['parent-uri'] = $toplevel_parent['uri'];
$item['parent-uri-id'] = $toplevel_parent['uri-id'];
$item['deleted'] = $toplevel_parent['deleted'];
$item['allow_cid'] = $toplevel_parent['allow_cid'];
$item['allow_gid'] = $toplevel_parent['allow_gid'];
$item['deny_cid'] = $toplevel_parent['deny_cid'];
$item['deny_gid'] = $toplevel_parent['deny_gid'];
// Reshares have to keep their permissions to allow forums to work
if (!$item['origin'] || ($item['verb'] != Activity::ANNOUNCE)) {
$item['allow_cid'] = $toplevel_parent['allow_cid'];
$item['allow_gid'] = $toplevel_parent['allow_gid'];
$item['deny_cid'] = $toplevel_parent['deny_cid'];
$item['deny_gid'] = $toplevel_parent['deny_gid'];
}
$parent_origin = $toplevel_parent['origin'];
// Don't federate received participation messages
@ -905,15 +924,6 @@ class Item
$item['private'] = $toplevel_parent['private'];
}
/*
* Edge case. We host a public forum that was originally posted to privately.
* The original author commented, but as this is a comment, the permissions
* weren't fixed up so it will still show the comment as private unless we fix it here.
*/
if ((intval($toplevel_parent['forum_mode']) == 1) && ($toplevel_parent['private'] != self::PUBLIC)) {
$item['private'] = self::PUBLIC;
}
// If its a post that originated here then tag the thread as "mention"
if ($item['origin'] && $item['uid']) {
DBA::update('post-thread-user', ['mention' => true], ['uri-id' => $item['parent-uri-id'], 'uid' => $item['uid']]);
@ -1066,6 +1076,13 @@ class Item
unset($item['causer-id']);
}
if (in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN])) {
$content_warning = BBCode::getAbstract($item['body'], Protocol::ACTIVITYPUB);
if (!empty($content_warning) && empty($item['content-warning'])) {
$item['content-warning'] = $content_warning;
}
}
Post::insert($item['uri-id'], $item);
if ($item['gravity'] == GRAVITY_PARENT) {
@ -1226,8 +1243,11 @@ class Item
return;
}
$self_contact = Contact::selectFirst(['id'], ['uid' => $item['uid'], 'self' => true]);
$self = !empty($self_contact) ? $self_contact['id'] : 0;
$cid = Contact::getIdForURL($author['url'], $item['uid']);
if (empty($cid) || !Contact::isSharing($cid, $item['uid'])) {
if (empty($cid) || (!Contact::isSharing($cid, $item['uid']) && ($cid != $self))) {
Logger::info('The resharer is not a following contact: quit', ['resharer' => $author['url'], 'uid' => $item['uid'], 'cid' => $cid]);
return;
}
@ -1398,7 +1418,7 @@ class Item
$is_reshare = ($item['gravity'] == GRAVITY_ACTIVITY) && ($item['verb'] == Activity::ANNOUNCE);
if ((($item['gravity'] == GRAVITY_PARENT) || $is_reshare) &&
DI::pConfig()->get($uid, 'system', 'accept_only_sharer') &&
DI::pConfig()->get($uid, 'system', 'accept_only_sharer') == self::COMPLETION_NONE &&
!Contact::isSharingByURL($item['author-link'], $uid) &&
!Contact::isSharingByURL($item['owner-link'], $uid)) {
Logger::info('Contact is not a follower, thread will not be stored', ['author' => $item['author-link'], 'uid' => $uid]);
@ -1406,9 +1426,15 @@ class Item
}
if ((($item['gravity'] == GRAVITY_COMMENT) || $is_reshare) && !Post::exists(['uri-id' => $item['thr-parent-id'], 'uid' => $uid])) {
// Only do an auto complete with the source uid "0" to prevent privavy problems
// Fetch the origin user for the post
$origin_uid = self::GetOriginUidForUriId($item['thr-parent-id'], $uid);
if (is_null($origin_uid)) {
Logger::info('Origin item was not found', ['uid' => $uid, 'uri-id' => $item['thr-parent-id']]);
return 0;
}
$causer = $item['causer-id'] ?: $item['author-id'];
$result = self::storeForUserByUriId($item['thr-parent-id'], $uid, ['causer-id' => $causer, 'post-reason' => self::PR_FETCHED]);
$result = self::storeForUserByUriId($item['thr-parent-id'], $uid, ['causer-id' => $causer, 'post-reason' => self::PR_FETCHED], $origin_uid);
Logger::info('Fetched thread parent', ['uri-id' => $item['thr-parent-id'], 'uid' => $uid, 'causer' => $causer, 'result' => $result]);
}
@ -1417,6 +1443,56 @@ class Item
return $stored;
}
/**
* Returns the origin uid of a post if the given user is allowed to see it.
*
* @param int $uriid
* @param int $uid
* @return int
*/
private static function GetOriginUidForUriId(int $uriid, int $uid)
{
if (Post::exists(['uri-id' => $uriid, 'uid' => $uid])) {
return $uid;
}
$post = Post::selectFirst(['uid', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'private'], ['uri-id' => $uriid, 'origin' => true]);
if (!empty($post)) {
if (in_array($post['private'], [Item::PUBLIC, Item::UNLISTED])) {
return $post['uid'];
}
$pcid = Contact::getPublicIdByUserId($uid);
if (empty($pcid)) {
return null;
}
foreach (Item::enumeratePermissions($post, true) as $receiver) {
if ($receiver == $pcid) {
return $post['uid'];
}
}
return null;
}
if (Post::exists(['uri-id' => $uriid, 'uid' => 0])) {
return 0;
}
// When the post belongs to a a forum then all forum users are allowed to access it
foreach (Tag::getByURIId($uriid, [Tag::MENTION, Tag::EXCLUSIVE_MENTION]) as $tag) {
if (DBA::exists('contact', ['uid' => $uid, 'nurl' => Strings::normaliseLink($tag['url']), 'contact-type' => Contact::TYPE_COMMUNITY])) {
$target_uid = User::getIdForURL($tag['url']);
if (!empty($target_uid)) {
return $target_uid;
}
}
}
return null;
}
/**
* Store a public item array for the given users
*
@ -1443,6 +1519,7 @@ class Item
return 0;
}
// Data from the "post-user" table
unset($item['id']);
unset($item['mention']);
unset($item['starred']);
@ -1451,11 +1528,14 @@ class Item
unset($item['pinned']);
unset($item['ignored']);
unset($item['pubmail']);
unset($item['forum_mode']);
unset($item['event-id']);
unset($item['hidden']);
unset($item['notification-type']);
unset($item['post-reason']);
// Data from the "post-delivery-data" table
unset($item['postopts']);
unset($item['inform']);
$item['uid'] = $uid;
$item['origin'] = 0;
@ -1693,7 +1773,10 @@ class Item
}
/**
* Creates an unique guid out of a given uri
* Creates an unique guid out of a given uri.
* This function is used for messages outside the fediverse (Connector posts, feeds, Mails, ...)
* Posts that are created on this system are using System::createUUID.
* Received ActivityPub posts are using Processor::getGUIDByURL.
*
* @param string $uri uri of an item entry
* @param string $host hostname for the GUID prefix
@ -1705,19 +1788,14 @@ class Item
// We have to avoid that different routines could accidentally create the same value
$parsed = parse_url($uri);
// We use a hash of the hostname as prefix for the guid
$guid_prefix = hash("crc32", $host);
// Remove the scheme to make sure that "https" and "http" doesn't make a difference
unset($parsed["scheme"]);
// Glue it together to be able to make a hash from it
$host_id = implode("/", $parsed);
// We could use any hash algorithm since it isn't a security issue
$host_hash = hash("ripemd128", $host_id);
return $guid_prefix.$host_hash;
// Use a mixture of several hashes to provide some GUID like experience
return hash("crc32", $host) . '-'. hash('joaat', $host_id) . '-'. hash('fnv164', $host_id);
}
/**
@ -1875,7 +1953,7 @@ class Item
$owner = User::getOwnerDataById($uid);
if (!DBA::isResult($owner)) {
Logger::warning('User not found, quitting.', ['uid' => $uid]);
Logger::warning('User not found, quitting here.', ['uid' => $uid]);
return false;
}
@ -1884,85 +1962,57 @@ class Item
return false;
}
$item = Post::selectFirst(self::ITEM_FIELDLIST, ['id' => $item_id]);
$item = Post::selectFirst(self::ITEM_FIELDLIST, ['id' => $item_id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'origin' => false]);
if (!DBA::isResult($item)) {
Logger::warning('Post not found, quitting.', ['id' => $item_id]);
Logger::debug('Post is an activity or origin or not found at all, quitting here.', ['id' => $item_id]);
return false;
}
if ($item['wall'] || $item['origin'] || ($item['gravity'] != GRAVITY_PARENT)) {
Logger::debug('Wall item, origin item or no parent post, quitting here.', ['wall' => $item['wall'], 'origin' => $item['origin'], 'gravity' => $item['gravity'], 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
return false;
}
$tags = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]);
foreach ($tags as $tag) {
if (Strings::compareLink($owner['url'], $tag['url'])) {
$mention = true;
Logger::info('Mention found in tag.', ['url' => $tag['url'], 'uri' => $item['uri'], 'uid' => $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
}
}
// This check can most likely be removed since we always are having the tags
if (!$mention) {
$cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism', $item['body'], $matches, PREG_SET_ORDER);
if ($cnt) {
foreach ($matches as $mtch) {
if (Strings::compareLink($owner['url'], $mtch[1])) {
$mention = true;
Logger::notice('Mention found in body.', ['mention' => $mtch[2], 'uri' => $item['uri'], 'uid' => $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
}
if ($item['gravity'] == GRAVITY_PARENT) {
$tags = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]);
foreach ($tags as $tag) {
if (Strings::compareLink($owner['url'], $tag['url'])) {
$mention = true;
Logger::info('Mention found in tag.', ['url' => $tag['url'], 'uri' => $item['uri'], 'uid' => $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
}
}
if (!$mention) {
Logger::info('Top-level post without mention is deleted.', ['uri' => $item['uri'], $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
Post\User::delete(['uri-id' => $item['uri-id'], 'uid' => $item['uid']]);
return true;
}
$arr = ['item' => $item, 'user' => $owner];
Hook::callAll('tagged', $arr);
} else {
$tags = Tag::getByURIId($item['parent-uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]);
foreach ($tags as $tag) {
if (Strings::compareLink($owner['url'], $tag['url'])) {
$mention = true;
Logger::info('Mention found in parent tag.', ['url' => $tag['url'], 'uri' => $item['uri'], 'uid' => $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
}
}
if (!$mention) {
Logger::debug('No mentions found in parent, quitting here.', ['id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
return false;
}
}
if (!$mention) {
Logger::info('Top-level post without mention is deleted.', ['uri' => $item['uri'], $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
Post\User::delete(['uri-id' => $item['uri-id'], 'uid' => $item['uid']]);
return true;
}
$arr = ['item' => $item, 'user' => $owner];
Hook::callAll('tagged', $arr);
Logger::info('Community post will be distributed', ['uri' => $item['uri'], 'uid' => $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
self::performActivity($item['id'], 'announce', $uid);
/**
* All the following lines are only needed for private forums and compatibility to older systems without AP support.
* A possible way would be that the followers list of a forum would always be readable by all followers.
* So this would mean that the comment distribution could be done exactly for the intended audience.
* Or possibly we could store the receivers that had been in the "announce" message above and use this.
*/
// also reset all the privacy bits to the forum default permissions
if ($owner['allow_cid'] || $owner['allow_gid'] || $owner['deny_cid'] || $owner['deny_gid']) {
$private = self::PRIVATE;
} elseif (DI::pConfig()->get($owner['uid'], 'system', 'unlisted')) {
$private = self::UNLISTED;
if ($owner['page-flags'] == User::PAGE_FLAGS_PRVGROUP) {
$allow_cid = '';
$allow_gid = '<' . Group::FOLLOWERS . '>';
$deny_cid = '';
$deny_gid = '';
self::performActivity($item['id'], 'announce', $uid, $allow_cid, $allow_gid, $deny_cid, $deny_gid);
} else {
$private = self::PUBLIC;
self::performActivity($item['id'], 'announce', $uid);
}
$permissionSet = DI::permissionSet()->selectOrCreate(
DI::permissionSetFactory()->createFromString(
$owner['uid'],
$owner['allow_cid'],
$owner['allow_gid'],
$owner['deny_cid'],
$owner['deny_gid']
));
$forum_mode = ($owner['page-flags'] == User::PAGE_FLAGS_PRVGROUP) ? 2 : 1;
$fields = ['wall' => true, 'origin' => true, 'forum_mode' => $forum_mode, 'contact-id' => $owner['id'],
'owner-id' => Contact::getPublicIdByUserId($uid), 'private' => $private, 'psid' => $permissionSet->id];
self::update($fields, ['id' => $item['id']]);
Worker::add(['priority' => PRIORITY_HIGH, 'dont_fork' => true], 'Notifier', Delivery::POST, (int)$item['uri-id'], (int)$item['uid']);
Logger::info('Community post had been distributed', ['uri' => $item['uri'], 'uid' => $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
return false;
}
@ -2325,12 +2375,17 @@ class Item
*
* Toggle activities as like,dislike,attend of an item
*
* @param int $item_id
* @param int $item_id
* @param string $verb
* Activity verb. One of
* like, unlike, dislike, undislike, attendyes, unattendyes,
* attendno, unattendno, attendmaybe, unattendmaybe,
* announce, unannouce
* @param int $uid
* @param string $allow_cid
* @param string $allow_gid
* @param string $deny_cid
* @param string $deny_gid
* @return bool
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
@ -2338,7 +2393,7 @@ class Item
* array $arr
* 'post_id' => ID of posted item
*/
public static function performActivity(int $item_id, string $verb, int $uid)
public static function performActivity(int $item_id, string $verb, int $uid, string $allow_cid = null, string $allow_gid = null, string $deny_cid = null, string $deny_gid = null)
{
if (empty($uid)) {
return false;
@ -2499,10 +2554,10 @@ class Item
'body' => $activity,
'verb' => $activity,
'object-type' => $objtype,
'allow_cid' => $item['allow_cid'],
'allow_gid' => $item['allow_gid'],
'deny_cid' => $item['deny_cid'],
'deny_gid' => $item['deny_gid'],
'allow_cid' => $allow_cid ?? $item['allow_cid'],
'allow_gid' => $allow_gid ?? $item['allow_gid'],
'deny_cid' => $deny_cid ?? $item['deny_cid'],
'deny_gid' => $deny_gid ?? $item['deny_gid'],
'visible' => 1,
'unseen' => 1,
];
@ -3163,30 +3218,20 @@ class Item
}
/**
* Is the given item array a post that is sent as starting post to a forum?
* Does the given uri-id belongs to a post that is sent as starting post to a forum?
*
* @param array $item
* @param array $owner
* @param int $uri_id
*
* @return boolean "true" when it is a forum post
*/
public static function isForumPost(array $item, array $owner = [])
public static function isForumPost(int $uri_id)
{
if (empty($owner)) {
$owner = User::getOwnerDataById($item['uid']);
if (empty($owner)) {
return false;
foreach (Tag::getByURIId($uri_id, [Tag::EXCLUSIVE_MENTION]) as $tag) {
if (DBA::exists('contact', ['uid' => 0, 'nurl' => Strings::normaliseLink($tag['url']), 'contact-type' => Contact::TYPE_COMMUNITY])) {
return true;
}
}
if (($item['author-id'] == $item['owner-id']) ||
($owner['id'] == $item['contact-id']) ||
($item['uri-id'] != $item['parent-uri-id']) ||
$item['origin']) {
return false;
}
return Contact::isForum($item['contact-id']);
return false;
}
/**

View file

@ -21,6 +21,7 @@
namespace Friendica\Model;
use Friendica\Core\ACL;
use Friendica\Core\Logger;
use Friendica\Core\System;
use Friendica\Core\Worker;
@ -39,10 +40,12 @@ class Mail
* Insert private message
*
* @param array $msg
* @param bool $notifiction
* @param bool $notification
* @return int|boolean Message ID or false on error
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function insert($msg, $notifiction = true)
public static function insert($msg, $notification = true)
{
if (!isset($msg['reply'])) {
$msg['reply'] = DBA::exists('mail', ['parent-uri' => $msg['parent-uri']]);
@ -92,7 +95,7 @@ class Mail
DBA::update('conv', ['updated' => DateTimeFormat::utcNow()], ['id' => $msg['convid']]);
}
if ($notifiction) {
if ($notification) {
$user = User::getById($msg['uid']);
// send notifications.
$notif_params = [
@ -139,11 +142,15 @@ class Mail
return -2;
}
$contact = DBA::selectFirst('contact', [], ['id' => $recipient, 'uid' => local_user()]);
if (!DBA::isResult($contact)) {
$contacts = ACL::getValidMessageRecipientsForUser(local_user());
$contactIndex = array_search($recipient, array_column($contacts, 'id'));
if ($contactIndex === false) {
return -2;
}
$contact = $contacts[$contactIndex];
Photo::setPermissionFromBody($body, local_user(), $me['id'], '<' . $contact['id'] . '>', '', '', '');
$guid = System::createUUID();
@ -167,20 +174,12 @@ class Mail
$convuri = '';
if (!$convid) {
// create a new conversation
$recip_host = substr($contact['url'], strpos($contact['url'], '://') + 3);
$recip_host = substr($recip_host, 0, strpos($recip_host, '/'));
$recip_handle = (($contact['addr']) ? $contact['addr'] : $contact['nick'] . '@' . $recip_host);
$sender_handle = $a->getLoggedInUserNickname() . '@' . substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3);
$conv_guid = System::createUUID();
$convuri = $recip_handle . ':' . $conv_guid;
$convuri = $contact['addr'] . ':' . $conv_guid;
$handles = $recip_handle . ';' . $sender_handle;
$fields = ['uid' => local_user(), 'guid' => $conv_guid, 'creator' => $sender_handle,
$fields = ['uid' => local_user(), 'guid' => $conv_guid, 'creator' => $me['addr'],
'created' => DateTimeFormat::utcNow(), 'updated' => DateTimeFormat::utcNow(),
'subject' => $subject, 'recips' => $handles];
'subject' => $subject, 'recips' => $contact['addr'] . ';' . $me['addr']];
if (DBA::insert('conv', $fields)) {
$convid = DBA::lastInsertId();
}

View file

@ -518,7 +518,7 @@ class Media
$condition = DBA::mergeConditions($condition, ['type' => $types]);
}
return DBA::selectToArray('post-media', [], $condition);
return DBA::selectToArray('post-media', [], $condition, ['order' => ['id']]);
}
/**

View file

@ -33,6 +33,7 @@ use Friendica\Model\Contact;
use Friendica\Model\Post;
use Friendica\Model\Subscription;
use Friendica\Model\Tag;
use Friendica\Model\User;
use Friendica\Navigation\Notifications;
use Friendica\Network\HTTPException;
use Friendica\Protocol\Activity;
@ -176,12 +177,24 @@ class UserNotification
return;
}
$user = User::getById($uid, ['account-type']);
if (in_array($user['account-type'], [User::ACCOUNT_TYPE_COMMUNITY, User::ACCOUNT_TYPE_RELAY])) {
return;
}
$author = Contact::getById($item['author-id'], ['contact-type']);
if (empty($author)) {
return;
}
$notification_type = self::TYPE_NONE;
if (self::checkShared($item, $uid)) {
$notification_type = $notification_type | self::TYPE_SHARED;
self::insertNotificationByItem(self::TYPE_SHARED, $uid, $item);
$notified = true;
} elseif ($author['contact-type'] == Contact::TYPE_COMMUNITY) {
return;
} else {
$notified = false;
}
@ -189,11 +202,16 @@ class UserNotification
$profiles = self::getProfileForUser($uid);
// Fetch all contacts for the given profiles
$contacts = [];
$contacts = [];
$iscommunity = false;
$ret = DBA::select('contact', ['id'], ['uid' => 0, 'nurl' => $profiles]);
$ret = DBA::select('contact', ['id', 'contact-type'], ['uid' => 0, 'nurl' => $profiles]);
while ($contact = DBA::fetch($ret)) {
$contacts[] = $contact['id'];
if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) {
$iscommunity = true;
}
}
DBA::close($ret);
@ -226,7 +244,7 @@ class UserNotification
}
}
if (self::checkDirectCommentedThread($item, $contacts)) {
if (!$iscommunity && self::checkDirectCommentedThread($item, $contacts)) {
$notification_type = $notification_type | self::TYPE_DIRECT_THREAD_COMMENT;
if (!$notified) {
self::insertNotificationByItem(self::TYPE_DIRECT_THREAD_COMMENT, $uid, $item);
@ -290,7 +308,7 @@ class UserNotification
return;
}
$notification = (new Notifications\Factory\Notification(DI::logger()))->createForUser(
$notification = (new Notifications\Factory\Notification(DI::baseUrl(), DI::l10n(), DI::localRelationship(), DI::logger()))->createForUser(
$uid,
$item['vid'],
$type,
@ -310,7 +328,7 @@ class UserNotification
/**
* Add a notification entry
*
* @param int $actor Contact ID of the actor
* @param int $actor Public contact ID of the actor
* @param string $verb One of the Activity verb constant values
* @param int $uid User ID
* @return boolean
@ -318,7 +336,7 @@ class UserNotification
*/
public static function insertNotification(int $actor, string $verb, int $uid): bool
{
$notification = (new Notifications\Factory\Notification(DI::logger()))->createForRelationship(
$notification = (new Notifications\Factory\Notification(DI::baseUrl(), DI::l10n(), DI::localRelationship(), DI::logger()))->createForRelationship(
$uid,
$actor,
$verb
@ -401,6 +419,14 @@ class UserNotification
return false;
}
// Don't notify about reshares by communities of our own posts or each time someone comments
if (($item['verb'] == Activity::ANNOUNCE) && DBA::exists('contact', ['id' => $item['contact-id'], 'contact-type' => Contact::TYPE_COMMUNITY])) {
$post = Post::selectFirst(['origin', 'gravity'], ['uri-id' => $item['thr-parent-id'], 'uid' => $uid]);
if ($post['origin'] || ($post['gravity'] != GRAVITY_PARENT)) {
return false;
}
}
// Check if the contact posted or shared something directly
if (DBA::exists('contact', ['id' => $item['contact-id'], 'notify_new_posts' => true])) {
return true;

View file

@ -362,7 +362,7 @@ class Profile
}
// Fetch the account type
$account_type = Contact::getAccountType($profile);
$account_type = Contact::getAccountType($profile['account-type']);
if (!empty($profile['address']) || !empty($profile['location'])) {
$location = DI::l10n()->t('Location:');

View file

@ -48,15 +48,20 @@ class Tag
*/
const IMPLICIT_MENTION = 8;
/**
* An exclusive mention transfers the ownership of the post to the target account, usually a forum.
* An exclusive mention transmits the post only to the target account without transmitting it to the followers, usually a forum.
*/
const EXCLUSIVE_MENTION = 9;
const TO = 10;
const CC = 11;
const BTO = 12;
const BCC = 13;
const TAG_CHARACTER = [
self::HASHTAG => '#',
self::MENTION => '@',
self::IMPLICIT_MENTION => '%',
self::EXCLUSIVE_MENTION => '!',
self::IMPLICIT_MENTION => '%',
];
/**
@ -66,9 +71,8 @@ class Tag
* @param integer $type
* @param string $name
* @param string $url
* @param boolean $probing
*/
public static function store(int $uriid, int $type, string $name, string $url = '', $probing = true)
public static function store(int $uriid, int $type, string $name, string $url = '')
{
if ($type == self::HASHTAG) {
// Trim Unicode non-word characters
@ -77,7 +81,7 @@ class Tag
$tags = explode(self::TAG_CHARACTER[self::HASHTAG], $name);
if (count($tags) > 1) {
foreach ($tags as $tag) {
self::store($uriid, $type, $tag, $url, $probing);
self::store($uriid, $type, $tag, $url);
}
return;
}
@ -90,7 +94,7 @@ class Tag
$cid = 0;
$tagid = 0;
if (in_array($type, [self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION])) {
if (in_array($type, [self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION, self::TO, self::CC, self::BTO, self::BCC])) {
if (empty($url)) {
// No mention without a contact url
return;
@ -100,32 +104,13 @@ class Tag
Logger::notice('Wrong scheme in url', ['url' => $url, 'callstack' => System::callstack(20)]);
}
if (!$probing) {
$condition = ['nurl' => Strings::normaliseLink($url), 'uid' => 0, 'deleted' => false];
$contact = DBA::selectFirst('contact', ['id'], $condition, ['order' => ['id']]);
if (DBA::isResult($contact)) {
$cid = $contact['id'];
Logger::info('Got id for contact url', ['cid' => $cid, 'url' => $url]);
}
if (empty($cid)) {
$ssl_url = str_replace('http://', 'https://', $url);
$condition = ['`alias` IN (?, ?, ?) AND `uid` = ? AND NOT `deleted`', $url, Strings::normaliseLink($url), $ssl_url, 0];
$contact = DBA::selectFirst('contact', ['id'], $condition, ['order' => ['id']]);
if (DBA::isResult($contact)) {
$cid = $contact['id'];
Logger::info('Got id for contact alias', ['cid' => $cid, 'url' => $url]);
}
}
} else {
$cid = Contact::getIdForURL($url, 0, false);
Logger::info('Got id by probing', ['cid' => $cid, 'url' => $url]);
}
$cid = Contact::getIdForURL($url, 0, false);
Logger::debug('Got id for contact', ['cid' => $cid, 'url' => $url]);
if (empty($cid)) {
// The contact wasn't found in the system (most likely some dead account)
// We ensure that we only store a single entry by overwriting the previous name
Logger::info('Contact not found, updating tag', ['url' => $url, 'name' => $name]);
Logger::info('URL is not a known contact, updating tag', ['url' => $url, 'name' => $name]);
if (!DBA::exists('tag', ['name' => substr($name, 0, 96), 'url' => $url])) {
DBA::update('tag', ['name' => substr($name, 0, 96)], ['url' => $url]);
}
@ -133,10 +118,12 @@ class Tag
}
if (empty($cid)) {
if (($type != self::HASHTAG) && !empty($url) && ($url != $name)) {
$url = strtolower($url);
} else {
$url = '';
if (!in_array($type, [self::TO, self::CC, self::BTO, self::BCC])) {
if (($type != self::HASHTAG) && !empty($url) && ($url != $name)) {
$url = strtolower($url);
} else {
$url = '';
}
}
$tagid = self::getID($name, $url);
@ -286,7 +273,7 @@ class Tag
*/
public static function existsForPost(int $uriid)
{
return DBA::exists('post-tag', ['uri-id' => $uriid, 'type' => [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION, self::EXCLUSIVE_MENTION]]);
return DBA::exists('post-tag', ['uri-id' => $uriid, 'type' => [self::HASHTAG, self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION]]);
}
/**
@ -368,7 +355,7 @@ class Tag
return;
}
$tags = DBA::select('tag-view', ['name', 'url'], ['uri-id' => $parent_uri_id]);
$tags = DBA::select('tag-view', ['name', 'url'], ['uri-id' => $parent_uri_id, 'type' => [self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION]]);
while ($tag = DBA::fetch($tags)) {
self::store($uri_id, self::IMPLICIT_MENTION, $tag['name'], $tag['url']);
}
@ -383,7 +370,7 @@ class Tag
* @return array
* @throws \Exception
*/
public static function getByURIId(int $uri_id, array $type = [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION, self::EXCLUSIVE_MENTION])
public static function getByURIId(int $uri_id, array $type = [self::HASHTAG, self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION])
{
$condition = ['uri-id' => $uri_id, 'type' => $type];
return DBA::selectToArray('tag-view', ['type', 'name', 'url'], $condition);
@ -397,7 +384,7 @@ class Tag
* @return string tags and mentions
* @throws \Exception
*/
public static function getCSVByURIId(int $uri_id, array $type = [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION, self::EXCLUSIVE_MENTION])
public static function getCSVByURIId(int $uri_id, array $type = [self::HASHTAG, self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION])
{
$tag_list = [];
$tags = self::getByURIId($uri_id, $type);

View file

@ -25,6 +25,7 @@ use Friendica\BaseModule;
use Friendica\Model\Contact;
use Friendica\Model\User;
use Friendica\Protocol\ActivityPub;
use Friendica\Util\HTTPSignature;
/**
* ActivityPub Followers
@ -45,7 +46,7 @@ class Followers extends BaseModule
$page = $_REQUEST['page'] ?? null;
$followers = ActivityPub\Transmitter::getContacts($owner, [Contact::FOLLOWER, Contact::FRIEND], 'followers', $page);
$followers = ActivityPub\Transmitter::getContacts($owner, [Contact::FOLLOWER, Contact::FRIEND], 'followers', $page, (string)HTTPSignature::getSigner('', $_SERVER));
header('Content-Type: application/activity+json');
echo json_encode($followers);

View file

@ -70,9 +70,7 @@ class Objects extends BaseModule
}
}
$item = Post::selectFirst(['id', 'uid', 'origin', 'author-link', 'changed', 'private', 'psid', 'gravity', 'deleted', 'parent-uri-id'],
['uri-id' => $itemuri['id']], ['order' => ['origin' => true]]);
$item = Post::selectFirst([], ['uri-id' => $itemuri['id'], 'origin' => true]);
if (!DBA::isResult($item)) {
throw new HTTPException\NotFoundException();
}
@ -81,25 +79,17 @@ class Objects extends BaseModule
if (!$validated) {
$requester = HTTPSignature::getSigner('', $_SERVER);
if (!empty($requester) && $item['origin']) {
$requester_id = Contact::getIdForURL($requester, $item['uid']);
if (!empty($requester_id)) {
$permissionSets = DI::permissionSet()->selectByContactId($requester_id, $item['uid']);
$psids = array_merge($permissionSets->column('id'), [PermissionSet::PUBLIC]);
$validated = in_array($item['psid'], $psids);
if (!empty($requester)) {
$receivers = Item::enumeratePermissions($item, false);
$receivers[] = $item['contact-id'];
$validated = in_array(Contact::getIdForURL($requester, $item['uid']), $receivers);
if (!$validated) {
$validated = in_array(Contact::getIdForURL($requester), $receivers);
}
}
}
if ($validated) {
// Valid items are original post or posted from this node (including in the case of a forum)
$validated = ($item['origin'] || (parse_url($item['author-link'], PHP_URL_HOST) == parse_url(DI::baseUrl()->get(), PHP_URL_HOST)));
if (!$validated && $item['deleted']) {
$validated = Post::exists(['origin' => true, 'uri-id' => $item['parent-uri-id']]);
}
}
if (!$validated) {
throw new HTTPException\NotFoundException();
}

View file

@ -164,19 +164,19 @@ class Federation extends BaseAdmin
}
$gserver['platform'] = $systems[$platform]['name'];
$gserver['totallbl'] = DI::l10n()->t('%d total systems', $gserver['total']);
$gserver['monthlbl'] = DI::l10n()->t('%d active users last month', $gserver['month']);
$gserver['halfyearlbl'] = DI::l10n()->t('%d active users last six months', $gserver['halfyear']);
$gserver['userslbl'] = DI::l10n()->t('%d registered users', $gserver['users']);
$gserver['postslbl'] = DI::l10n()->t('%d locally created posts and comments', $gserver['posts']);
$gserver['totallbl'] = DI::l10n()->t('%s total systems', number_format($gserver['total']));
$gserver['monthlbl'] = DI::l10n()->t('%s active users last month', number_format($gserver['month']));
$gserver['halfyearlbl'] = DI::l10n()->t('%s active users last six months', number_format($gserver['halfyear']));
$gserver['userslbl'] = DI::l10n()->t('%s registered users', number_format($gserver['users']));
$gserver['postslbl'] = DI::l10n()->t('%s locally created posts and comments', number_format($gserver['posts']));
if (($gserver['users'] > 0) && ($gserver['posts'] > 0)) {
$gserver['postsuserlbl'] = DI::l10n()->t('%d posts per user', $gserver['posts'] / $gserver['users']);
$gserver['postsuserlbl'] = DI::l10n()->t('%s posts per user', number_format($gserver['posts'] / $gserver['users'], 1));
} else {
$gserver['postsuserlbl'] = '';
}
if (($gserver['users'] > 0) && ($gserver['total'] > 0)) {
$gserver['userssystemlbl'] = DI::l10n()->t('%d users per system', $gserver['users'] / $gserver['total']);
$gserver['userssystemlbl'] = DI::l10n()->t('%s users per system', number_format($gserver['users'] / $gserver['total'], 1));
} else {
$gserver['userssystemlbl'] = '';
}
@ -196,7 +196,7 @@ class Federation extends BaseAdmin
'$intro' => $intro,
'$counts' => $counts,
'$version' => FRIENDICA_VERSION,
'$legendtext' => DI::l10n()->t('Currently this node is aware of %d nodes (%d active users last month, %d active users last six months, %d registered users in total) from the following platforms:', $total, $month, $halfyear, $users),
'$legendtext' => DI::l10n()->t('Currently this node is aware of %d nodes (%d active users last month, %d active users last six months, %d registered users in total) from the following platforms:', number_format($total), number_format($month), number_format($halfyear), number_format($users)),
]);
}

View file

@ -21,9 +21,9 @@
namespace Friendica\Module\Admin\Logs;
use Friendica\DI;
use Friendica\Core\Renderer;
use Friendica\Core\Theme;
use Friendica\DI;
use Friendica\Module\BaseAdmin;
use Psr\Log\LogLevel;
@ -80,9 +80,10 @@ class View extends BaseAdmin
}
}
return Renderer::replaceMacros($t, [
'$title' => DI::l10n()->t('Administration'),
'$page' => DI::l10n()->t('View Logs'),
'$l10n' => [
'$baseurl' => DI::baseUrl()->get(true),
'$title' => DI::l10n()->t('Administration'),
'$page' => DI::l10n()->t('View Logs'),
'$l10n' => [
'Search' => DI::l10n()->t('Search'),
'Search_in_logs' => DI::l10n()->t('Search in logs'),
'Show_all' => DI::l10n()->t('Show all'),

View file

@ -526,7 +526,7 @@ class Site extends BaseAdmin
'$touch_icon' => ['touch_icon', DI::l10n()->t('Touch icon'), DI::config()->get('system', 'touch_icon'), DI::l10n()->t('Link to an icon that will be used for tablets and mobiles.')],
'$additional_info' => ['additional_info', DI::l10n()->t('Additional Info'), $additional_info, DI::l10n()->t('For public servers: you can add additional information here that will be listed at %s/servers.', Search::getGlobalDirectory())],
'$language' => ['language', DI::l10n()->t('System language'), DI::config()->get('system', 'language'), '', $lang_choices],
'$theme' => ['theme', DI::l10n()->t('System theme'), DI::config()->get('system', 'theme'), DI::l10n()->t('Default system theme - may be over-ridden by user profiles - <a href="/admin/themes" id="cnftheme">Change default theme settings</a>'), $theme_choices],
'$theme' => ['theme', DI::l10n()->t('System theme'), DI::config()->get('system', 'theme'), DI::l10n()->t('Default system theme - may be over-ridden by user profiles - <a href="%s" id="cnftheme">Change default theme settings</a>', DI::baseUrl()->get(true) . '/admin/themes'), $theme_choices],
'$theme_mobile' => ['theme_mobile', DI::l10n()->t('Mobile system theme'), DI::config()->get('system', 'mobile-theme', '---'), DI::l10n()->t('Theme for mobile devices'), $theme_choices_mobile],
'$ssl_policy' => ['ssl_policy', DI::l10n()->t('SSL link policy'), DI::config()->get('system', 'ssl_policy'), DI::l10n()->t('Determines whether generated links should be forced to use SSL'), $ssl_choices],
'$force_ssl' => ['force_ssl', DI::l10n()->t('Force SSL'), DI::config()->get('system', 'force_ssl'), DI::l10n()->t('Force all Non-SSL requests to SSL - Attention: on some systems it could lead to endless loops.')],
@ -570,8 +570,8 @@ class Site extends BaseAdmin
'$diaspora_not_able' => DI::l10n()->t('Diaspora support can\'t be enabled because Friendica was installed into a sub directory.'),
'$diaspora_enabled' => ['diaspora_enabled', DI::l10n()->t('Enable Diaspora support'), DI::config()->get('system', 'diaspora_enabled', $diaspora_able), DI::l10n()->t('Enable built-in Diaspora network compatibility for communicating with diaspora servers.')],
'$verifyssl' => ['verifyssl', DI::l10n()->t('Verify SSL'), DI::config()->get('system', 'verifyssl'), DI::l10n()->t('If you wish, you can turn on strict certificate checking. This will mean you cannot connect (at all) to self-signed SSL sites.')],
'$proxyuser' => ['proxyuser', DI::l10n()->t('Proxy user'), DI::config()->get('system', 'proxyuser'), ''],
'$proxy' => ['proxy', DI::l10n()->t('Proxy URL'), DI::config()->get('system', 'proxy'), ''],
'$proxyuser' => ['proxyuser', DI::l10n()->t('Proxy user'), DI::config()->get('system', 'proxyuser'), DI::l10n()->t('User name for the proxy server.')],
'$proxy' => ['proxy', DI::l10n()->t('Proxy URL'), DI::config()->get('system', 'proxy'), DI::l10n()->t('If you want to use a proxy server that Friendica should use to connect to the network, put the URL of the proxy here.')],
'$timeout' => ['timeout', DI::l10n()->t('Network timeout'), DI::config()->get('system', 'curl_timeout'), DI::l10n()->t('Value is in seconds. Set to 0 for unlimited (not recommended).')],
'$maxloadavg' => ['maxloadavg', DI::l10n()->t('Maximum Load Average'), DI::config()->get('system', 'maxloadavg'), DI::l10n()->t('Maximum system load before delivery and poll processes are deferred - default %d.', 20)],
'$min_memory' => ['min_memory', DI::l10n()->t('Minimal Memory'), DI::config()->get('system', 'min_memory'), DI::l10n()->t('Minimal free memory in MB for the worker. Needs access to /proc/meminfo - default 0 (deactivated).')],

View file

@ -76,7 +76,7 @@ class Details extends BaseAdmin
require_once "view/theme/$theme/config.php";
if (function_exists('theme_admin')) {
$admin_form = '<iframe onload="resizeIframe(this);" src="/admin/themes/' . $theme . '/embed?mode=minimal" width="100%" height="600px" frameborder="no"></iframe>';
$admin_form = '<iframe onload="resizeIframe(this);" src="' . DI::baseUrl()->get(true) . '/admin/themes/' . $theme . '/embed?mode=minimal" width="100%" height="600px" frameborder="no"></iframe>';
}
}

View file

@ -24,6 +24,7 @@ namespace Friendica\Module\Admin\Themes;
use Friendica\App;
use Friendica\Core\L10n;
use Friendica\Core\Renderer;
use Friendica\DI;
use Friendica\Module\BaseAdmin;
use Friendica\Module\Response;
use Friendica\Util\Profiler;
@ -94,7 +95,7 @@ class Embed extends BaseAdmin
$t = Renderer::getMarkupTemplate('admin/addons/embed.tpl');
return Renderer::replaceMacros($t, [
'$action' => '/admin/themes/' . $theme . '/embed?mode=minimal',
'$action' => DI::baseUrl()->get(true) . '/admin/themes/' . $theme . '/embed?mode=minimal',
'$form' => $admin_form,
'$form_security_token' => self::getFormSecurityToken("admin_theme_settings"),
]);

View file

@ -37,7 +37,7 @@ class Index extends BaseAdmin
// reload active themes
if (!empty($_GET['action'])) {
self::checkFormSecurityTokenRedirectOnError(DI::baseUrl()->get() . '/admin/themes', 'admin_themes', 't');
self::checkFormSecurityTokenRedirectOnError('/admin/themes', 'admin_themes', 't');
switch ($_GET['action']) {
case 'reload':

View file

@ -23,7 +23,9 @@ namespace Friendica\Module\Api\Friendica;
use Friendica\DI;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException\BadRequestException;
/**
* API endpoints:
@ -49,15 +51,16 @@ class Activity extends BaseApi
'id' => 0, // Id of the post
], $request);
$res = Item::performActivity($request['id'], $this->parameters['verb'], $uid);
$post = Post::selectFirst(['id'], ['uri-id' => $request['id'], 'uid' => [0, $uid]], ['order' => ['uid' => true]]);
if (empty($post['id'])) {
throw new BadRequestException('Item id not found');
}
$res = Item::performActivity($post['id'], $this->parameters['verb'], $uid);
if ($res) {
if (($this->parameters['extension'] ?? '') == 'xml') {
$ok = 'true';
} else {
$ok = 'ok';
}
$this->response->exit('ok', ['ok' => $ok], $this->parameters['extension'] ?? null);
$status_info = DI::twitterStatus()->createFromUriId($request['id'], $uid)->toArray();
$this->response->exit('status', ['status' => $status_info], $this->parameters['extension'] ?? null);
} else {
$this->response->error(500, 'Error adding activity', '', $this->parameters['extension'] ?? null);
}

View file

@ -23,7 +23,6 @@ namespace Friendica\Module\Api\Friendica\Events;
use Friendica\Content\Text\BBCode;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Module\BaseApi;
/**
@ -40,7 +39,7 @@ class Index extends BaseApi
$request = $this->getRequest([
'since_id' => 0,
'count' => 0,
'count' => 50,
], $request);
$condition = ["`id` > ? AND `uid` = ?", $request['since_id'], $uid];

View file

@ -32,7 +32,7 @@ use Friendica\Network\HTTPException;
*/
class Show extends BaseApi
{
protected function post(array $request = [])
protected function rawContent(array $request = [])
{
BaseApi::checkAllowedScope(BaseApi::SCOPE_READ);
$uid = BaseApi::getCurrentUserID();

View file

@ -44,7 +44,7 @@ class Photo extends BaseApi
$this->friendicaPhoto = $friendicaPhoto;
}
protected function post(array $request = [])
protected function rawContent(array $request = [])
{
BaseApi::checkAllowedScope(BaseApi::SCOPE_READ);
$uid = BaseApi::getCurrentUserID();

View file

@ -56,7 +56,7 @@ class Conversation extends BaseApi
Logger::info(BaseApi::LOG_PREFIX . '{subaction}', ['module' => 'api', 'action' => 'conversation', 'subaction' => 'show', 'id' => $id]);
// try to fetch the item for the local user - or the public item, if there is no local one
$item = Post::selectFirst(['parent-uri-id'], ['id' => $id]);
$item = Post::selectFirst(['parent-uri-id'], ['uri-id' => $id]);
if (!DBA::isResult($item)) {
throw new BadRequestException("There is no status with the id $id.");
}
@ -68,15 +68,15 @@ class Conversation extends BaseApi
$id = $parent['id'];
$condition = ["`parent` = ? AND `uid` IN (0, ?) AND `gravity` IN (?, ?) AND `id` > ?",
$condition = ["`parent` = ? AND `uid` IN (0, ?) AND `gravity` IN (?, ?) AND `uri-id` > ?",
$id, $uid, GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
if ($max_id > 0) {
$condition[0] .= " AND `id` <= ?";
$condition[0] .= " AND `uri-id` <= ?";
$condition[] = $max_id;
}
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$params = ['order' => ['uri-id' => true], 'limit' => [$start, $count]];
$statuses = Post::selectForUser($uid, [], $condition, $params);
if (!DBA::isResult($statuses)) {

View file

@ -59,8 +59,7 @@ class Block extends BaseApi
Contact\User::setBlocked($cdata['user'], $uid, true);
// Mastodon-expected behavior: relationship is severed on block
Contact::terminateFriendship($owner, $contact);
Contact::revokeFollow($contact);
Contact::terminateFriendship($contact);
System::jsonExit(DI::mstdnRelationship()->createFromContactId($this->parameters['id'], $uid)->toArray());
}

View file

@ -40,7 +40,14 @@ class Unfollow extends BaseApi
DI::mstdnError()->UnprocessableEntity();
}
Contact::unfollow($this->parameters['id'], $uid);
$cdata = Contact::getPublicAndUserContactID($this->parameters['id'], $uid);
if (empty($cdata['user'])) {
DI::mstdnError()->RecordNotFound();
}
$contact = Contact::getById($cdata['user']);
Contact::unfollow($contact);
System::jsonExit(DI::mstdnRelationship()->createFromContactId($this->parameters['id'], $uid)->toArray());
}

View file

@ -21,20 +21,44 @@
namespace Friendica\Module\Api\Mastodon;
use Friendica\App;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n;
use Friendica\Core\System;
use Friendica\Database\Database;
use Friendica\Module\Api\ApiResponse;
use Friendica\Module\BaseApi;
use Friendica\Object\Api\Mastodon\Instance as InstanceEntity;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
/**
* @see https://docs.joinmastodon.org/api/rest/instances/
*/
class Instance extends BaseApi
{
/** @var Database */
private $database;
/** @var IManageConfigValues */
private $config;
public function __construct(App $app, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, ApiResponse $response, Database $database, IManageConfigValues $config, array $server, array $parameters = [])
{
parent::__construct($app, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->database = $database;
$this->config = $config;
}
/**
* @param array $request
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \Friendica\Network\HTTPException\NotFoundException
* @throws \ImagickException
*/
protected function rawContent(array $request = [])
{
System::jsonExit(InstanceEntity::get());
System::jsonExit(new InstanceEntity($this->config, $this->baseUrl, $this->database));
}
}

View file

@ -36,12 +36,32 @@ class Accounts extends BaseApi
{
protected function delete(array $request = [])
{
$this->response->unsupported(Router::DELETE, $request);
self::checkAllowedScope(self::SCOPE_WRITE);
$request = $this->getRequest([
'account_ids' => [], // Array of account IDs to remove from the list
], $request);
if (empty($request['account_ids']) || empty($this->parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
return Group::removeMembers($this->parameters['id'], $request['account_ids']);
}
protected function post(array $request = [])
{
$this->response->unsupported(Router::POST, $request);
self::checkAllowedScope(self::SCOPE_WRITE);
$request = $this->getRequest([
'account_ids' => [], // Array of account IDs to add to the list
], $request);
if (empty($request['account_ids']) || empty($this->parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
return Group::addMembers($this->parameters['id'], $request['account_ids']);
}
/**

View file

@ -21,8 +21,8 @@
namespace Friendica\Module\Api\Mastodon;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\Markdown;
use Friendica\Core\Protocol;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
@ -63,17 +63,12 @@ class Statuses extends BaseApi
// The imput is defined as text. So we can use Markdown for some enhancements
$body = Markdown::toBBCode($request['status']);
// Avoids potential double expansion of existing links
$body = BBCode::performWithEscapedTags($body, ['url'], function ($body) {
return BBCode::expandTags($body);
});
$item = [];
$item = [];
$item['network'] = Protocol::DFRN;
$item['uid'] = $uid;
$item['verb'] = Activity::POST;
$item['contact-id'] = $owner['id'];
$item['author-id'] = $item['owner-id'] = Contact::getPublicIdByUserId($uid);
$item['title'] = $request['spoiler_text'];
$item['body'] = $body;
if (!empty(self::getCurrentApplication()['name'])) {
@ -114,14 +109,20 @@ class Statuses extends BaseApi
$item['private'] = Item::PRIVATE;
break;
case 'direct':
// Direct messages are currently unsupported
DI::mstdnError()->InternalError('Direct messages are currently unsupported');
// The permissions are assigned in "expandTags"
break;
default:
$item['allow_cid'] = $owner['allow_cid'];
$item['allow_gid'] = $owner['allow_gid'];
$item['deny_cid'] = $owner['deny_cid'];
$item['deny_gid'] = $owner['deny_gid'];
if (is_numeric($request['visibility']) && Group::exists($request['visibility'], $uid)) {
$item['allow_cid'] = '';
$item['allow_gid'] = '<' . $request['visibility'] . '>';
$item['deny_cid'] = '';
$item['deny_gid'] = '';
} else {
$item['allow_cid'] = $owner['allow_cid'];
$item['allow_gid'] = $owner['allow_gid'];
$item['deny_cid'] = $owner['deny_cid'];
$item['deny_gid'] = $owner['deny_gid'];
}
if (!empty($item['allow_cid'] . $item['allow_gid'] . $item['deny_cid'] . $item['deny_gid'])) {
$item['private'] = Item::PRIVATE;
@ -139,16 +140,21 @@ class Statuses extends BaseApi
if ($request['in_reply_to_id']) {
$parent = Post::selectFirst(['uri'], ['uri-id' => $request['in_reply_to_id'], 'uid' => [0, $uid]]);
$item['thr-parent'] = $parent['uri'];
$item['gravity'] = GRAVITY_COMMENT;
$item['object-type'] = Activity\ObjectType::COMMENT;
$item['body'] = '[abstract=' . Protocol::ACTIVITYPUB . ']' . $request['spoiler_text'] . "[/abstract]\n" . $item['body'];
} else {
self::checkThrottleLimit();
$item['gravity'] = GRAVITY_PARENT;
$item['object-type'] = Activity\ObjectType::NOTE;
$item['title'] = $request['spoiler_text'];
}
$item = DI::contentItem()->expandTags($item, $request['visibility'] == 'direct');
if (!empty($request['media_ids'])) {
$item['object-type'] = Activity\ObjectType::IMAGE;
$item['post-type'] = Item::PT_IMAGE;

View file

@ -42,7 +42,7 @@ class Bookmark extends BaseApi
DI::mstdnError()->UnprocessableEntity();
}
$item = Post::selectFirstForUser($uid, ['id', 'gravity'], ['uri-id' => $this->parameters['id'], 'uid' => [$uid, 0]]);
$item = Post::selectFirst(['uid', 'id', 'gravity'], ['uri-id' => $this->parameters['id'], 'uid' => [$uid, 0]], ['order' => ['uid' => true]]);
if (!DBA::isResult($item)) {
DI::mstdnError()->RecordNotFound();
}
@ -51,6 +51,18 @@ class Bookmark extends BaseApi
DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Only starting posts can be bookmarked'));
}
if ($item['uid'] == 0) {
$stored = Item::storeForUserByUriId($this->parameters['id'], $uid);
if (!empty($stored)) {
$item = Post::selectFirst(['id', 'gravity'], ['id' => $stored]);
if (!DBA::isResult($item)) {
DI::mstdnError()->RecordNotFound();
}
} else {
DI::mstdnError()->RecordNotFound();
}
}
Item::update(['starred' => true], ['id' => $item['id']]);
System::jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid)->toArray());

View file

@ -42,7 +42,7 @@ class Unbookmark extends BaseApi
DI::mstdnError()->UnprocessableEntity();
}
$item = Post::selectFirstForUser($uid, ['id', 'gravity'], ['uri-id' => $this->parameters['id'], 'uid' => [$uid, 0]]);
$item = Post::selectFirst(['uid', 'id', 'gravity'], ['uri-id' => $this->parameters['id'], 'uid' => [$uid, 0]], ['order' => ['uid' => true]]);
if (!DBA::isResult($item)) {
DI::mstdnError()->RecordNotFound();
}
@ -51,6 +51,18 @@ class Unbookmark extends BaseApi
DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Only starting posts can be unbookmarked'));
}
if ($item['uid'] == 0) {
$stored = Item::storeForUserByUriId($this->parameters['id'], $uid);
if (!empty($stored)) {
$item = Post::selectFirst(['id', 'gravity'], ['id' => $stored]);
if (!DBA::isResult($item)) {
DI::mstdnError()->RecordNotFound();
}
} else {
DI::mstdnError()->RecordNotFound();
}
}
Item::update(['starred' => false], ['id' => $item['id']]);
System::jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid)->toArray());

View file

@ -53,13 +53,13 @@ class Favorites extends BaseApi
$start = max(0, ($page - 1) * $count);
$condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `id` > ? AND `starred`",
$condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `uri-id` > ? AND `starred`",
$uid, GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$params = ['order' => ['uri-id' => true], 'limit' => [$start, $count]];
if ($max_id > 0) {
$condition[0] .= " AND `id` <= ?";
$condition[0] .= " AND `uri-id` <= ?";
$condition[] = $max_id;
}

View file

@ -23,6 +23,7 @@ namespace Friendica\Module\Api\Twitter\Favorites;
use Friendica\DI;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException\BadRequestException;
@ -42,9 +43,14 @@ class Create extends BaseApi
throw new BadRequestException('Item id not specified');
}
Item::performActivity($id, 'like', $uid);
$post = Post::selectFirst(['id'], ['uri-id' => $request['id'], 'uid' => [0, $uid]], ['order' => ['uid' => true]]);
if (empty($post['id'])) {
throw new BadRequestException('Item id not found');
}
$status_info = DI::twitterStatus()->createFromItemId($id, $uid)->toArray();
Item::performActivity($post['id'], 'like', $uid);
$status_info = DI::twitterStatus()->createFromUriId($id, $uid)->toArray();
$this->response->exit('status', ['status' => $status_info], $this->parameters['extension'] ?? null);
}

View file

@ -23,6 +23,7 @@ namespace Friendica\Module\Api\Twitter\Favorites;
use Friendica\DI;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException\BadRequestException;
@ -42,9 +43,14 @@ class Destroy extends BaseApi
throw new BadRequestException('Item id not specified');
}
Item::performActivity($id, 'unlike', $uid);
$post = Post::selectFirst(['id'], ['uri-id' => $request['id'], 'uid' => [0, $uid]], ['order' => ['uid' => true]]);
if (empty($post['id'])) {
throw new BadRequestException('Item id not found');
}
$status_info = DI::twitterStatus()->createFromItemId($id, $uid)->toArray();
Item::performActivity($post['id'], 'unlike', $uid);
$status_info = DI::twitterStatus()->createFromUriId($id, $uid)->toArray();
$this->response->exit('status', ['status' => $status_info], $this->parameters['extension'] ?? null);
}

View file

@ -22,13 +22,18 @@
namespace Friendica\Module\Api\Twitter\Friendships;
use Exception;
use Friendica\App;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\DI;
use Friendica\Factory\Api\Twitter\User as TwitterUser;
use Friendica\Model\Contact;
use Friendica\Model\User;
use Friendica\Module\Api\ApiResponse;
use Friendica\Module\Api\Twitter\ContactEndpoint;
use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
/**
* Unfollow Contact
@ -37,6 +42,16 @@ use Friendica\Network\HTTPException;
*/
class Destroy extends ContactEndpoint
{
/** @var TwitterUser */
private $twitterUser;
public function __construct(App $app, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, ApiResponse $response, TwitterUser $twitterUser, array $server, array $parameters = [])
{
parent::__construct($app, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->twitterUser = $twitterUser;
}
protected function post(array $request = [])
{
BaseApi::checkAllowedScope(BaseApi::SCOPE_WRITE);
@ -66,18 +81,9 @@ class Destroy extends ContactEndpoint
$user = $this->twitterUser->createFromContactId($contact_id, $uid, true)->toArray();
try {
$result = Contact::terminateFriendship($owner, $contact);
if ($result === null) {
Logger::notice(BaseApi::LOG_PREFIX . 'Not supported for {network}', ['module' => 'api', 'action' => 'friendships_destroy', 'network' => $contact['network']]);
throw new HTTPException\ExpectationFailedException('Unfollowing is currently not supported by this contact\'s network.');
}
if ($result === false) {
throw new HTTPException\ServiceUnavailableException('Unable to unfollow this contact, please retry in a few minutes or contact your administrator.');
}
Contact::unfollow($contact);
} catch (Exception $e) {
Logger::error(BaseApi::LOG_PREFIX . $e->getMessage(), ['owner' => $owner, 'contact' => $contact]);
Logger::error(BaseApi::LOG_PREFIX . $e->getMessage(), ['contact' => $contact]);
throw new HTTPException\InternalServerErrorException('Unable to unfollow this contact, please contact your administrator');
}

View file

@ -46,32 +46,32 @@ class Incoming extends ContactEndpoint
$max_id = $this->getRequestValue($request, 'max_id', 0, 0);
$min_id = $this->getRequestValue($request, 'min_id', 0, 0);
$params = ['order' => ['cid' => true], 'limit' => $count];
$params = ['order' => ['contact-id' => true], 'limit' => $count];
$condition = ['uid' => $uid, 'pending' => true];
$condition = ["`uid` = ? AND NOT `blocked` AND NOT `ignore` AND `contact-id` != 0 AND (`suggest-cid` = 0 OR `suggest-cid` IS NULL)", $uid];
$total_count = (int)DBA::count('user-contact', $condition);
$total_count = (int)DBA::count('intro', $condition);
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`cid` < ?", $max_id]);
$condition = DBA::mergeConditions($condition, ["`contact-id` < ?", $max_id]);
}
if (!empty($since_id)) {
$condition = DBA::mergeConditions($condition, ["`cid` > ?", $since_id]);
$condition = DBA::mergeConditions($condition, ["`contact-id` > ?", $since_id]);
}
if (!empty($min_id)) {
$condition = DBA::mergeConditions($condition, ["`cid` > ?", $min_id]);
$condition = DBA::mergeConditions($condition, ["`contact-id` > ?", $min_id]);
$params['order'] = ['cid'];
$params['order'] = ['contact-id'];
}
$ids = [];
$contacts = DBA::select('user-contact', ['cid'], $condition, $params);
$contacts = DBA::select('intro', ['contact-id'], $condition, $params);
while ($contact = DBA::fetch($contacts)) {
self::setBoundaries($contact['cid']);
$ids[] = $contact['cid'];
self::setBoundaries($contact['contact-id']);
$ids[] = $contact['contact-id'];
}
DBA::close($contacts);

View file

@ -56,7 +56,7 @@ class Ownership extends BaseApi
BaseApi::checkAllowedScope(BaseApi::SCOPE_READ);
$uid = BaseApi::getCurrentUserID();
$groups = $this->dba->select('group', [], ['deleted' => false, 'uid' => $uid]);
$groups = $this->dba->select('group', [], ['deleted' => false, 'uid' => $uid, 'cid' => null]);
// loop through all groups
$lists = [];

View file

@ -78,10 +78,10 @@ class Statuses extends BaseApi
$groups = $this->dba->selectToArray('group_member', ['contact-id'], ['gid' => $request['list_id']]);
$gids = array_column($groups, 'contact-id');
$condition = ['uid' => $uid, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'contact-id' => $gids];
$condition = DBA::mergeConditions($condition, ["`id` > ?", $since_id]);
$condition = DBA::mergeConditions($condition, ["`uri-id` > ?", $since_id]);
if ($max_id > 0) {
$condition[0] .= " AND `id` <= ?";
$condition[0] .= " AND `uri-id` <= ?";
$condition[] = $max_id;
}
if ($exclude_replies) {
@ -89,11 +89,11 @@ class Statuses extends BaseApi
$condition[] = GRAVITY_PARENT;
}
if ($conversation_id > 0) {
$condition[0] .= " AND `parent` = ?";
$condition[0] .= " AND `parent-uri-id` = ?";
$condition[] = $conversation_id;
}
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$params = ['order' => ['uri-id' => true], 'limit' => [$start, $count]];
$statuses = Post::selectForUser($uid, [], $condition, $params);
$items = [];

View file

@ -59,10 +59,10 @@ class Tweets extends BaseApi
$start = max(0, ($page - 1) * $count);
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$params = ['order' => ['uri-id' => true], 'limit' => [$start, $count]];
if (preg_match('/^#(\w+)$/', $searchTerm, $matches) === 1 && isset($matches[1])) {
$searchTerm = $matches[1];
$condition = ["`iid` > ? AND `name` = ? AND (NOT `private` OR (`private` AND `uid` = ?))", $since_id, $searchTerm, $uid];
$condition = ["`uri-id` > ? AND `name` = ? AND (NOT `private` OR (`private` AND `uid` = ?))", $since_id, $searchTerm, $uid];
$tags = DBA::select('tag-search-view', ['uri-id'], $condition);
$uriids = [];
@ -83,13 +83,13 @@ class Tweets extends BaseApi
$params['group_by'] = ['uri-id'];
} else {
$condition = ["`id` > ?
$condition = ["`uri-id` > ?
" . ($exclude_replies ? " AND `gravity` = " . GRAVITY_PARENT : ' ') . "
AND (`uid` = 0 OR (`uid` = ? AND NOT `global`))
AND `body` LIKE CONCAT('%',?,'%')",
$since_id, $uid, $_REQUEST['q']];
if ($max_id > 0) {
$condition[0] .= ' AND `id` <= ?';
$condition[0] .= ' AND `uri-id` <= ?';
$condition[] = $max_id;
}
}

View file

@ -25,6 +25,7 @@ use Friendica\Module\BaseApi;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Network\HTTPException\BadRequestException;
/**
@ -45,13 +46,18 @@ class Destroy extends BaseApi
throw new BadRequestException('An id is missing.');
}
$post = Post::selectFirst(['id'], ['uri-id' => $request['id'], 'uid' => [0, $uid]], ['order' => ['uid' => true]]);
if (empty($post['id'])) {
throw new BadRequestException('Item id not found');
}
$this->logger->notice('API: api_statuses_destroy: ' . $id);
$include_entities = $this->getRequestValue($request, 'include_entities', false);
$ret = DI::twitterStatus()->createFromItemId($id, $uid, $include_entities)->toArray();
$ret = DI::twitterStatus()->createFromUriId($id, $uid, $include_entities)->toArray();
Item::deleteForUser(['id' => $id], $uid);
Item::deleteForUser(['id' => $post['id']], $uid);
$this->response->exit('status', ['status' => $ret], $this->parameters['extension'] ?? null, Contact::getPublicIdByUserId($uid));
}

View file

@ -53,11 +53,11 @@ class HomeTimeline extends BaseApi
$start = max(0, ($page - 1) * $count);
$condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `id` > ?",
$condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `uri-id` > ?",
$uid, GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
if ($max_id > 0) {
$condition[0] .= " AND `id` <= ?";
$condition[0] .= " AND `uri-id` <= ?";
$condition[] = $max_id;
}
if ($exclude_replies) {
@ -65,11 +65,11 @@ class HomeTimeline extends BaseApi
$condition[] = GRAVITY_PARENT;
}
if ($conversation_id > 0) {
$condition[0] .= " AND `parent` = ?";
$condition[0] .= " AND `parent-uri-id` = ?";
$condition[] = $conversation_id;
}
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$params = ['order' => ['uri-id' => true], 'limit' => [$start, $count]];
$statuses = Post::selectForUser($uid, [], $condition, $params);
$ret = [];

View file

@ -52,7 +52,7 @@ class Mentions extends BaseApi
$query = "`gravity` IN (?, ?) AND `uri-id` IN
(SELECT `uri-id` FROM `post-user-notification` WHERE `uid` = ? AND `notification-type` & ? != 0 ORDER BY `uri-id`)
AND (`uid` = 0 OR (`uid` = ? AND NOT `global`)) AND `id` > ?";
AND (`uid` = 0 OR (`uid` = ? AND NOT `global`)) AND `uri-id` > ?";
$condition = [
GRAVITY_PARENT, GRAVITY_COMMENT,
@ -64,13 +64,13 @@ class Mentions extends BaseApi
];
if ($max_id > 0) {
$query .= " AND `id` <= ?";
$query .= " AND `uri-id` <= ?";
$condition[] = $max_id;
}
array_unshift($condition, $query);
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$params = ['order' => ['uri-id' => true], 'limit' => [$start, $count]];
$statuses = Post::selectForUser($uid, [], $condition, $params);
$ret = [];

View file

@ -46,15 +46,15 @@ class NetworkPublicTimeline extends BaseApi
$start = max(0, ($page - 1) * $count);
$condition = ["`uid` = 0 AND `gravity` IN (?, ?) AND `id` > ? AND `private` = ?",
$condition = ["`uid` = 0 AND `gravity` IN (?, ?) AND `uri-id` > ? AND `private` = ?",
GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, Item::PUBLIC];
if ($max_id > 0) {
$condition[0] .= " AND `id` <= ?";
$condition[0] .= " AND `uri-id` <= ?";
$condition[] = $max_id;
}
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$params = ['order' => ['uri-id' => true], 'limit' => [$start, $count]];
$statuses = Post::selectForUser($uid, Item::DISPLAY_FIELDLIST, $condition, $params);
$ret = [];

View file

@ -52,30 +52,30 @@ class PublicTimeline extends BaseApi
$start = max(0, ($page - 1) * $count);
if ($exclude_replies && !$conversation_id) {
$condition = ["`gravity` = ? AND `id` > ? AND `private` = ? AND `wall` AND NOT `author-hidden`",
$condition = ["`gravity` = ? AND `uri-id` > ? AND `private` = ? AND `wall` AND NOT `author-hidden`",
GRAVITY_PARENT, $since_id, Item::PUBLIC];
if ($max_id > 0) {
$condition[0] .= " AND `id` <= ?";
$condition[0] .= " AND `uri-id` <= ?";
$condition[] = $max_id;
}
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$params = ['order' => ['uri-id' => true], 'limit' => [$start, $count]];
$statuses = Post::selectForUser($uid, [], $condition, $params);
} else {
$condition = ["`gravity` IN (?, ?) AND `id` > ? AND `private` = ? AND `wall` AND `origin` AND NOT `author-hidden`",
$condition = ["`gravity` IN (?, ?) AND `uri-id` > ? AND `private` = ? AND `wall` AND `origin` AND NOT `author-hidden`",
GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, Item::PUBLIC];
if ($max_id > 0) {
$condition[0] .= " AND `id` <= ?";
$condition[0] .= " AND `uri-id` <= ?";
$condition[] = $max_id;
}
if ($conversation_id > 0) {
$condition[0] .= " AND `parent` = ?";
$condition[0] .= " AND `parent-uri-id` = ?";
$condition[] = $conversation_id;
}
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$params = ['order' => ['uri-id' => true], 'limit' => [$start, $count]];
$statuses = Post::selectForUser($uid, [], $condition, $params);
}

View file

@ -50,8 +50,8 @@ class Retweet extends BaseApi
throw new BadRequestException('An id is missing.');
}
$fields = ['uri-id', 'network', 'body', 'title', 'author-name', 'author-link', 'author-avatar', 'guid', 'created', 'plink'];
$item = Post::selectFirst($fields, ['id' => $id, 'private' => [Item::PUBLIC, Item::UNLISTED]]);
$fields = ['id', 'uri-id', 'network', 'body', 'title', 'author-name', 'author-link', 'author-avatar', 'guid', 'created', 'plink'];
$item = Post::selectFirst($fields, ['uri-id' => $id, 'uid' => [0, $uid], 'private' => [Item::PUBLIC, Item::UNLISTED]], ['order' => ['uid' => true]]);
if (DBA::isResult($item) && !empty($item['body'])) {
if (in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::TWITTER])) {
@ -59,7 +59,7 @@ class Retweet extends BaseApi
throw new InternalServerErrorException();
}
$item_id = $id;
$item_id = $item['id'];
} else {
$item_id = Diaspora::performReshare($item['uri-id'], $uid);
}

View file

@ -52,23 +52,18 @@ class Show extends BaseApi
$conversation = !empty($request['conversation']);
// try to fetch the item for the local user - or the public item, if there is no local one
$uri_item = Post::selectFirst(['uri-id'], ['id' => $id]);
if (!DBA::isResult($uri_item)) {
throw new BadRequestException(sprintf("There is no status with the id %d", $id));
}
$item = Post::selectFirst(['id'], ['uri-id' => $uri_item['uri-id'], 'uid' => [0, $uid]], ['order' => ['uid' => true]]);
$item = Post::selectFirst(['id'], ['uri-id' => $id, 'uid' => [0, $uid]], ['order' => ['uid' => true]]);
if (!DBA::isResult($item)) {
throw new BadRequestException(sprintf("There is no status with the uri-id %d for the given user.", $uri_item['uri-id']));
throw new BadRequestException(sprintf("There is no status with the uri-id %d for the given user.", $id));
}
$id = $item['id'];
$item_id = $item['id'];
if ($conversation) {
$condition = ['parent' => $id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]];
$params = ['order' => ['id' => true]];
$condition = ['parent' => $item_id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]];
$params = ['order' => ['uri-id' => true]];
} else {
$condition = ['id' => $id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]];
$condition = ['id' => $item_id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]];
$params = [];
}

View file

@ -21,9 +21,9 @@
namespace Friendica\Module\Api\Twitter\Statuses;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML;
use Friendica\Content\Text\Markdown;
use Friendica\Core\Protocol;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
@ -78,17 +78,12 @@ class Update extends BaseApi
$body = Markdown::toBBCode($request['status']);
}
// Avoids potential double expansion of existing links
$body = BBCode::performWithEscapedTags($body, ['url'], function ($body) {
return BBCode::expandTags($body);
});
$item = [];
$item['network'] = Protocol::DFRN;
$item['uid'] = $uid;
$item['verb'] = Activity::POST;
$item['contact-id'] = $owner['id'];
$item['author-id'] = Contact::getPublicIdByUserId($uid);
$item['owner-id'] = $item['author-id'];
$item['author-id'] = $item['owner-id'] = Contact::getPublicIdByUserId($uid);
$item['title'] = $request['title'];
$item['body'] = $body;
$item['app'] = $request['source'];
@ -115,7 +110,7 @@ class Update extends BaseApi
}
if ($request['in_reply_to_status_id']) {
$parent = Post::selectFirst(['uri'], ['id' => $request['in_reply_to_status_id'], 'uid' => [0, $uid]]);
$parent = Post::selectFirst(['uri'], ['uri-id' => $request['in_reply_to_status_id'], 'uid' => [0, $uid]]);
$item['thr-parent'] = $parent['uri'];
$item['gravity'] = GRAVITY_COMMENT;
@ -127,6 +122,8 @@ class Update extends BaseApi
$item['object-type'] = Activity\ObjectType::NOTE;
}
$item = DI::contentItem()->expandTags($item);
if (!empty($request['media_ids'])) {
$ids = explode(',', $request['media_ids']);
} elseif (!empty($_FILES['media'])) {

View file

@ -53,7 +53,7 @@ class UserTimeline extends BaseApi
$start = max(0, ($page - 1) * $count);
$condition = ["(`uid` = ? OR (`uid` = ? AND NOT `global`)) AND `gravity` IN (?, ?) AND `id` > ? AND `author-id` = ?",
$condition = ["(`uid` = ? OR (`uid` = ? AND NOT `global`)) AND `gravity` IN (?, ?) AND `uri-id` > ? AND `author-id` = ?",
0, $uid, GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, $cid];
if ($exclude_replies) {
@ -62,15 +62,15 @@ class UserTimeline extends BaseApi
}
if ($conversation_id > 0) {
$condition[0] .= " AND `parent` = ?";
$condition[0] .= " AND `parent-uri-id` = ?";
$condition[] = $conversation_id;
}
if ($max_id > 0) {
$condition[0] .= " AND `id` <= ?";
$condition[0] .= " AND `uri-id` <= ?";
$condition[] = $max_id;
}
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$params = ['order' => ['uri-id' => true], 'limit' => [$start, $count]];
$statuses = Post::selectForUser($uid, [], $condition, $params);
$ret = [];

View file

@ -29,7 +29,7 @@ use Friendica\Content\Pager;
use Friendica\Core\L10n;
use Friendica\Core\Renderer;
use Friendica\Core\System;
use Friendica\Navigation\Notifications\ValueObject\FormattedNotification;
use Friendica\Navigation\Notifications\ValueObject\FormattedNotify;
use Friendica\Network\HTTPException\ForbiddenException;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
@ -43,29 +43,29 @@ abstract class BaseNotifications extends BaseModule
{
/** @var array Array of URL parameters */
const URL_TYPES = [
FormattedNotification::NETWORK => 'network',
FormattedNotification::SYSTEM => 'system',
FormattedNotification::HOME => 'home',
FormattedNotification::PERSONAL => 'personal',
FormattedNotification::INTRO => 'intros',
FormattedNotify::NETWORK => 'network',
FormattedNotify::SYSTEM => 'system',
FormattedNotify::HOME => 'home',
FormattedNotify::PERSONAL => 'personal',
FormattedNotify::INTRO => 'intros',
];
/** @var array Array of the allowed notifications and their printable name */
const PRINT_TYPES = [
FormattedNotification::NETWORK => 'Network',
FormattedNotification::SYSTEM => 'System',
FormattedNotification::HOME => 'Home',
FormattedNotification::PERSONAL => 'Personal',
FormattedNotification::INTRO => 'Introductions',
FormattedNotify::NETWORK => 'Network',
FormattedNotify::SYSTEM => 'System',
FormattedNotify::HOME => 'Home',
FormattedNotify::PERSONAL => 'Personal',
FormattedNotify::INTRO => 'Introductions',
];
/** @var array The array of access keys for notification pages */
const ACCESS_KEYS = [
FormattedNotification::NETWORK => 'w',
FormattedNotification::SYSTEM => 'y',
FormattedNotification::HOME => 'h',
FormattedNotification::PERSONAL => 'r',
FormattedNotification::INTRO => 'i',
FormattedNotify::NETWORK => 'w',
FormattedNotify::SYSTEM => 'y',
FormattedNotify::HOME => 'h',
FormattedNotify::PERSONAL => 'r',
FormattedNotify::INTRO => 'i',
];
/** @var int The default count of items per page */

View file

@ -558,7 +558,7 @@ class Contact extends BaseModule
'details' => $contact['location'],
'tags' => $contact['keywords'],
'about' => $contact['about'],
'account_type' => Model\Contact::getAccountType($contact),
'account_type' => Model\Contact::getAccountType($contact['contact-type']),
'sparkle' => $sparkle,
'itemurl' => ($contact['addr'] ?? '') ?: $contact['url'],
'network' => ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']),

View file

@ -101,7 +101,7 @@ class Hovercard extends BaseModule
'network_link' => Strings::formatNetworkName($contact['network'], $contact['url']),
'tags' => $contact['keywords'],
'bd' => $contact['bd'] <= DBA::NULL_DATE ? '' : $contact['bd'],
'account_type' => Contact::getAccountType($contact),
'account_type' => Contact::getAccountType($contact['contact-type']),
'actions' => $actions,
],
]);

View file

@ -364,7 +364,7 @@ class Profile extends BaseModule
'$url' => $url,
'$profileurllabel' => $this->t('Profile URL'),
'$profileurl' => $contact['url'],
'$account_type' => Contact::getAccountType($contact),
'$account_type' => Contact::getAccountType($contact['contact-type']),
'$location' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['location']),
'$location_label' => $this->t('Location:'),
'$xmpp' => BBCode::convertForUriId($contact['uri-id'] ?? 0, $contact['xmpp']),

View file

@ -38,7 +38,10 @@ use Psr\Log\LoggerInterface;
class Revoke extends BaseModule
{
/** @var array */
/**
* User-specific contact (uid != 0) array
* @var array
*/
protected $contact;
/** @var Database */
@ -82,14 +85,9 @@ class Revoke extends BaseModule
self::checkFormSecurityTokenRedirectOnError('contact/' . $this->parameters['id'], 'contact_revoke');
$result = Model\Contact::revokeFollow($this->contact);
if ($result === true) {
notice($this->t('Follow was successfully revoked.'));
} elseif ($result === null) {
notice($this->t('Follow was successfully revoked, however the remote contact won\'t be aware of this revokation.'));
} else {
notice($this->t('Unable to revoke follow, please try again later or contact the administrator.'));
}
Model\Contact::revokeFollow($this->contact);
notice($this->t('Follow was successfully revoked.'));
$this->baseUrl->redirect('contact/' . $this->parameters['id']);
}

View file

@ -119,7 +119,7 @@ class Network extends BaseModule
if (self::$forumContactId) {
// If self::$forumContactId belongs to a communitity forum or a privat goup,.add a mention to the status editor
$condition = ["`id` = ? AND (`forum` OR `prv`)", self::$forumContactId];
$condition = ["`id` = ? AND `contact-type` = ?", self::$forumContactId, Contact::TYPE_COMMUNITY];
$contact = DBA::selectFirst('contact', ['addr'], $condition);
if (!empty($contact['addr'])) {
$content = '!' . $contact['addr'];

View file

@ -114,6 +114,10 @@ class ActivityPubConversion extends BaseModule
$object_data['thread-completion'] = $activity['thread-completion'];
}
if (!empty($activity['completion-mode'])) {
$object_data['completion-mode'] = $activity['completion-mode'];
}
$results[] = [
'title' => DI::l10n()->t('Object data'),
'content' => visible_whitespace(var_export($object_data, true))

View file

@ -78,7 +78,7 @@ class Receive extends BaseModule
$this->logger->info('Diaspora: Dispatching.');
Diaspora::dispatchPublic($msg);
Diaspora::dispatchPublic($msg, Diaspora::PUSHED);
}
/**
@ -92,8 +92,19 @@ class Receive extends BaseModule
$this->logger->info('Diaspora: Receiving post.');
$importer = User::getByGuid($this->parameters['guid']);
if (empty($importer)) {
// We haven't found the user.
// To avoid the remote system trying again we send the message that we accepted the content.
throw new HTTPException\AcceptedException();
}
$msg = $this->decodePost(false, $importer['prvkey'] ?? '');
if ($importer['account-type'] == User::ACCOUNT_TYPE_COMMUNITY) {
// Communities aren't working with the Diaspora protoccol
// We throw an "accepted" here, so that the sender doesn't repeat the delivery
throw new HTTPException\AcceptedException();
}
$msg = $this->decodePost(false, $importer['prvkey']);
$this->logger->info('Diaspora: Dispatching.');

View file

@ -165,7 +165,7 @@ class Directory extends BaseModule
'img_hover' => $contact['name'],
'name' => $contact['name'],
'details' => $details,
'account_type' => Model\Contact::getAccountType($contact),
'account_type' => Model\Contact::getAccountType($contact['contact-type']),
'profile' => $profile,
'location' => $location_e,
'tags' => $contact['pub_keywords'],

View file

@ -21,18 +21,45 @@
namespace Friendica\Module\Notifications;
use Friendica\App;
use Friendica\BaseModule;
use Friendica\Contact\Introduction\Repository\Introduction;
use Friendica\Core\L10n;
use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Module\Response;
use Friendica\Module\Security\Login;
use Friendica\Navigation\Notifications\Factory;
use Friendica\Navigation\Notifications\Repository;
use Friendica\Network\HTTPException;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
/**
* Interacting with the /notification command
*/
class Notification extends BaseModule
{
/** @var Introduction */
private $introductionRepo;
/** @var Repository\Notification */
private $notificationRepo;
/** @var Repository\Notify */
private $notifyRepo;
/** @var IManagePersonalConfigValues */
private $pconfig;
/** @var Factory\Notification */
private $notificationFactory;
public function __construct(Introduction $introductionRepo, Repository\Notification $notificationRepo, Factory\Notification $notificationFactory, Repository\Notify $notifyRepo, IManagePersonalConfigValues $pconfig, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->introductionRepo = $introductionRepo;
$this->notificationRepo = $notificationRepo;
$this->notificationFactory = $notificationFactory;
$this->notifyRepo = $notifyRepo;
$this->pconfig = $pconfig;
}
/**
* {@inheritDoc}
*
@ -45,26 +72,26 @@ class Notification extends BaseModule
protected function post(array $request = [])
{
if (!local_user()) {
throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.'));
throw new HTTPException\UnauthorizedException($this->l10n->t('Permission denied.'));
}
$request_id = $this->parameters['id'] ?? false;
if ($request_id) {
$intro = DI::intro()->selectOneById($request_id, local_user());
$intro = $this->introductionRepo->selectOneById($request_id, local_user());
switch ($_POST['submit']) {
case DI::l10n()->t('Discard'):
case $this->l10n->t('Discard'):
Contact\Introduction::discard($intro);
DI::intro()->delete($intro);
$this->introductionRepo->delete($intro);
break;
case DI::l10n()->t('Ignore'):
case $this->l10n->t('Ignore'):
$intro->ignore();
DI::intro()->save($intro);
$this->introductionRepo->save($intro);
break;
}
DI::baseUrl()->redirect('notifications/intros');
$this->baseUrl->redirect('notifications/intros');
}
}
@ -76,15 +103,15 @@ class Notification extends BaseModule
protected function rawContent(array $request = [])
{
if (!local_user()) {
throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.'));
throw new HTTPException\UnauthorizedException($this->l10n->t('Permission denied.'));
}
if (DI::args()->get(1) === 'mark' && DI::args()->get(2) === 'all') {
if ($this->args->get(1) === 'mark' && $this->args->get(2) === 'all') {
try {
DI::notification()->setAllSeenForUser(local_user());
$success = DI::notify()->setAllSeenForUser(local_user());
$this->notificationRepo->setAllSeenForUser(local_user());
$success = $this->notifyRepo->setAllSeenForUser(local_user());
} catch (\Exception $e) {
DI::logger()->warning('set all seen failed.', ['exception' => $e]);
$this->logger->warning('set all seen failed.', ['exception' => $e]);
$success = false;
}
@ -104,38 +131,71 @@ class Notification extends BaseModule
protected function content(array $request = []): string
{
if (!local_user()) {
notice(DI::l10n()->t('You must be logged in to show this page.'));
notice($this->l10n->t('You must be logged in to show this page.'));
return Login::form();
}
$request_id = $this->parameters['id'] ?? false;
if ($request_id) {
$Notify = DI::notify()->selectOneById($request_id);
if ($Notify->uid !== local_user()) {
throw new HTTPException\ForbiddenException();
}
if (DI::pConfig()->get(local_user(), 'system', 'detailed_notif')) {
$Notify->setSeen();
DI::notify()->save($Notify);
} else {
if ($Notify->uriId) {
DI::notification()->setAllSeenForUser($Notify->uid, ['target-uri-id' => $Notify->uriId]);
}
DI::notify()->setAllSeenForRelatedNotify($Notify);
}
if ((string)$Notify->link) {
System::externalRedirect($Notify->link);
}
DI::baseUrl()->redirect();
if (isset($this->parameters['notify_id'])) {
$this->handleNotify($this->parameters['notify_id']);
} elseif (isset($this->parameters['id'])) {
$this->handleNotification($this->parameters['id']);
}
DI::baseUrl()->redirect('notifications/system');
$this->baseUrl->redirect('notifications/system');
return '';
}
private function handleNotify(int $notifyId)
{
$Notify = $this->notifyRepo->selectOneById($notifyId);
if ($Notify->uid !== local_user()) {
throw new HTTPException\ForbiddenException();
}
if ($this->pconfig->get(local_user(), 'system', 'detailed_notif')) {
$Notify->setSeen();
$this->notifyRepo->save($Notify);
} else {
if ($Notify->uriId) {
$this->notificationRepo->setAllSeenForUser($Notify->uid, ['target-uri-id' => $Notify->uriId]);
}
$this->notifyRepo->setAllSeenForRelatedNotify($Notify);
}
if ((string)$Notify->link) {
System::externalRedirect($Notify->link);
}
$this->baseUrl->redirect();
}
private function handleNotification(int $notificationId)
{
$Notification = $this->notificationRepo->selectOneById($notificationId);
if ($Notification->uid !== local_user()) {
throw new HTTPException\ForbiddenException();
}
if ($this->pconfig->get(local_user(), 'system', 'detailed_notif')) {
$Notification->setSeen();
$this->notificationRepo->save($Notification);
} else {
if ($Notification->parentUriId) {
$this->notificationRepo->setAllSeenForUser($Notification->uid, ['parent-uri-id' => $Notification->parentUriId]);
} else {
$Notification->setSeen();
$this->notificationRepo->save($Notification);
}
}
$message = $this->notificationFactory->getMessageFromNotification($Notification);
if ($message['link']) {
System::externalRedirect($message['link']);
}
$this->baseUrl->redirect();
}
}

View file

@ -28,7 +28,7 @@ use Friendica\Core\L10n;
use Friendica\Core\Renderer;
use Friendica\Module\BaseNotifications;
use Friendica\Module\Response;
use Friendica\Navigation\Notifications\ValueObject\FormattedNotification;
use Friendica\Navigation\Notifications\ValueObject\FormattedNotify;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
@ -41,14 +41,14 @@ use Psr\Log\LoggerInterface;
*/
class Notifications extends BaseNotifications
{
/** @var \Friendica\Navigation\Notifications\Factory\FormattedNotification */
protected $formattedNotificationFactory;
/** @var \Friendica\Navigation\Notifications\Factory\FormattedNotify */
protected $formattedNotifyFactory;
public function __construct(L10n $l10n, App\BaseURL $baseUrl, Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, \Friendica\Navigation\Notifications\Factory\FormattedNotification $formattedNotificationFactory, array $server, array $parameters = [])
public function __construct(L10n $l10n, App\BaseURL $baseUrl, Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, \Friendica\Navigation\Notifications\Factory\FormattedNotify $formattedNotifyFactory, array $server, array $parameters = [])
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->formattedNotificationFactory = $formattedNotificationFactory;
$this->formattedNotifyFactory = $formattedNotifyFactory;
}
/**
@ -59,30 +59,30 @@ class Notifications extends BaseNotifications
$notificationHeader = '';
$notifications = [];
$factory = $this->formattedNotificationFactory;
$factory = $this->formattedNotifyFactory;
if (($this->args->get(1) == 'network')) {
$notificationHeader = $this->t('Network Notifications');
$notifications = [
'ident' => FormattedNotification::NETWORK,
'ident' => FormattedNotify::NETWORK,
'notifications' => $factory->getNetworkList($this->showAll, $this->firstItemNum, self::ITEMS_PER_PAGE),
];
} elseif (($this->args->get(1) == 'system')) {
$notificationHeader = $this->t('System Notifications');
$notifications = [
'ident' => FormattedNotification::SYSTEM,
'ident' => FormattedNotify::SYSTEM,
'notifications' => $factory->getSystemList($this->showAll, $this->firstItemNum, self::ITEMS_PER_PAGE),
];
} elseif (($this->args->get(1) == 'personal')) {
$notificationHeader = $this->t('Personal Notifications');
$notifications = [
'ident' => FormattedNotification::PERSONAL,
'ident' => FormattedNotify::PERSONAL,
'notifications' => $factory->getPersonalList($this->showAll, $this->firstItemNum, self::ITEMS_PER_PAGE),
];
} elseif (($this->args->get(1) == 'home')) {
$notificationHeader = $this->t('Home Notifications');
$notifications = [
'ident' => FormattedNotification::HOME,
'ident' => FormattedNotify::HOME,
'notifications' => $factory->getHomeList($this->showAll, $this->firstItemNum, self::ITEMS_PER_PAGE),
];
} else {
@ -120,7 +120,7 @@ class Notifications extends BaseNotifications
];
// Loop trough ever notification This creates an array with the output html for each
// notification and apply the correct template according to the notificationtype (label).
/** @var FormattedNotification $Notification */
/** @var FormattedNotify $Notification */
foreach ($notifications['notifications'] as $Notification) {
$notificationArray = $Notification->toArray();

View file

@ -0,0 +1,295 @@
<?php
/**
* @copyright Copyright (C) 2010-2022, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Notifications;
use Friendica\App;
use Friendica\BaseModule;
use Friendica\Contact\Introduction\Repository\Introduction;
use Friendica\Content\ForumManager;
use Friendica\Core\Cache\Enum\Duration;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Group;
use Friendica\Model\Post;
use Friendica\Model\Verb;
use Friendica\Module\Register;
use Friendica\Module\Response;
use Friendica\Navigation\Notifications\Entity;
use Friendica\Navigation\Notifications\Exception\NoMessageException;
use Friendica\Navigation\Notifications\Factory;
use Friendica\Navigation\Notifications\Repository;
use Friendica\Navigation\Notifications\ValueObject;
use Friendica\Protocol\Activity;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Profiler;
use GuzzleHttp\Psr7\Uri;
use Psr\Log\LoggerInterface;
class Ping extends BaseModule
{
/** @var Repository\Notification */
private $notificationRepo;
/** @var Introduction */
private $introductionRepo;
/** @var Factory\FormattedNavNotification */
private $formattedNavNotification;
public function __construct(Repository\Notification $notificationRepo, Introduction $introductionRepo, Factory\FormattedNavNotification $formattedNavNotification, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->notificationRepo = $notificationRepo;
$this->introductionRepo = $introductionRepo;
$this->formattedNavNotification = $formattedNavNotification;
}
protected function rawContent(array $request = [])
{
$regs = [];
$navNotifications = [];
$intro_count = 0;
$mail_count = 0;
$home_count = 0;
$network_count = 0;
$register_count = 0;
$sysnotify_count = 0;
$groups_unseen = [];
$forums_unseen = [];
$event_count = 0;
$today_event_count = 0;
$birthday_count = 0;
$today_birthday_count = 0;
if (local_user()) {
if (DI::pConfig()->get(local_user(), 'system', 'detailed_notif')) {
$notifications = $this->notificationRepo->selectDetailedForUser(local_user());
} else {
$notifications = $this->notificationRepo->selectDigestForUser(local_user());
}
$condition = [
"`unseen` AND `uid` = ? AND NOT `origin` AND (`vid` != ? OR `vid` IS NULL)",
local_user(), Verb::getID(Activity::FOLLOW)
];
$items = Post::selectForUser(local_user(), ['wall', 'uid', 'uri-id'], $condition, ['limit' => 1000]);
if (DBA::isResult($items)) {
$items_unseen = Post::toArray($items, false);
$arr = ['items' => $items_unseen];
Hook::callAll('network_ping', $arr);
foreach ($items_unseen as $item) {
if ($item['wall']) {
$home_count++;
} else {
$network_count++;
}
}
}
DBA::close($items);
if ($network_count) {
// Find out how unseen network posts are spread across groups
$group_counts = Group::countUnseen();
if (DBA::isResult($group_counts)) {
foreach ($group_counts as $group_count) {
if ($group_count['count'] > 0) {
$groups_unseen[] = $group_count;
}
}
}
$forum_counts = ForumManager::countUnseenItems();
if (DBA::isResult($forum_counts)) {
foreach ($forum_counts as $forum_count) {
if ($forum_count['count'] > 0) {
$forums_unseen[] = $forum_count;
}
}
}
}
$intros = $this->introductionRepo->selectForUser(local_user());
$intro_count = $intros->count();
$myurl = DI::baseUrl() . '/profile/' . DI::app()->getLoggedInUserNickname();
$mail_count = DBA::count('mail', ["`uid` = ? AND NOT `seen` AND `from-url` != ?", local_user(), $myurl]);
if (intval(DI::config()->get('config', 'register_policy')) === Register::APPROVE && DI::app()->isSiteAdmin()) {
$regs = \Friendica\Model\Register::getPending();
if (DBA::isResult($regs)) {
$register_count = count($regs);
}
}
$cachekey = 'ping:events:' . local_user();
$ev = DI::cache()->get($cachekey);
if (is_null($ev)) {
$ev = DBA::selectToArray('event', ['type', 'start'],
["`uid` = ? AND `start` < ? AND `finish` > ? AND NOT `ignore`",
local_user(), DateTimeFormat::utc('now + 7 days'), DateTimeFormat::utcNow()]);
if (DBA::isResult($ev)) {
DI::cache()->set($cachekey, $ev, Duration::HOUR);
}
}
if (DBA::isResult($ev)) {
$all_events = count($ev);
if ($all_events) {
$str_now = DateTimeFormat::localNow('Y-m-d');
foreach ($ev as $x) {
$bd = false;
if ($x['type'] === 'birthday') {
$birthday_count++;
$bd = true;
} else {
$event_count++;
}
if (DateTimeFormat::local($x['start'], 'Y-m-d') === $str_now) {
if ($bd) {
$today_birthday_count++;
} else {
$today_event_count++;
}
}
}
}
}
$navNotifications = array_map(function (Entity\Notification $notification) {
try {
return $this->formattedNavNotification->createFromNotification($notification);
} catch (NoMessageException $e) {
return null;
}
}, $notifications->getArrayCopy());
$navNotifications = array_filter($navNotifications);
$sysnotify_count = array_reduce($navNotifications, function (int $carry, ValueObject\FormattedNavNotification $navNotification) {
return $carry + ($navNotification->seen ? 0 : 1);
}, 0);
// merge all notification types in one array
foreach ($intros as $intro) {
$navNotifications[] = $this->formattedNavNotification->createFromIntro($intro);
}
if (DBA::isResult($regs)) {
if (count($regs) <= 1 || DI::pConfig()->get(local_user(), 'system', 'detailed_notif')) {
foreach ($regs as $reg) {
$navNotifications[] = $this->formattedNavNotification->createFromParams(
[
'name' => $reg['name'],
'url' => $reg['url'],
],
DI::l10n()->t('{0} requested registration'),
new \DateTime($reg['created'], new \DateTimeZone('UTC')),
new Uri(DI::baseUrl()->get(true) . '/admin/users/pending')
);
}
} else {
$navNotifications[] = $this->formattedNavNotification->createFromParams(
[
'name' => $regs[0]['name'],
'url' => $regs[0]['url'],
],
DI::l10n()->t('{0} and %d others requested registration', count($regs) - 1),
new \DateTime($regs[0]['created'], new \DateTimeZone('UTC')),
new Uri(DI::baseUrl()->get(true) . '/admin/users/pending')
);
}
}
// sort notifications by $[]['date']
$sort_function = function (ValueObject\FormattedNavNotification $a, ValueObject\FormattedNavNotification $b) {
$a = $a->toArray();
$b = $b->toArray();
// Unseen messages are kept at the top
if ($a['seen'] == $b['seen']) {
if ($a['timestamp'] == $b['timestamp']) {
return 0;
} else {
return $a['timestamp'] < $b['timestamp'] ? 1 : -1;
}
} else {
return $a['seen'] ? 1 : -1;
}
};
usort($navNotifications, $sort_function);
}
$sysmsgs = [];
$sysmsgs_info = [];
if (!empty($_SESSION['sysmsg'])) {
$sysmsgs = $_SESSION['sysmsg'];
unset($_SESSION['sysmsg']);
}
if (!empty($_SESSION['sysmsg_info'])) {
$sysmsgs_info = $_SESSION['sysmsg_info'];
unset($_SESSION['sysmsg_info']);
}
$notification_count = $sysnotify_count + $intro_count + $register_count;
$data = [];
$data['intro'] = $intro_count;
$data['mail'] = $mail_count;
$data['net'] = ($network_count < 1000) ? $network_count : '999+';
$data['home'] = ($home_count < 1000) ? $home_count : '999+';
$data['register'] = $register_count;
$data['events'] = $event_count;
$data['events-today'] = $today_event_count;
$data['birthdays'] = $birthday_count;
$data['birthdays-today'] = $today_birthday_count;
$data['groups'] = $groups_unseen;
$data['forums'] = $forums_unseen;
$data['notification'] = ($notification_count < 50) ? $notification_count : '49+';
$data['notifications'] = $navNotifications;
$data['sysmsgs'] = [
'notice' => $sysmsgs,
'info' => $sysmsgs_info
];
if (isset($_GET['callback'])) {
// JSONP support
header("Content-type: application/javascript");
echo $_GET['callback'] . '(' . json_encode(['result' => $data]) . ')';
exit;
} else {
System::jsonExit(['result' => $data]);
}
}
}

View file

@ -24,10 +24,14 @@ namespace Friendica\Module;
use Friendica\Core\Hook;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\APContact;
use Friendica\Model\Group;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Model\Tag;
use Friendica\Model\User;
use Friendica\Network\HTTPException;
use Friendica\Protocol\ActivityPub;
/**
* Outputs the permission tooltip HTML content for the provided item, photo or event id.
@ -44,9 +48,9 @@ class PermissionTooltip extends \Friendica\BaseModule
throw new HTTPException\BadRequestException(DI::l10n()->t('Wrong type "%s", expected one of: %s', $type, implode(', ', $expectedTypes)));
}
$condition = ['id' => $referenceId];
$condition = ['id' => $referenceId, 'uid' => [0, local_user()]];
if ($type == 'item') {
$fields = ['uid', 'psid', 'private'];
$fields = ['uid', 'psid', 'private', 'uri-id'];
$model = Post::selectFirst($fields, $condition);
} else {
$fields = ['uid', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid'];
@ -72,13 +76,32 @@ class PermissionTooltip extends \Friendica\BaseModule
// Kept for backwards compatiblity
Hook::callAll('lockview_content', $model);
if ($model['uid'] != local_user() ||
isset($model['private'])
&& $model['private'] == Item::PRIVATE
&& empty($model['allow_cid'])
if ($type == 'item') {
$receivers = $this->fetchReceivers($model['uri-id']);
if (empty($receivers)) {
switch ($model['private']) {
case Item::PUBLIC:
$receivers = DI::l10n()->t('Public');
break;
case Item::UNLISTED:
$receivers = DI::l10n()->t('Unlisted');
break;
case Item::PRIVATE:
$receivers = DI::l10n()->t('Limited/Private');
break;
}
}
} else {
$receivers = '';
}
if (empty($model['allow_cid'])
&& empty($model['allow_gid'])
&& empty($model['deny_cid'])
&& empty($model['deny_gid']))
&& empty($model['deny_gid'])
&& empty($receivers))
{
echo DI::l10n()->t('Remote privacy information not available.');
exit;
@ -136,7 +159,75 @@ class PermissionTooltip extends \Friendica\BaseModule
$l[] = '<strike>' . $contact['name'] . '</strike>';
}
echo $o . implode(', ', $l);
if (!empty($l)) {
echo $o . implode(', ', $l);
} else {
echo $o . $receivers;
}
exit();
}
/**
* Fetch a list of receivers
*
* @param int $uriId
* @return string
*/
private function fetchReceivers(int $uriId):string
{
$own_url = '';
$uid = local_user();
if ($uid) {
$owner = User::getOwnerDataById($uid);
if (!empty($owner['url'])) {
$own_url = $owner['url'];
}
}
$receivers = [];
foreach (Tag::getByURIId($uriId, [Tag::TO, Tag::CC, Tag::BCC]) as $receiver) {
// We only display BCC when it contains the current user
if (($receiver['type'] == Tag::BCC) && ($receiver['url'] != $own_url)) {
continue;
}
if ($receiver['url'] == ActivityPub::PUBLIC_COLLECTION) {
$receivers[$receiver['type']][] = DI::l10n()->t('Public');
} else {
$apcontact = DBA::selectFirst('apcontact', ['name'], ['followers' => $receiver['url']]);
if (!empty($apcontact['name'])) {
$receivers[$receiver['type']][] = DI::l10n()->t('Followers (%s)', $apcontact['name']);
} elseif ($apcontact = APContact::getByURL($receiver['url'], false)) {
$receivers[$receiver['type']][] = $apcontact['name'];
} else {
$receivers[$receiver['type']][] = $receiver['name'];
}
}
}
$output = '';
foreach ($receivers as $type => $receiver) {
$max = DI::config()->get('system', 'max_receivers');
$total = count($receiver);
if ($total > $max) {
$receiver = array_slice($receiver, 0, $max);
$receiver[] = DI::l10n()->t('%d more', $total - $max);
}
switch ($type) {
case Tag::TO:
$output .= DI::l10n()->t('<b>To:</b> %s<br>', implode(', ', $receiver));
break;
case Tag::CC:
$output .= DI::l10n()->t('<b>CC:</b> %s<br>', implode(', ', $receiver));
break;
case Tag::BCC:
$output .= DI::l10n()->t('<b>BCC:</b> %s<br>', implode(', ', $receiver));
break;
}
}
return $output;
}
}

View file

@ -288,9 +288,10 @@ class Photo extends BaseModule
}
}
If (($contact['uid'] != 0) && empty($contact['photo']) && empty($contact['avatar'])) {
if (!empty($contact['uid']) && empty($contact['photo']) && empty($contact['avatar'])) {
$contact = Contact::getByURL($contact['url'], false, ['avatar', 'photo', 'xmpp', 'addr']);
}
if (!empty($contact['photo']) && !empty($contact['avatar'])) {
// Fetch photo directly
$resourceid = MPhoto::ridFromURI($contact['photo']);

View file

@ -118,7 +118,7 @@ class Status extends BaseProfile
$commvisitor = $commpage && $remote_contact;
DI::page()['aside'] .= Widget::postedByYear(DI::baseUrl() . '/profile/' . $profile['nickname'] . '/status', $profile['profile_uid'] ?? 0, true);
DI::page()['aside'] .= Widget::categories(DI::baseUrl() . '/profile/' . $profile['nickname'] . '/status', XML::escape($category));
DI::page()['aside'] .= Widget::categories($profile['uid'], DI::baseUrl() . '/profile/' . $profile['nickname'] . '/status', $category);
DI::page()['aside'] .= Widget::tagCloud($profile['uid']);
if (Security::canWriteToUserWall($profile['uid'])) {
@ -159,7 +159,7 @@ class Status extends BaseProfile
// Does the profile page belong to a forum?
// If not then we can improve the performance with an additional condition
$condition2 = ['uid' => $profile['uid'], 'page-flags' => [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_PRVGROUP]];
$condition2 = ['uid' => $profile['uid'], 'account-type' => User::ACCOUNT_TYPE_COMMUNITY];
if (!DBA::exists('user', $condition2)) {
$condition = DBA::mergeConditions($condition, ['contact-id' => $profile['id']]);
}

View file

@ -380,11 +380,11 @@ class Register extends BaseModule
'type' => Model\Notification\Type::SYSTEM,
'event' => 'SYSTEM_REGISTER_REQUEST',
'uid' => $admin['uid'],
'link' => $base_url . '/admin/users/',
'link' => DI::baseUrl()->get(true) . '/admin/users/',
'source_name' => $user['username'],
'source_mail' => $user['email'],
'source_nick' => $user['nickname'],
'source_link' => $base_url . '/admin/users/',
'source_link' => DI::baseUrl()->get(true) . '/admin/users/',
'source_photo' => User::getAvatarUrl($user, Proxy::SIZE_THUMB),
'show_in_notification_page' => false
]);

View file

@ -208,7 +208,7 @@ class Index extends BaseSettings
'$baseurl' => DI::baseUrl()->get(true),
]);
$personal_account = !in_array($profile['page-flags'], [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_PRVGROUP]);
$personal_account = ($profile['account-type'] != User::ACCOUNT_TYPE_COMMUNITY);
$tpl = Renderer::getMarkupTemplate('settings/profile/index.tpl');
$o .= Renderer::replaceMacros($tpl, [

View file

@ -24,12 +24,15 @@ namespace Friendica\Navigation\Notifications\Collection;
use Friendica\BaseCollection;
use Friendica\Navigation\Notifications\ValueObject;
class FormattedNotifications extends BaseCollection
/**
* @deprecated since 2022.05 Use \Friendica\Navigation\Notifications\Collection\FormattedNotifications instead
*/
class FormattedNotifies extends BaseCollection
{
/**
* @return ValueObject\FormattedNotification
* @return ValueObject\FormattedNotify
*/
public function current(): ValueObject\FormattedNotification
public function current(): ValueObject\FormattedNotify
{
return parent::current();
}

View file

@ -47,4 +47,11 @@ class Notifications extends BaseCollection
$Notification->setDismissed();
});
}
public function countUnseen(): int
{
return array_reduce($this->getArrayCopy(), function (int $carry, Entity\Notification $Notification) {
return $carry + ($Notification->seen ? 0 : 1);
}, 0);
}
}

View file

@ -34,6 +34,7 @@ use Friendica\BaseEntity;
* @property-read $parentUriId
* @property-read $created
* @property-read $seen
* @property-read $dismissed
*/
class Notification extends BaseEntity
{
@ -72,11 +73,11 @@ class Notification extends BaseEntity
* @param int|null $parentUriId
* @param DateTime|null $created
* @param bool $seen
* @param int|null $id
* @param bool $dismissed
* @param int|null $id
* @see \Friendica\Navigation\Notifications\Factory\Notification
*/
public function __construct(int $uid, string $verb, int $type, int $actorId, int $targetUriId = null, int $parentUriId = null, DateTime $created = null, bool $seen = false, int $id = null, bool $dismissed = false)
public function __construct(int $uid, string $verb, int $type, int $actorId, int $targetUriId = null, int $parentUriId = null, DateTime $created = null, bool $seen = false, bool $dismissed = false, int $id = null)
{
$this->uid = $uid;
$this->verb = $verb;
@ -86,8 +87,9 @@ class Notification extends BaseEntity
$this->parentUriId = $parentUriId ?: $targetUriId;
$this->created = $created;
$this->seen = $seen;
$this->id = $id;
$this->dismissed = $dismissed;
$this->id = $id;
}
public function setSeen()

View file

@ -46,6 +46,8 @@ use Psr\Http\Message\UriInterface;
* @property-read $uriId
* @property-read $parentUriId
* @property-read $id
*
* @deprecated since 2022.05 Use \Friendica\Navigation\Notifications\Entity\Notification instead
*/
class Notify extends BaseEntity
{
@ -132,16 +134,6 @@ class Notify extends BaseEntity
*/
public static function formatMessage(string $name, string $message): string
{
if ($name != '') {
$pos = strpos($message, $name);
} else {
$pos = false;
}
if ($pos !== false) {
$message = substr_replace($message, '{0}', $pos, strlen($name));
}
return $message;
return str_replace('{0}', '<span class="contactname">' . strip_tags(BBCode::convert($name)) . '</span>', $message);
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @copyright Copyright (C) 2010-2022, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Navigation\Notifications\Exception;
class NoMessageException extends \Exception
{
}

View file

@ -0,0 +1,140 @@
<?php
/**
* @copyright Copyright (C) 2010-2022, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Navigation\Notifications\Factory;
use Friendica\BaseFactory;
use Friendica\Core\Renderer;
use Friendica\Model\Contact;
use Friendica\Navigation\Notifications\Entity;
use Friendica\Navigation\Notifications\Exception\NoMessageException;
use Friendica\Navigation\Notifications\ValueObject;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Proxy;
use Friendica\Util\Temporal;
use GuzzleHttp\Psr7\Uri;
use Psr\Log\LoggerInterface;
/**
* Factory for creating notification objects based on items
*/
class FormattedNavNotification extends BaseFactory
{
private static $contacts = [];
/** @var Notification */
private $notification;
/** @var \Friendica\App\BaseURL */
private $baseUrl;
/** @var \Friendica\Core\L10n */
private $l10n;
/** @var string */
private $tpl;
public function __construct(Notification $notification, \Friendica\App\BaseURL $baseUrl, \Friendica\Core\L10n $l10n, LoggerInterface $logger)
{
parent::__construct($logger);
$this->notification = $notification;
$this->baseUrl = $baseUrl;
$this->l10n = $l10n;
$this->tpl = Renderer::getMarkupTemplate('notifications/nav/notify.tpl');
}
/**
* @param array $contact A contact array with the following keys: name, url
* @param string $message A notification message with the {0} placeholder for the contact name
* @param \DateTime $date
* @param Uri $href
* @param bool $seen
* @return ValueObject\FormattedNavNotification
* @throws \Friendica\Network\HTTPException\ServiceUnavailableException
*/
public function createFromParams(array $contact, string $message, \DateTime $date, Uri $href, bool $seen = false): ValueObject\FormattedNavNotification
{
$contact['photo'] = Contact::getAvatarUrlForUrl($contact['url'], local_user(), Proxy::SIZE_MICRO);
$dateMySQL = $date->format(DateTimeFormat::MYSQL);
$templateNotify = [
'contact' => $contact,
'href' => $href->__toString(),
'message' => $message,
'seen' => $seen,
'localdate' => DateTimeFormat::local($dateMySQL),
'ago' => Temporal::getRelativeDate($dateMySQL),
'richtext' => Entity\Notify::formatMessage($contact['name'], $message),
];
return new ValueObject\FormattedNavNotification(
$contact,
$date->getTimestamp(),
strip_tags($templateNotify['richtext']),
Renderer::replaceMacros($this->tpl, ['notify' => $templateNotify]),
$href,
$seen,
);
}
/**
* @param Entity\Notification $notification
* @return ValueObject\FormattedNavNotification
* @throws NoMessageException
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \Friendica\Network\HTTPException\NotFoundException
* @throws \Friendica\Network\HTTPException\ServiceUnavailableException
*/
public function createFromNotification(Entity\Notification $notification): ValueObject\FormattedNavNotification
{
$message = $this->notification->getMessageFromNotification($notification);
if (empty($message)) {
throw new NoMessageException();
}
if (!isset(self::$contacts[$notification->actorId])) {
self::$contacts[$notification->actorId] = Contact::getById($notification->actorId, ['name', 'url']);
}
return $this->createFromParams(
self::$contacts[$notification->actorId],
$message['notification'],
$notification->created,
new Uri($this->baseUrl->get() . '/notification/' . $notification->id),
$notification->seen,
);
}
public function createFromIntro(\Friendica\Contact\Introduction\Entity\Introduction $intro): ValueObject\FormattedNavNotification
{
if (!isset(self::$contacts[$intro->cid])) {
self::$contacts[$intro->cid] = Contact::getById($intro->cid, ['name', 'url']);
}
return $this->createFromParams(
self::$contacts[$intro->cid],
$this->l10n->t('{0} wants to follow you'),
$intro->datetime,
new Uri($this->baseUrl->get() . '/notifications/intros/' . $intro->id)
);
}
}

View file

@ -31,7 +31,7 @@ use Friendica\Database\Database;
use Friendica\Model\Contact;
use Friendica\Model\Post;
use Friendica\Module\BaseNotifications;
use Friendica\Navigation\Notifications\Collection\FormattedNotifications;
use Friendica\Navigation\Notifications\Collection\FormattedNotifies;
use Friendica\Navigation\Notifications\Repository;
use Friendica\Navigation\Notifications\ValueObject;
use Friendica\Network\HTTPException\InternalServerErrorException;
@ -49,8 +49,10 @@ use Psr\Log\LoggerInterface;
* - system
* - home
* - personal
*
* @deprecated since 2022.05 Use \Friendica\Navigation\Notifications\Factory\FormattedNotification instead
*/
class FormattedNotification extends BaseFactory
class FormattedNotify extends BaseFactory
{
/** @var Database */
private $dba;
@ -61,12 +63,12 @@ class FormattedNotification extends BaseFactory
/** @var L10n */
private $l10n;
public function __construct(LoggerInterface $logger, Database $dba, Repository\Notify $notify, BaseURL $baseUrl, L10n $l10n)
public function __construct(LoggerInterface $logger, Database $dba, Repository\Notify $notification, BaseURL $baseUrl, L10n $l10n)
{
parent::__construct($logger);
$this->dba = $dba;
$this->notify = $notify;
$this->notify = $notification;
$this->baseUrl = $baseUrl;
$this->l10n = $l10n;
}
@ -74,14 +76,14 @@ class FormattedNotification extends BaseFactory
/**
* @param array $formattedItem The return of $this->formatItem
*
* @return ValueObject\FormattedNotification
* @return ValueObject\FormattedNotify
*/
private function createFromFormattedItem(array $formattedItem): ValueObject\FormattedNotification
private function createFromFormattedItem(array $formattedItem): ValueObject\FormattedNotify
{
// Transform the different types of notification in a usable array
switch ($formattedItem['verb'] ?? '') {
case Activity::LIKE:
return new ValueObject\FormattedNotification(
return new ValueObject\FormattedNotify(
'like',
$this->baseUrl->get(true) . '/display/' . $formattedItem['parent-guid'],
$formattedItem['author-avatar'],
@ -93,7 +95,7 @@ class FormattedNotification extends BaseFactory
);
case Activity::DISLIKE:
return new ValueObject\FormattedNotification(
return new ValueObject\FormattedNotify(
'dislike',
$this->baseUrl->get(true) . '/display/' . $formattedItem['parent-guid'],
$formattedItem['author-avatar'],
@ -105,7 +107,7 @@ class FormattedNotification extends BaseFactory
);
case Activity::ATTEND:
return new ValueObject\FormattedNotification(
return new ValueObject\FormattedNotify(
'attend',
$this->baseUrl->get(true) . '/display/' . $formattedItem['parent-guid'],
$formattedItem['author-avatar'],
@ -117,7 +119,7 @@ class FormattedNotification extends BaseFactory
);
case Activity::ATTENDNO:
return new ValueObject\FormattedNotification(
return new ValueObject\FormattedNotify(
'attendno',
$this->baseUrl->get(true) . '/display/' . $formattedItem['parent-guid'],
$formattedItem['author-avatar'],
@ -129,7 +131,7 @@ class FormattedNotification extends BaseFactory
);
case Activity::ATTENDMAYBE:
return new ValueObject\FormattedNotification(
return new ValueObject\FormattedNotify(
'attendmaybe',
$this->baseUrl->get(true) . '/display/' . $formattedItem['parent-guid'],
$formattedItem['author-avatar'],
@ -142,7 +144,7 @@ class FormattedNotification extends BaseFactory
case Activity::FRIEND:
if (!isset($formattedItem['object'])) {
return new ValueObject\FormattedNotification(
return new ValueObject\FormattedNotify(
'friend',
$formattedItem['link'],
$formattedItem['image'],
@ -159,7 +161,7 @@ class FormattedNotification extends BaseFactory
$formattedItem['fname'] = $obj->title;
return new ValueObject\FormattedNotification(
return new ValueObject\FormattedNotify(
'friend',
$this->baseUrl->get(true) . '/display/' . $formattedItem['parent-guid'],
$formattedItem['author-avatar'],
@ -171,7 +173,7 @@ class FormattedNotification extends BaseFactory
);
default:
return new ValueObject\FormattedNotification(
return new ValueObject\FormattedNotify(
$formattedItem['label'] ?? '',
$formattedItem['link'] ?? '',
$formattedItem['image'] ?? '',
@ -192,9 +194,9 @@ class FormattedNotification extends BaseFactory
* @param int $start Start the query at this point
* @param int $limit Maximum number of query results
*
* @return FormattedNotifications
* @return FormattedNotifies
*/
public function getSystemList(bool $seen = false, int $start = 0, int $limit = BaseNotifications::DEFAULT_PAGE_LIMIT): FormattedNotifications
public function getSystemList(bool $seen = false, int $start = 0, int $limit = BaseNotifications::DEFAULT_PAGE_LIMIT): FormattedNotifies
{
$conditions = [];
if (!$seen) {
@ -205,14 +207,14 @@ class FormattedNotification extends BaseFactory
$params['order'] = ['date' => 'DESC'];
$params['limit'] = [$start, $limit];
$formattedNotifications = new FormattedNotifications();
$formattedNotifications = new FormattedNotifies();
try {
$Notifies = $this->notify->selectForUser(local_user(), $conditions, $params);
foreach ($Notifies as $Notify) {
$formattedNotifications[] = new ValueObject\FormattedNotification(
$formattedNotifications[] = new ValueObject\FormattedNotify(
'notification',
$this->baseUrl->get(true) . '/notification/' . $Notify->id,
$this->baseUrl->get(true) . '/notify/' . $Notify->id,
Contact::getAvatarUrlForUrl($Notify->url, $Notify->uid, Proxy::SIZE_MICRO),
$Notify->url,
strip_tags(BBCode::toPlaintext($Notify->msg)),
@ -236,9 +238,9 @@ class FormattedNotification extends BaseFactory
* @param int $start Start the query at this point
* @param int $limit Maximum number of query results
*
* @return FormattedNotifications
* @return FormattedNotifies
*/
public function getNetworkList(bool $seen = false, int $start = 0, int $limit = BaseNotifications::DEFAULT_PAGE_LIMIT): FormattedNotifications
public function getNetworkList(bool $seen = false, int $start = 0, int $limit = BaseNotifications::DEFAULT_PAGE_LIMIT): FormattedNotifies
{
$condition = ['wall' => false, 'uid' => local_user()];
@ -250,7 +252,7 @@ class FormattedNotification extends BaseFactory
'network', 'created', 'object', 'parent-author-name', 'parent-author-link', 'parent-guid', 'gravity'];
$params = ['order' => ['received' => true], 'limit' => [$start, $limit]];
$formattedNotifications = new FormattedNotifications();
$formattedNotifications = new FormattedNotifies();
try {
$userPosts = Post::selectForUser(local_user(), $fields, $condition, $params);
@ -272,9 +274,9 @@ class FormattedNotification extends BaseFactory
* @param int $start Start the query at this point
* @param int $limit Maximum number of query results
*
* @return FormattedNotifications
* @return FormattedNotifies
*/
public function getPersonalList(bool $seen = false, int $start = 0, int $limit = BaseNotifications::DEFAULT_PAGE_LIMIT): FormattedNotifications
public function getPersonalList(bool $seen = false, int $start = 0, int $limit = BaseNotifications::DEFAULT_PAGE_LIMIT): FormattedNotifies
{
$condition = ['wall' => false, 'uid' => local_user(), 'author-id' => public_contact()];
@ -286,7 +288,7 @@ class FormattedNotification extends BaseFactory
'network', 'created', 'object', 'parent-author-name', 'parent-author-link', 'parent-guid', 'gravity'];
$params = ['order' => ['received' => true], 'limit' => [$start, $limit]];
$formattedNotifications = new FormattedNotifications();
$formattedNotifications = new FormattedNotifies();
try {
$userPosts = Post::selectForUser(local_user(), $fields, $condition, $params);
@ -308,9 +310,9 @@ class FormattedNotification extends BaseFactory
* @param int $start Start the query at this point
* @param int $limit Maximum number of query results
*
* @return FormattedNotifications
* @return FormattedNotifies
*/
public function getHomeList(bool $seen = false, int $start = 0, int $limit = BaseNotifications::DEFAULT_PAGE_LIMIT): FormattedNotifications
public function getHomeList(bool $seen = false, int $start = 0, int $limit = BaseNotifications::DEFAULT_PAGE_LIMIT): FormattedNotifies
{
$condition = ['wall' => true, 'uid' => local_user()];
@ -322,7 +324,7 @@ class FormattedNotification extends BaseFactory
'network', 'created', 'object', 'parent-author-name', 'parent-author-link', 'parent-guid', 'gravity'];
$params = ['order' => ['received' => true], 'limit' => [$start, $limit]];
$formattedNotifications = new FormattedNotifications();
$formattedNotifications = new FormattedNotifies();
try {
$userPosts = Post::selectForUser(local_user(), $fields, $condition, $params);

View file

@ -24,16 +24,35 @@ namespace Friendica\Navigation\Notifications\Factory;
use Friendica\App\BaseURL;
use Friendica\BaseFactory;
use Friendica\Capabilities\ICanCreateFromTableRow;
use Friendica\Contact\LocalRelationship\Repository\LocalRelationship;
use Friendica\Content\Text\Plaintext;
use Friendica\Core\L10n;
use Friendica\Model\Contact;
use Friendica\Model\Post;
use Friendica\Model\Verb;
use Friendica\Navigation\Notifications\Entity;
use Friendica\Network\HTTPException;
use Friendica\Protocol\Activity;
use Psr\Log\LoggerInterface;
class Notification extends BaseFactory implements ICanCreateFromTableRow
{
/** @var BaseURL */
private $baseUrl;
/** @var L10n */
private $l10n;
/** @var LocalRelationship */
private $localRelationshipRepo;
public function __construct(\Friendica\App\BaseURL $baseUrl, \Friendica\Core\L10n $l10n, \Friendica\Contact\LocalRelationship\Repository\LocalRelationship $localRelationshipRepo, LoggerInterface $logger)
{
parent::__construct($logger);
$this->baseUrl = $baseUrl;
$this->l10n = $l10n;
$this->localRelationshipRepo = $localRelationshipRepo;
}
public function createFromTableRow(array $row): Entity\Notification
{
return new Entity\Notification(
@ -45,7 +64,8 @@ class Notification extends BaseFactory implements ICanCreateFromTableRow
$row['parent-uri-id'],
new \DateTime($row['created'], new \DateTimeZone('UTC')),
$row['seen'],
$row['id']
$row['dismissed'],
$row['id'],
);
}
@ -61,6 +81,12 @@ class Notification extends BaseFactory implements ICanCreateFromTableRow
);
}
/**
* @param int $uid
* @param int $contactId Public contact id
* @param string $verb
* @return Entity\Notification
*/
public function createForRelationship(int $uid, int $contactId, string $verb): Entity\Notification
{
return new Entity\Notification(
@ -73,40 +99,49 @@ class Notification extends BaseFactory implements ICanCreateFromTableRow
/**
* @param Entity\Notification $Notification
* @param BaseURL $baseUrl
* @param L10n $userL10n Seeded with the language of the user we mean the notification for
* @return array
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws HTTPException\InternalServerErrorException
* @throws HTTPException\NotFoundException
*/
public function getMessageFromNotification(Entity\Notification $Notification, BaseURL $baseUrl, L10n $userL10n)
public function getMessageFromNotification(Entity\Notification $Notification): array
{
$message = [];
$causer = $author = Contact::getById($Notification->actorId, ['id', 'name', 'url', 'pending']);
$causer = $author = Contact::getById($Notification->actorId, ['id', 'name', 'url', 'contact-type', 'pending']);
if (empty($causer)) {
$this->logger->info('Causer not found', ['contact' => $Notification->actorId]);
return $message;
}
if ($Notification->type === Post\UserNotification::TYPE_NONE) {
if ($causer['pending']) {
$msg = $userL10n->t('%1$s wants to follow you');
$localRelationship = $this->localRelationshipRepo->getForUserContact($Notification->uid, $Notification->actorId);
if ($localRelationship->pending) {
$msg = $this->l10n->t('%1$s wants to follow you');
} else {
$msg = $userL10n->t('%1$s had started following you');
$msg = $this->l10n->t('%1$s has started following you');
}
$title = $causer['name'];
$link = $baseUrl . '/contact/' . $causer['id'];
$link = $this->baseUrl . '/contact/' . $causer['id'];
} else {
if (!$Notification->targetUriId) {
return $message;
}
if (Post\ThreadUser::getIgnored($Notification->parentUriId, $Notification->uid)) {
$this->logger->info('Thread is ignored', ['parent-uri-id' => $Notification->parentUriId, 'type' => $Notification->type]);
return $message;
}
if (in_array($Notification->type, [Post\UserNotification::TYPE_THREAD_COMMENT, Post\UserNotification::TYPE_COMMENT_PARTICIPATION, Post\UserNotification::TYPE_ACTIVITY_PARTICIPATION, Post\UserNotification::TYPE_EXPLICIT_TAGGED])) {
$item = Post::selectFirst([], ['uri-id' => $Notification->parentUriId, 'uid' => [0, $Notification->uid]], ['order' => ['uid' => true]]);
if (empty($item)) {
$this->logger->info('Parent post not found', ['uri-id' => $Notification->parentUriId]);
return $message;
}
if ($Notification->type == Post\UserNotification::TYPE_COMMENT_PARTICIPATION) {
$link_item = Post::selectFirst(['guid'], ['uri-id' => $Notification->targetUriId, 'uid' => [0, $Notification->uid]], ['order' => ['uid' => true]]);
}
} else {
$item = Post::selectFirst([], ['uri-id' => $Notification->targetUriId, 'uid' => [0, $Notification->uid]], ['order' => ['uid' => true]]);
if (empty($item)) {
@ -124,14 +159,14 @@ class Notification extends BaseFactory implements ICanCreateFromTableRow
}
if (in_array($Notification->type, [Post\UserNotification::TYPE_COMMENT_PARTICIPATION, Post\UserNotification::TYPE_ACTIVITY_PARTICIPATION, Post\UserNotification::TYPE_SHARED])) {
$author = Contact::getById($item['author-id'], ['id', 'name', 'url']);
$author = Contact::getById($item['author-id'], ['id', 'name', 'url', 'contact-type']);
if (empty($author)) {
$this->logger->info('Author not found', ['author' => $item['author-id']]);
return $message;
}
}
$link = $baseUrl . '/display/' . urlencode($item['guid']);
$link = $this->baseUrl . '/display/' . urlencode($link_item['guid'] ?? $item['guid']);
$content = Plaintext::getPost($item, 70);
if (!empty($content['text'])) {
@ -146,40 +181,40 @@ class Notification extends BaseFactory implements ICanCreateFromTableRow
case Activity::LIKE:
switch ($Notification->type) {
case Post\UserNotification::TYPE_DIRECT_COMMENT:
$msg = $userL10n->t('%1$s liked your comment %2$s');
$msg = $this->l10n->t('%1$s liked your comment on %2$s');
break;
case Post\UserNotification::TYPE_DIRECT_THREAD_COMMENT:
$msg = $userL10n->t('%1$s liked your post %2$s');
$msg = $this->l10n->t('%1$s liked your post %2$s');
break;
}
break;
case Activity::DISLIKE:
switch ($Notification->type) {
case Post\UserNotification::TYPE_DIRECT_COMMENT:
$msg = $userL10n->t('%1$s disliked your comment %2$s');
$msg = $this->l10n->t('%1$s disliked your comment on %2$s');
break;
case Post\UserNotification::TYPE_DIRECT_THREAD_COMMENT:
$msg = $userL10n->t('%1$s disliked your post %2$s');
$msg = $this->l10n->t('%1$s disliked your post %2$s');
break;
}
break;
case Activity::ANNOUNCE:
switch ($Notification->type) {
case Post\UserNotification::TYPE_DIRECT_COMMENT:
$msg = $userL10n->t('%1$s shared your comment %2$s');
$msg = $this->l10n->t('%1$s shared your comment %2$s');
break;
case Post\UserNotification::TYPE_DIRECT_THREAD_COMMENT:
$msg = $userL10n->t('%1$s shared your post %2$s');
$msg = $this->l10n->t('%1$s shared your post %2$s');
break;
case Post\UserNotification::TYPE_SHARED:
if (($causer['id'] != $author['id']) && ($title != '')) {
$msg = $userL10n->t('%1$s shared the post %2$s from %3$s');
$msg = $this->l10n->t('%1$s shared the post %2$s from %3$s');
} elseif ($causer['id'] != $author['id']) {
$msg = $userL10n->t('%1$s shared a post from %3$s');
$msg = $this->l10n->t('%1$s shared a post from %3$s');
} elseif ($title != '') {
$msg = $userL10n->t('%1$s shared the post %2$s');
$msg = $this->l10n->t('%1$s shared the post %2$s');
} else {
$msg = $userL10n->t('%1$s shared a post');
$msg = $this->l10n->t('%1$s shared a post');
}
break;
}
@ -187,68 +222,68 @@ class Notification extends BaseFactory implements ICanCreateFromTableRow
case Activity::ATTEND:
switch ($Notification->type) {
case Post\UserNotification::TYPE_DIRECT_THREAD_COMMENT:
$msg = $userL10n->t('%1$s wants to attend your event %2$s');
$msg = $this->l10n->t('%1$s wants to attend your event %2$s');
break;
}
break;
case Activity::ATTENDNO:
switch ($Notification->type) {
case Post\UserNotification::TYPE_DIRECT_THREAD_COMMENT:
$msg = $userL10n->t('%1$s does not want to attend your event %2$s');
$msg = $this->l10n->t('%1$s does not want to attend your event %2$s');
break;
}
break;
case Activity::ATTENDMAYBE:
switch ($Notification->type) {
case Post\UserNotification::TYPE_DIRECT_THREAD_COMMENT:
$msg = $userL10n->t('%1$s maybe wants to attend your event %2$s');
$msg = $this->l10n->t('%1$s maybe wants to attend your event %2$s');
break;
}
break;
case Activity::POST:
switch ($Notification->type) {
case Post\UserNotification::TYPE_EXPLICIT_TAGGED:
$msg = $userL10n->t('%1$s tagged you on %2$s');
$msg = $this->l10n->t('%1$s tagged you on %2$s');
break;
case Post\UserNotification::TYPE_IMPLICIT_TAGGED:
$msg = $userL10n->t('%1$s replied to you on %2$s');
$msg = $this->l10n->t('%1$s replied to you on %2$s');
break;
case Post\UserNotification::TYPE_THREAD_COMMENT:
$msg = $userL10n->t('%1$s commented in your thread %2$s');
$msg = $this->l10n->t('%1$s commented in your thread %2$s');
break;
case Post\UserNotification::TYPE_DIRECT_COMMENT:
$msg = $userL10n->t('%1$s commented on your comment %2$s');
$msg = $this->l10n->t('%1$s commented on your comment %2$s');
break;
case Post\UserNotification::TYPE_COMMENT_PARTICIPATION:
case Post\UserNotification::TYPE_ACTIVITY_PARTICIPATION:
if (($causer['id'] == $author['id']) && ($title != '')) {
$msg = $userL10n->t('%1$s commented in their thread %2$s');
$msg = $this->l10n->t('%1$s commented in their thread %2$s');
} elseif ($causer['id'] == $author['id']) {
$msg = $userL10n->t('%1$s commented in their thread');
$msg = $this->l10n->t('%1$s commented in their thread');
} elseif ($title != '') {
$msg = $userL10n->t('%1$s commented in the thread %2$s from %3$s');
$msg = $this->l10n->t('%1$s commented in the thread %2$s from %3$s');
} else {
$msg = $userL10n->t('%1$s commented in the thread from %3$s');
$msg = $this->l10n->t('%1$s commented in the thread from %3$s');
}
break;
case Post\UserNotification::TYPE_DIRECT_THREAD_COMMENT:
$msg = $userL10n->t('%1$s commented on your thread %2$s');
$msg = $this->l10n->t('%1$s commented on your thread %2$s');
break;
case Post\UserNotification::TYPE_SHARED:
if (($causer['id'] != $author['id']) && ($title != '')) {
$msg = $userL10n->t('%1$s shared the post %2$s from %3$s');
$msg = $this->l10n->t('%1$s shared the post %2$s from %3$s');
} elseif ($causer['id'] != $author['id']) {
$msg = $userL10n->t('%1$s shared a post from %3$s');
$msg = $this->l10n->t('%1$s shared a post from %3$s');
} elseif ($title != '') {
$msg = $userL10n->t('%1$s shared the post %2$s');
$msg = $this->l10n->t('%1$s shared the post %2$s');
} else {
$msg = $userL10n->t('%1$s shared a post');
$msg = $this->l10n->t('%1$s shared a post');
}
break;
}
@ -268,6 +303,9 @@ class Notification extends BaseFactory implements ICanCreateFromTableRow
'[url=' . $causer['url'] . ']' . $causer['name'] . '[/url]',
'[url=' . $link . ']' . $title . '[/url]',
'[url=' . $author['url'] . ']' . $author['name'] . '[/url]');
$message['link'] = $link;
} else {
$this->logger->debug('Unhandled notification', ['notification' => $Notification]);
}
return $message;

View file

@ -26,6 +26,9 @@ use Friendica\Capabilities\ICanCreateFromTableRow;
use Friendica\Content\Text\BBCode;
use GuzzleHttp\Psr7\Uri;
/**
* @deprecated since 2022.05 Use \Friendica\Navigation\Notifications\Factory\Notification instead
*/
class Notify extends BaseFactory implements ICanCreateFromTableRow
{
public function createFromTableRow(array $row): \Friendica\Navigation\Notifications\Entity\Notify

View file

@ -24,6 +24,7 @@ namespace Friendica\Navigation\Notifications\Repository;
use Exception;
use Friendica\BaseCollection;
use Friendica\BaseRepository;
use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues;
use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\Model\Verb;
@ -41,9 +42,14 @@ class Notification extends BaseRepository
protected static $table_name = 'notification';
public function __construct(Database $database, LoggerInterface $logger, Factory\Notification $factory = null)
/** @var IManagePersonalConfigValues */
private $pconfig;
public function __construct(IManagePersonalConfigValues $pconfig, Database $database, LoggerInterface $logger, Factory\Notification $factory)
{
parent::__construct($database, $logger, $factory ?? new Factory\Notification($logger));
parent::__construct($database, $logger, $factory);
$this->pconfig = $pconfig;
}
/**
@ -100,6 +106,74 @@ class Notification extends BaseRepository
return $this->select($condition, $params);
}
/**
* Returns only the most recent notifications for the same conversation or contact
*
* @param int $uid
* @return Collection\Notifications
* @throws Exception
*/
public function selectDetailedForUser(int $uid): Collection\Notifications
{
$condition = [];
if (!$this->pconfig->get($uid, 'system', 'notify_like')) {
$condition = DBA::mergeConditions($condition, ['`vid` != ?', Verb::getID(\Friendica\Protocol\Activity::LIKE)]);
}
if (!$this->pconfig->get($uid, 'system', 'notify_announce')) {
$condition = DBA::mergeConditions($condition, ['`vid` != ?', Verb::getID(\Friendica\Protocol\Activity::ANNOUNCE)]);
}
return $this->selectForUser($uid, $condition, ['limit' => 50, 'order' => ['id' => true]]);
}
/**
* Returns only the most recent notifications for the same conversation or contact
*
* @param int $uid
* @return Collection\Notifications
* @throws Exception
*/
public function selectDigestForUser(int $uid): Collection\Notifications
{
$values = [$uid];
$like_condition = '';
if (!$this->pconfig->get($uid, 'system', 'notify_like')) {
$like_condition = 'AND vid != ?';
$values[] = Verb::getID(\Friendica\Protocol\Activity::LIKE);
}
$announce_condition = '';
if (!$this->pconfig->get($uid, 'system', 'notify_announce')) {
$announce_condition = 'AND vid != ?';
$values[] = Verb::getID(\Friendica\Protocol\Activity::ANNOUNCE);
}
$rows = $this->db->p("
SELECT notification.*
FROM notification
WHERE id IN (
SELECT MAX(`id`)
FROM notification
WHERE uid = ?
$like_condition
$announce_condition
GROUP BY IFNULL(`parent-uri-id`, `actor-id`)
)
ORDER BY `seen`, `id` DESC
LIMIT 50
", ...$values);
$Entities = new Collection\Notifications();
foreach ($rows as $fields) {
$Entities[] = $this->factory->createFromTableRow($fields);
}
return $Entities;
}
public function selectAllForUser(int $uid): Collection\Notifications
{
return $this->selectForUser($uid);
@ -165,4 +239,14 @@ class Notification extends BaseRepository
return $Notification;
}
public function deleteForUserByVerb(int $uid, string $verb, array $condition = []): bool
{
$condition['uid'] = $uid;
$condition['vid'] = Verb::getID($verb);
$this->logger->notice('deleteForUserByVerb', ['condition' => $condition]);
return $this->db->delete(self::$table_name, $condition);
}
}

View file

@ -41,6 +41,9 @@ use Friendica\Util\DateTimeFormat;
use Friendica\Util\Emailer;
use Psr\Log\LoggerInterface;
/**
* @deprecated since 2022.05 Use \Friendica\Navigation\Notifications\Repository\Notification instead
*/
class Notify extends BaseRepository
{
/** @var Factory\Notify */
@ -216,7 +219,7 @@ class Notify extends BaseRepository
}
// Ensure that the important fields are set at any time
$fields = ['nickname', 'page-flags', 'notify-flags', 'language', 'username', 'email'];
$fields = ['nickname', 'account-type', 'notify-flags', 'language', 'username', 'email'];
$user = DBA::selectFirst('user', $fields, ['uid' => $params['uid']]);
if (!DBA::isResult($user)) {
@ -225,7 +228,7 @@ class Notify extends BaseRepository
}
// There is no need to create notifications for forum accounts
if (in_array($user['page-flags'], [Model\User::PAGE_FLAGS_COMMUNITY, Model\User::PAGE_FLAGS_PRVGROUP])) {
if ($user['account-type'] == Model\User::ACCOUNT_TYPE_COMMUNITY) {
return false;
}
@ -567,7 +570,7 @@ class Notify extends BaseRepository
$Notify->updateMsgFromPreamble($epreamble);
$Notify = $this->save($Notify);
$itemlink = $this->baseUrl->get() . '/notification/' . $Notify->id;
$itemlink = $this->baseUrl->get() . '/notify/' . $Notify->id;
$notify_id = $Notify->id;
}
@ -729,7 +732,7 @@ class Notify extends BaseRepository
$subject = $l10n->t('%1$s Comment to conversation #%2$d by %3$s', $subjectPrefix, $item['parent'], $contact['name']);
}
$msg = $this->notification->getMessageFromNotification($Notification, $this->baseUrl, $l10n);
$msg = $this->notification->getMessageFromNotification($Notification);
if (empty($msg)) {
$this->logger->info('No notification message, quitting', ['uid' => $Notification->uid, 'id' => $Notification->id, 'type' => $Notification->type]);
return false;

View file

@ -0,0 +1,61 @@
<?php
/**
* @copyright Copyright (C) 2010-2022, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Navigation\Notifications\ValueObject;
use Friendica\BaseEntity;
/**
* A view-only object for printing item notifications to the frontend
*/
class FormattedNavNotification extends BaseEntity
{
/** @var array */
protected $contact;
/** @var string */
protected $timestamp;
/** @var string */
protected $plaintext;
/** @var string */
protected $html;
/** @var string */
protected $href;
/** @var bool */
protected $seen;
/**
* @param array $contact Contact array with the following keys: name, url, photo
* @param string $timestamp Unix timestamp
* @param string $plaintext Localized notification message with the placeholder replaced by the contact name
* @param string $html Full HTML string of the notification menu element
* @param string $href Absolute URL this notification should send the user to when interacted with
* @param bool $seen Whether the user interacted with this notification once
*/
public function __construct(array $contact, string $timestamp, string $plaintext, string $html, string $href, bool $seen)
{
$this->contact = $contact;
$this->timestamp = $timestamp;
$this->plaintext = $plaintext;
$this->html = $html;
$this->href = $href;
$this->seen = $seen;
}
}

View file

@ -25,8 +25,10 @@ use Friendica\BaseDataTransferObject;
/**
* A view-only object for printing item notifications to the frontend
*
* @deprecated since 2022.05 Use \Friendica\Navigation\Notifications\ValueObject\FormattedNotification instead
*/
class FormattedNotification extends BaseDataTransferObject
class FormattedNotify extends BaseDataTransferObject
{
const SYSTEM = 'system';
const PERSONAL = 'personal';

View file

@ -21,11 +21,15 @@
namespace Friendica\Object\Api\Mastodon;
use Friendica\App\BaseURL;
use Friendica\BaseDataTransferObject;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\User;
use Friendica\Module\Register;
use Friendica\Network\HTTPException;
/**
* Class Instance
@ -68,43 +72,39 @@ class Instance extends BaseDataTransferObject
protected $rules = [];
/**
* Creates an instance record
*
* @return Instance
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @param IManageConfigValues $config
* @param BaseURL $baseUrl
* @param Database $database
* @throws HTTPException\InternalServerErrorException
* @throws HTTPException\NotFoundException
* @throws \ImagickException
*/
public static function get()
public function __construct(IManageConfigValues $config, BaseURL $baseUrl, Database $database)
{
$register_policy = intval(DI::config()->get('config', 'register_policy'));
$register_policy = intval($config->get('config', 'register_policy'));
$baseUrl = DI::baseUrl();
$this->uri = $baseUrl->get();
$this->title = $config->get('config', 'sitename');
$this->short_description = $this->description = $config->get('config', 'info');
$this->email = $config->get('config', 'admin_email');
$this->version = '2.8.0 (compatible; Friendica ' . FRIENDICA_VERSION . ')';
$this->urls = null; // Not supported
$this->stats = new Stats($config, $database);
$this->thumbnail = $baseUrl->get() . ($config->get('system', 'shortcut_icon') ?? 'images/friendica-32.png');
$this->languages = [$config->get('system', 'language')];
$this->max_toot_chars = (int)$config->get('config', 'api_import_size', $config->get('config', 'max_import_size'));
$this->registrations = ($register_policy != Register::CLOSED);
$this->approval_required = ($register_policy == Register::APPROVE);
$this->invites_enabled = false;
$this->contact_account = [];
$instance = new Instance();
$instance->uri = $baseUrl->get();
$instance->title = DI::config()->get('config', 'sitename');
$instance->short_description = $instance->description = DI::config()->get('config', 'info');
$instance->email = DI::config()->get('config', 'admin_email');
$instance->version = FRIENDICA_VERSION;
$instance->urls = null; // Not supported
$instance->stats = Stats::get();
$instance->thumbnail = $baseUrl->get() . (DI::config()->get('system', 'shortcut_icon') ?? 'images/friendica-32.png');
$instance->languages = [DI::config()->get('system', 'language')];
$instance->max_toot_chars = (int)DI::config()->get('config', 'api_import_size', DI::config()->get('config', 'max_import_size'));
$instance->registrations = ($register_policy != Register::CLOSED);
$instance->approval_required = ($register_policy == Register::APPROVE);
$instance->invites_enabled = false;
$instance->contact_account = [];
if (!empty(DI::config()->get('config', 'admin_email'))) {
$adminList = explode(',', str_replace(' ', '', DI::config()->get('config', 'admin_email')));
if (!empty($config->get('config', 'admin_email'))) {
$adminList = explode(',', str_replace(' ', '', $config->get('config', 'admin_email')));
$administrator = User::getByEmail($adminList[0], ['nickname']);
if (!empty($administrator)) {
$adminContact = DBA::selectFirst('contact', ['id'], ['nick' => $administrator['nickname'], 'self' => true]);
$instance->contact_account = DI::mstdnAccount()->createFromContactId($adminContact['id']);
$adminContact = $database->selectFirst('contact', ['id'], ['nick' => $administrator['nickname'], 'self' => true]);
$this->contact_account = DI::mstdnAccount()->createFromContactId($adminContact['id']);
}
}
return $instance;
}
}

View file

@ -71,7 +71,7 @@ class ScheduledStatus extends BaseDataTransferObject
'media_ids' => $media_ids,
'sensitive' => null,
'spoiler_text' => $parameters['item']['title'] ?? '',
'visibility' => $visibility[$parameters['item']['private']],
'visibility' => $visibility[$parameters['item']['private'] ?? 1],
'scheduled_at' => $this->scheduled_at,
'poll' => null,
'idempotency' => null,

View file

@ -22,7 +22,9 @@
namespace Friendica\Object\Api\Mastodon;
use Friendica\BaseDataTransferObject;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Protocol;
use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\DI;
@ -40,19 +42,12 @@ class Stats extends BaseDataTransferObject
/** @var int */
protected $domain_count = 0;
/**
* Creates a stats record
*
* @return Stats
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function get() {
$stats = new Stats();
if (!empty(DI::config()->get('system', 'nodeinfo'))) {
$stats->user_count = intval(DI::config()->get('nodeinfo', 'total_users'));
$stats->status_count = DI::config()->get('nodeinfo', 'local_posts') + DI::config()->get('nodeinfo', 'local_comments');
$stats->domain_count = DBA::count('gserver', ["`network` in (?, ?) AND NOT `failed`", Protocol::DFRN, Protocol::ACTIVITYPUB]);
public function __construct(IManageConfigValues $config, Database $database)
{
if (!empty($config->get('system', 'nodeinfo'))) {
$this->user_count = intval($config->get('nodeinfo', 'total_users'));
$this->status_count = $config->get('nodeinfo', 'local_posts') + $config->get('nodeinfo', 'local_comments');
$this->domain_count = $database->count('gserver', ["`network` in (?, ?) AND NOT `failed`", Protocol::DFRN, Protocol::ACTIVITYPUB]);
}
return $stats;
}
}

View file

@ -107,8 +107,8 @@ class Status extends BaseDataTransferObject
$this->in_reply_to_account_id = (string)$item['parent-author-id'];
}
$this->sensitive = $sensitive;
$this->spoiler_text = $item['title'];
$this->sensitive = $sensitive;
$this->spoiler_text = $item['title'] ?: $item['content-warning'];
$visibility = ['public', 'private', 'unlisted'];
$this->visibility = $visibility[$item['private']];

View file

@ -100,9 +100,9 @@ class Status extends BaseDataTransferObject
*/
public function __construct(string $text, string $statusnetHtml, string $friendicaHtml, array $item, User $author, User $owner, array $retweeted, array $quoted, array $geo, array $friendica_activities, array $entities, array $attachments, int $friendica_comments, bool $liked)
{
$this->id = (int)$item['id'];
$this->id_str = (string)$item['id'];
$this->statusnet_conversation_id = (int)$item['parent'];
$this->id = (int)$item['uri-id'];
$this->id_str = (string)$item['uri-id'];
$this->statusnet_conversation_id = (int)$item['parent-uri-id'];
$this->created_at = DateTimeFormat::utc($item['created'], DateTimeFormat::API);
@ -118,7 +118,7 @@ class Status extends BaseDataTransferObject
$this->friendica_title = $item['title'];
$this->statusnet_html = $statusnetHtml;
$this->friendica_html = $friendicaHtml;
$this->user = $author->toArray();
$this->user = $owner->toArray();
$this->friendica_author = $author->toArray();
$this->friendica_owner = $owner->toArray();
$this->truncated = false;

View file

@ -121,6 +121,29 @@ class Post
}
}
/**
* Fetch the privacy of the post
*
* @param array $item
* @return string
*/
private function fetchPrivacy(array $item):string
{
switch ($item['private']) {
case Item::PRIVATE:
$output = DI::l10n()->t('Private Message');
break;
case Item::PUBLIC:
$output = DI::l10n()->t('Public Message');
break;
case Item::UNLISTED:
$output = DI::l10n()->t('Unlisted Message');
break;
}
return $output;
}
/**
* Get data in a form usable by a conversation template
*
@ -170,12 +193,9 @@ class Post
$conv = $this->getThread();
$lock = ((($item['private'] == Item::PRIVATE) || (($item['uid'] == local_user()) && (strlen($item['allow_cid']) || strlen($item['allow_gid'])
|| strlen($item['deny_cid']) || strlen($item['deny_gid']))))
? DI::l10n()->t('Private Message')
: false);
$connector = !$item['global'] ? DI::l10n()->t('Connector Message') : false;
$privacy = $this->fetchPrivacy($item);
$lock = ($item['private'] == Item::PRIVATE) ? $privacy : false;
$connector = !in_array($item['network'], Protocol::NATIVE_SUPPORT) ? DI::l10n()->t('Connector Message') : false;
$shareable = in_array($conv->getProfileOwner(), [0, local_user()]) && $item['private'] != Item::PRIVATE;
$announceable = $shareable && in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::TWITTER]);
@ -416,12 +436,6 @@ class Post
$direction = [];
if (!empty($item['direction'])) {
$direction = $item['direction'];
} elseif (DI::config()->get('debug', 'show_direction')) {
$conversation = DBA::selectFirst('conversation', ['direction'], ['item-uri' => $item['uri']]);
if (!empty($conversation['direction']) && in_array($conversation['direction'], [1, 2])) {
$direction_title = [1 => DI::l10n()->t('Pushed'), 2 => DI::l10n()->t('Pulled')];
$direction = ['direction' => $conversation['direction'], 'title' => $direction_title[$conversation['direction']]];
}
}
$languages = [];
@ -469,6 +483,8 @@ class Post
'app' => $item['app'],
'created' => $ago,
'lock' => $lock,
'private' => $item['private'],
'privacy' => $privacy,
'connector' => $connector,
'location_html' => $location_html,
'indent' => $indent,
@ -875,22 +891,26 @@ class Post
$owner = User::getOwnerDataById($a->getLoggedInUserId());
if (!Feature::isEnabled(local_user(), 'explicit_mentions')) {
return '';
}
$item = PostModel::selectFirst(['author-addr', 'uri-id', 'network', 'gravity'], ['id' => $this->getId()]);
$item = PostModel::selectFirst(['author-addr', 'uri-id', 'network', 'gravity', 'content-warning'], ['id' => $this->getId()]);
if (!DBA::isResult($item) || empty($item['author-addr'])) {
// Should not happen
return '';
}
if (($item['author-addr'] != $owner['addr']) && (($item['gravity'] != GRAVITY_PARENT) || !in_array($item['network'], [Protocol::DIASPORA]))) {
$text = '@' . $item['author-addr'] . ' ';
if (!empty($item['content-warning']) && Feature::isEnabled(local_user(), 'add_abstract')) {
$text = '[abstract=' . Protocol::ACTIVITYPUB . ']' . $item['content-warning'] . "[/abstract]\n";
} else {
$text = '';
}
if (!Feature::isEnabled(local_user(), 'explicit_mentions')) {
return $text;
}
if (($item['author-addr'] != $owner['addr']) && (($item['gravity'] != GRAVITY_PARENT) || !in_array($item['network'], [Protocol::DIASPORA]))) {
$text .= '@' . $item['author-addr'] . ' ';
}
$terms = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION]);
foreach ($terms as $term) {
if (!$term['url']) {

View file

@ -192,8 +192,8 @@ class Processor
/**
* Update an existing event
*
* @param int $event_id
* @param array $activity
* @param int $event_id
* @param array $activity
*/
private static function updateEvent(int $event_id, array $activity)
{
@ -235,7 +235,7 @@ class Processor
if (empty($activity['directmessage']) && ($activity['id'] != $activity['reply-to-id']) && !Post::exists(['uri' => $activity['reply-to-id']])) {
Logger::notice('Parent not found. Try to refetch it.', ['parent' => $activity['reply-to-id']]);
self::fetchMissingActivity($activity['reply-to-id'], $activity);
self::fetchMissingActivity($activity['reply-to-id'], $activity, '', Receiver::COMPLETION_AUTO);
}
$item['diaspora_signed_text'] = $activity['diaspora:comment'] ?? '';
@ -306,7 +306,7 @@ class Processor
} else {
// Store the original actor in the "causer" fields to enable the check for ignored or blocked contacts
$item['causer-link'] = $item['owner-link'];
$item['causer-id'] = $item['owner-id'];
$item['causer-id'] = $item['owner-id'];
Logger::info('Use actor as causer.', ['id' => $item['owner-id'], 'actor' => $item['owner-link']]);
}
@ -526,6 +526,8 @@ class Processor
self::storeFromBody($item);
self::storeTags($item['uri-id'], $activity['tags']);
self::storeReceivers($item['uri-id'], $activity['receiver_urls'] ?? []);
$item['location'] = $activity['location'];
if (!empty($activity['latitude']) && !empty($activity['longitude'])) {
@ -551,7 +553,7 @@ class Processor
}
/**
* Generate a GUID out of an URL
* Generate a GUID out of an URL of an ActivityPub post.
*
* @param string $url message URL
* @return string with GUID
@ -570,6 +572,56 @@ class Processor
return $host_hash . '-'. hash('fnv164', $path) . '-'. hash('joaat', $path);
}
/**
* Checks if an incoming message is wanted
*
* @param array $activity
* @param array $item
* @return boolean Is the message wanted?
*/
private static function isSolicitedMessage(array $activity, array $item)
{
// The checks are split to improve the support when searching why a message was accepted.
if (count($activity['receiver']) != 1) {
// The message has more than one receiver, so it is wanted.
Logger::debug('Message has got several receivers - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]);
return true;
}
if ($item['private'] == Item::PRIVATE) {
// We only look at public posts here. Private posts are expected to be intentionally posted to the single receiver.
Logger::debug('Message is private - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]);
return true;
}
if (!empty($activity['from-relay'])) {
// We check relay posts at another place. When it arrived here, the message is already checked.
Logger::debug('Message is a relay post that is already checked - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]);
return true;
}
if (in_array($activity['completion-mode'] ?? Receiver::COMPLETION_NONE, [Receiver::COMPLETION_MANUAL, Receiver::COMPLETION_ANNOUCE])) {
// Manual completions and completions caused by reshares are allowed without any further checks.
Logger::debug('Message is in completion mode - accepted', ['mode' => $activity['completion-mode'], 'uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]);
return true;
}
if ($item['gravity'] != GRAVITY_PARENT) {
// We cannot reliably check at this point if a comment or activity belongs to an accepted post or needs to be fetched
// This can possibly be improved in the future.
Logger::debug('Message is no parent - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]);
return true;
}
$tags = array_column(Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]), 'name');
if (Relay::isSolicitedPost($tags, $item['body'], $item['author-id'], $item['uri'], Protocol::ACTIVITYPUB)) {
Logger::debug('Post is accepted because of the relay settings', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]);
return true;
} else {
return false;
}
}
/**
* Creates an item post
*
@ -587,6 +639,11 @@ class Processor
$stored = false;
ksort($activity['receiver']);
if (!self::isSolicitedMessage($activity, $item)) {
DBA::delete('item-uri', ['id' => $item['uri-id']]);
return;
}
foreach ($activity['receiver'] as $receiver) {
if ($receiver == -1) {
continue;
@ -642,10 +699,21 @@ class Processor
continue;
}
if (!($item['isForum'] ?? false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT) &&
($item['post-reason'] == Item::PR_BCC) && !Contact::isSharingByURL($activity['author'], $receiver)) {
Logger::info('Top level post via BCC from a non sharer, ignoring', ['uid' => $receiver, 'contact' => $item['contact-id']]);
continue;
if (!($item['isForum'] ?? false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT) && !Contact::isSharingByURL($activity['author'], $receiver)) {
if ($item['post-reason'] == Item::PR_BCC) {
Logger::info('Top level post via BCC from a non sharer, ignoring', ['uid' => $receiver, 'contact' => $item['contact-id']]);
continue;
}
if (
!empty($activity['thread-children-type'])
&& in_array($activity['thread-children-type'], Receiver::ACTIVITY_TYPES)
&& DI::pConfig()->get($receiver, 'system', 'accept_only_sharer') != Item::COMPLETION_LIKE
) {
Logger::info('Top level post from thread completion from a non sharer had been initiated via an activity, ignoring',
['type' => $activity['thread-children-type'], 'user' => $item['uid'], 'causer' => $item['causer-link'], 'author' => $activity['author'], 'url' => $item['uri']]);
continue;
}
}
$is_forum = false;
@ -657,7 +725,7 @@ class Processor
}
}
if (!$is_forum && DI::pConfig()->get($receiver, 'system', 'accept_only_sharer', false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT)) {
if (!$is_forum && DI::pConfig()->get($receiver, 'system', 'accept_only_sharer') == Item::COMPLETION_NONE && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT)) {
$skip = !Contact::isSharingByURL($activity['author'], $receiver);
if ($skip && (($activity['type'] == 'as:Announce') || ($item['isForum'] ?? false))) {
@ -745,6 +813,22 @@ class Processor
}
}
public static function storeReceivers(int $uriid, array $receivers)
{
foreach (['as:to' => Tag::TO, 'as:cc' => Tag::CC, 'as:bto' => Tag::BTO, 'as:bcc' => Tag::BCC] as $element => $type) {
if (!empty($receivers[$element])) {
foreach ($receivers[$element] as $receiver) {
if ($receiver == ActivityPub::PUBLIC_COLLECTION) {
$name = Receiver::PUBLIC_COLLECTION;
} else {
$name = trim(parse_url($receiver, PHP_URL_PATH), '/');
}
Tag::store($uriid, $type, $name, $receiver);
}
}
}
}
/**
* Creates an mail post
*
@ -814,10 +898,11 @@ class Processor
* @param string $url message URL
* @param array $child activity array with the child of this message
* @param string $relay_actor Relay actor
* @param int $completion Completion mode, see Receiver::COMPLETION_*
* @return string fetched message URL
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function fetchMissingActivity(string $url, array $child = [], string $relay_actor = '')
public static function fetchMissingActivity(string $url, array $child = [], string $relay_actor = '', int $completion = Receiver::COMPLETION_MANUAL)
{
if (!empty($child['receiver'])) {
$uid = ActivityPub\Receiver::getFirstUserFromReceivers($child['receiver']);
@ -881,10 +966,17 @@ class Processor
if (!empty($relay_actor)) {
$ldactivity['thread-completion'] = $ldactivity['from-relay'] = Contact::getIdForURL($relay_actor);
$ldactivity['completion-mode'] = Receiver::COMPLETION_RELAY;
} elseif (!empty($child['thread-completion'])) {
$ldactivity['thread-completion'] = $child['thread-completion'];
$ldactivity['completion-mode'] = $child['completion-mode'] ?? Receiver::COMPLETION_NONE;
} else {
$ldactivity['thread-completion'] = Contact::getIdForURL($actor);
$ldactivity['completion-mode'] = $completion;
}
if (!empty($child['type'])) {
$ldactivity['thread-children-type'] = $child['type'];
}
if (!empty($relay_actor) && !self::acceptIncomingMessage($ldactivity, $object['id'])) {

View file

@ -68,6 +68,12 @@ class Receiver
const TARGET_ANSWER = 6;
const TARGET_GLOBAL = 7;
const COMPLETION_NONE = 0;
const COMPLETION_ANNOUCE = 1;
const COMPLETION_RELAY = 2;
const COMPLETION_MANUAL = 3;
const COMPLETION_AUTO = 4;
/**
* Checks incoming message from the inbox
*
@ -190,7 +196,7 @@ class Receiver
return;
}
$id = Processor::fetchMissingActivity($object_id, [], $actor);
$id = Processor::fetchMissingActivity($object_id, [], $actor, self::COMPLETION_RELAY);
if (empty($id)) {
Logger::notice('Relayed message had not been fetched', ['id' => $object_id]);
return;
@ -263,7 +269,9 @@ class Receiver
{
$id = JsonLD::fetchElement($activity, '@id');
if (!empty($id) && !$trust_source) {
$fetched_activity = ActivityPub::fetchContent($id, $uid ?? 0);
$fetch_uid = $uid ?: self::getBestUserForActivity($activity);
$fetched_activity = ActivityPub::fetchContent($id, $fetch_uid);
if (!empty($fetched_activity)) {
$object = JsonLD::compact($fetched_activity);
$fetched_id = JsonLD::fetchElement($object, '@id');
@ -295,19 +303,25 @@ class Receiver
$reception_types[$data['uid']] = $data['type'] ?? self::TARGET_UNKNOWN;
}
$urls = self::getReceiverURL($activity);
// When it is a delivery to a personal inbox we add that user to the receivers
if (!empty($uid)) {
$additional = [$uid => $uid];
$receivers = array_replace($receivers, $additional);
if (empty($activity['thread-completion']) && (empty($reception_types[$uid]) || in_array($reception_types[$uid], [self::TARGET_UNKNOWN, self::TARGET_FOLLOWER, self::TARGET_ANSWER, self::TARGET_GLOBAL]))) {
$reception_types[$uid] = self::TARGET_BCC;
$owner = User::getOwnerDataById($uid);
if (!empty($owner['url'])) {
$urls['as:bcc'][] = $owner['url'];
}
}
} else {
// We possibly need some user to fetch private content,
// so we fetch the first out ot the list.
$uid = self::getFirstUserFromReceivers($receivers);
}
// We possibly need some user to fetch private content,
// so we fetch one out of the receivers if no uid is provided.
$fetch_uid = $uid ?: self::getBestUserForActivity($activity);
$object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
if (empty($object_id)) {
Logger::info('No object found');
@ -319,11 +333,11 @@ class Receiver
return [];
}
$object_type = self::fetchObjectType($activity, $object_id, $uid);
$object_type = self::fetchObjectType($activity, $object_id, $fetch_uid);
// Fetch the activity on Lemmy "Announce" messages (announces of activities)
if (($type == 'as:Announce') && in_array($object_type, array_merge(self::ACTIVITY_TYPES, ['as:Delete', 'as:Undo', 'as:Update']))) {
$data = ActivityPub::fetchContent($object_id, $uid);
$data = ActivityPub::fetchContent($object_id, $fetch_uid);
if (!empty($data)) {
$type = $object_type;
$activity = JsonLD::compact($data);
@ -331,7 +345,7 @@ class Receiver
// Some variables need to be refetched since the activity changed
$actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
$object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
$object_type = self::fetchObjectType($activity, $object_id, $uid);
$object_type = self::fetchObjectType($activity, $object_id, $fetch_uid);
}
}
@ -348,7 +362,7 @@ class Receiver
// Fetch the content only on activities where this matters
// We can receive "#emojiReaction" when fetching content from Hubzilla systems
// Always fetch on "Announce"
$object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source && ($type != 'as:Announce'), $uid);
$object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source && ($type != 'as:Announce'), $fetch_uid);
if (empty($object_data)) {
Logger::info("Object data couldn't be processed");
return [];
@ -396,7 +410,7 @@ class Receiver
// An Undo is done on the object of an object, so we need that type as well
if (($type == 'as:Undo') && !empty($object_data['object_object'])) {
$object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object'], $uid);
$object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object'], $fetch_uid);
}
}
@ -406,6 +420,12 @@ class Receiver
$object_data['object_type'] = $object_type;
}
foreach (['as:to', 'as:cc', 'as:bto', 'as:bcc'] as $element) {
if ((empty($object_data['receiver_urls'][$element]) || in_array($element, ['as:bto', 'as:bcc'])) && !empty($urls[$element])) {
$object_data['receiver_urls'][$element] = array_unique(array_merge($object_data['receiver_urls'][$element] ?? [], $urls[$element]));
}
}
$object_data['type'] = $type;
$object_data['actor'] = $actor;
$object_data['item_receiver'] = $receivers;
@ -516,6 +536,14 @@ class Receiver
$object_data['thread-completion'] = $activity['thread-completion'];
}
if (!empty($activity['completion-mode'])) {
$object_data['completion-mode'] = $activity['completion-mode'];
}
if (!empty($activity['thread-children-type'])) {
$object_data['thread-children-type'] = $activity['thread-children-type'];
}
// Internal flag for posts that arrived via relay
if (!empty($activity['from-relay'])) {
$object_data['from-relay'] = $activity['from-relay'];
@ -538,6 +566,7 @@ class Receiver
case 'as:Announce':
if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
$object_data['thread-completion'] = Contact::getIdForURL($actor);
$object_data['completion-mode'] = self::COMPLETION_ANNOUCE;
$item = ActivityPub\Processor::createItem($object_data);
if (empty($item)) {
@ -640,6 +669,61 @@ class Receiver
}
}
/**
* Fetch a user id from an activity array
*
* @param array $activity
* @param string $actor
*
* @return int user id
*/
public static function getBestUserForActivity(array $activity)
{
$uid = 0;
$actor = JsonLD::fetchElement($activity, 'as:actor', '@id') ?? '';
$receivers = self::getReceivers($activity, $actor);
foreach ($receivers as $receiver) {
if ($receiver['type'] == self::TARGET_GLOBAL) {
return 0;
}
if (empty($uid) || ($receiver['type'] == self::TARGET_TO)) {
$uid = $receiver['uid'];
}
}
// When we haven't found any user yet, we just chose a user who most likely could have access to the content
if (empty($uid)) {
$contact = Contact::selectFirst(['uid'], ['nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND]]);
if (!empty($contact['uid'])) {
$uid = $contact['uid'];
}
}
return $uid;
}
public static function getReceiverURL($activity)
{
$urls = [];
foreach (['as:to', 'as:cc', 'as:bto', 'as:bcc'] as $element) {
$receiver_list = JsonLD::fetchElementArray($activity, $element, '@id');
if (empty($receiver_list)) {
continue;
}
foreach ($receiver_list as $receiver) {
if ($receiver == self::PUBLIC_COLLECTION) {
$receiver = ActivityPub::PUBLIC_COLLECTION;
}
$urls[$element][] = $receiver;
}
}
return $urls;
}
/**
* Fetch the receiver list from an activity array
*
@ -1469,7 +1553,8 @@ class Receiver
$reception_types[$data['uid']] = $data['type'] ?? 0;
}
$object_data['receiver'] = $receivers;
$object_data['receiver_urls'] = self::getReceiverURL($object);
$object_data['receiver'] = $receivers;
$object_data['reception_type'] = $reception_types;
$object_data['unlisted'] = in_array(-1, $object_data['receiver']);

View file

@ -36,7 +36,6 @@ use Friendica\Model\GServer;
use Friendica\Model\Item;
use Friendica\Model\Photo;
use Friendica\Model\Post;
use Friendica\Model\Profile;
use Friendica\Model\Tag;
use Friendica\Model\User;
use Friendica\Network\HTTPException;
@ -49,6 +48,7 @@ use Friendica\Util\JsonLD;
use Friendica\Util\LDSignature;
use Friendica\Util\Map;
use Friendica\Util\Network;
use Friendica\Util\Strings;
use Friendica\Util\XML;
/**
@ -146,15 +146,16 @@ class Transmitter
/**
* Collects a list of contacts of the given owner
*
* @param array $owner Owner array
* @param int|array $rel The relevant value(s) contact.rel should match
* @param string $module The name of the relevant AP endpoint module (followers|following)
* @param integer $page Page number
* @param array $owner Owner array
* @param int|array $rel The relevant value(s) contact.rel should match
* @param string $module The name of the relevant AP endpoint module (followers|following)
* @param integer $page Page number
* @param string $requester URL of the requester
*
* @return array of owners
* @throws \Exception
*/
public static function getContacts($owner, $rel, $module, $page = null)
public static function getContacts($owner, $rel, $module, $page = null, string $requester = null)
{
$parameters = [
'rel' => $rel,
@ -179,8 +180,14 @@ class Transmitter
$data['totalItems'] = $total;
// When we hide our friends we will only show the pure number but don't allow more.
$profile = Profile::getByUID($owner['uid']);
if (!empty($profile['hide-friends'])) {
$show_contacts = empty($owner['hide-friends']);
// Allow fetching the contact list when the requester is part of the list.
if (($owner['page-flags'] == User::PAGE_FLAGS_PRVGROUP) && !empty($requester)) {
$show_contacts = DBA::exists('contact', ['nurl' => Strings::normaliseLink($requester), 'uid' => $owner['uid'], 'blocked' => false]);
}
if (!$show_contacts) {
return $data;
}
@ -417,42 +424,34 @@ class Transmitter
}
/**
* Returns an array with permissions of a given item array
* Returns an array with permissions of the thread parent of the given item array
*
* @param array $item
* @param bool $is_forum_thread
*
* @return array with permissions
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function fetchPermissionBlockFromConversation($item)
private static function fetchPermissionBlockFromThreadParent(array $item, bool $is_forum_thread)
{
if (empty($item['thr-parent'])) {
if (empty($item['thr-parent-id'])) {
return [];
}
$condition = ['item-uri' => $item['thr-parent'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB];
$conversation = DBA::selectFirst('conversation', ['source'], $condition);
if (!DBA::isResult($conversation)) {
$parent = Post::selectFirstPost(['author-link'], ['uri-id' => $item['thr-parent-id']]);
if (empty($parent)) {
return [];
}
$permissions = [
'to' => [],
'to' => [$parent['author-link']],
'cc' => [],
'bto' => [],
'bcc' => [],
];
$activity = json_decode($conversation['source'], true);
$actor = JsonLD::fetchElement($activity, 'actor', 'id');
if (!empty($actor)) {
$permissions['to'][] = $actor;
$profile = APContact::getByURL($actor);
} else {
$profile = [];
}
$parent_profile = APContact::getByURL($parent['author-link']);
$item_profile = APContact::getByURL($item['author-link']);
$exclude[] = $item['author-link'];
@ -461,26 +460,17 @@ class Transmitter
$exclude[] = $item['owner-link'];
}
foreach (['to', 'cc', 'bto', 'bcc'] as $element) {
if (empty($activity[$element])) {
continue;
}
if (is_string($activity[$element])) {
$activity[$element] = [$activity[$element]];
}
foreach ($activity[$element] as $receiver) {
if (empty($receiver)) {
continue;
}
if (!empty($profile['followers']) && $receiver == $profile['followers'] && !empty($item_profile['followers'])) {
$permissions[$element][] = $item_profile['followers'];
} elseif (!in_array($receiver, $exclude)) {
$permissions[$element][] = $receiver;
$type = [Tag::TO => 'to', Tag::CC => 'cc', Tag::BTO => 'bto', Tag::BCC => 'bcc'];
foreach (Tag::getByURIId($item['thr-parent-id'], [Tag::TO, Tag::CC, Tag::BTO, Tag::BCC]) as $receiver) {
if (!empty($parent_profile['followers']) && $receiver['url'] == $parent_profile['followers'] && !empty($item_profile['followers'])) {
if (!$is_forum_thread) {
$permissions[$type[$receiver['type']]][] = $item_profile['followers'];
}
} elseif (!in_array($receiver['url'], $exclude)) {
$permissions[$type[$receiver['type']]][] = $receiver['url'];
}
}
return $permissions;
}
@ -502,28 +492,33 @@ class Transmitter
/**
* Creates an array of permissions from an item thread
*
* @param array $item Item array
* @param boolean $blindcopy addressing via "bcc" or "cc"?
* @param integer $last_id Last item id for adding receivers
* @param boolean $forum_mode "true" means that we are sending content to a forum
* @param array $item Item array
* @param boolean $blindcopy addressing via "bcc" or "cc"?
* @param integer $last_id Last item id for adding receivers
*
* @return array with permission data
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function createPermissionBlockForItem($item, $blindcopy, $last_id = 0, $forum_mode = false)
private static function createPermissionBlockForItem($item, $blindcopy, $last_id = 0)
{
if ($last_id == 0) {
$last_id = $item['id'];
}
$always_bcc = false;
$is_forum = false;
$follower = '';
// Check if we should always deliver our stuff via BCC
if (!empty($item['uid'])) {
$profile = User::getOwnerDataById($item['uid']);
if (!empty($profile)) {
$always_bcc = $profile['hide-friends'];
$owner = User::getOwnerDataById($item['uid']);
if (!empty($owner)) {
$always_bcc = $owner['hide-friends'];
$is_forum = ($owner['account-type'] == User::ACCOUNT_TYPE_COMMUNITY) && $owner['manually-approve'];
$profile = APContact::getByURL($owner['url'], false);
$follower = $profile['followers'] ?? '';
}
}
@ -531,6 +526,14 @@ class Transmitter
$always_bcc = true;
}
$parent = Post::selectFirst(['causer-link', 'post-reason'], ['id' => $item['parent']]);
if (($parent['post-reason'] == Item::PR_ANNOUNCEMENT) && !empty($parent['causer-link'])) {
$profile = APContact::getByURL($parent['causer-link'], false);
$is_forum_thread = isset($profile['type']) && $profile['type'] == 'Group';
} else {
$is_forum_thread = false;
}
if (self::isAnnounce($item) || DI::config()->get('debug', 'total_ap_delivery') || self::isAPPost($last_id)) {
// Will be activated in a later step
$networks = Protocol::FEDERATED;
@ -561,7 +564,7 @@ class Transmitter
$data['cc'][] = $announce['actor']['url'];
}
$data = array_merge($data, self::fetchPermissionBlockFromConversation($item));
$data = array_merge($data, self::fetchPermissionBlockFromThreadParent($item, $is_forum_thread));
// Check if the item is completely public or unlisted
if ($item['private'] == Item::PUBLIC) {
@ -593,30 +596,41 @@ class Transmitter
continue;
}
if (!empty($profile = APContact::getByURL($contact['url'], false))) {
$profile = APContact::getByURL($term['url'], false);
if (!empty($profile)) {
if ($term['type'] == Tag::EXCLUSIVE_MENTION) {
$exclusive = true;
if (!empty($profile['followers']) && ($profile['type'] == 'Group')) {
$data['cc'][] = $profile['followers'];
}
}
$data['to'][] = $profile['url'];
}
}
}
foreach ($receiver_list as $receiver) {
$contact = DBA::selectFirst('contact', ['url', 'hidden', 'network', 'protocol', 'gsid'], ['id' => $receiver, 'network' => Protocol::FEDERATED]);
if (!DBA::isResult($contact) || !self::isAPContact($contact, $networks)) {
continue;
}
if ($is_forum && !$exclusive && !empty($follower)) {
$data['cc'][] = $follower;
} elseif (!$exclusive) {
foreach ($receiver_list as $receiver) {
$contact = DBA::selectFirst('contact', ['url', 'hidden', 'network', 'protocol', 'gsid'], ['id' => $receiver, 'network' => Protocol::FEDERATED]);
if (!DBA::isResult($contact) || !self::isAPContact($contact, $networks)) {
continue;
}
if (!empty($profile = APContact::getByURL($contact['url'], false))) {
if ($contact['hidden'] || $always_bcc) {
$data['bcc'][] = $profile['url'];
} else {
$data['cc'][] = $profile['url'];
if (!empty($profile = APContact::getByURL($contact['url'], false))) {
if ($contact['hidden'] || $always_bcc) {
$data['bcc'][] = $profile['url'];
} else {
$data['cc'][] = $profile['url'];
}
}
}
}
}
if (!empty($item['parent'])) {
$parents = Post::select(['id', 'author-link', 'owner-link', 'gravity', 'uri'], ['parent' => $item['parent']]);
$parents = Post::select(['id', 'author-link', 'owner-link', 'gravity', 'uri'], ['parent' => $item['parent']], ['order' => ['id']]);
while ($parent = Post::fetch($parents)) {
if ($parent['gravity'] == GRAVITY_PARENT) {
$profile = APContact::getByURL($parent['owner-link'], false);
@ -630,15 +644,13 @@ class Transmitter
$data['to'][] = $profile['url'];
} else {
$data['cc'][] = $profile['url'];
if (($item['private'] != Item::PRIVATE) && !empty($actor_profile['followers'])) {
if (($item['private'] != Item::PRIVATE) && !empty($actor_profile['followers'])&& !$is_forum_thread) {
$data['cc'][] = $actor_profile['followers'];
}
}
} elseif (!$exclusive) {
} elseif (!$exclusive && !$is_forum_thread) {
// Public thread parent post always are directed to the followers.
// This mustn't be done by posts that are directed to forum servers via the exclusive mention.
// But possibly in that case we could add the "followers" collection of the forum to the message.
if (($item['private'] != Item::PRIVATE) && !$forum_mode) {
if ($item['private'] != Item::PRIVATE) {
$data['cc'][] = $actor_profile['followers'];
}
}
@ -700,6 +712,19 @@ class Transmitter
unset($receivers['bcc']);
}
foreach (['to' => Tag::TO, 'cc' => Tag::CC, 'bcc' => Tag::BCC] as $element => $type) {
if (!empty($receivers[$element])) {
foreach ($receivers[$element] as $receiver) {
if ($receiver == ActivityPub::PUBLIC_COLLECTION) {
$name = Receiver::PUBLIC_COLLECTION;
} else {
$name = trim(parse_url($receiver, PHP_URL_PATH), '/');
}
Tag::store($item['uri-id'], $type, $name, $receiver);
}
}
}
return $receivers;
}
@ -804,18 +829,17 @@ class Transmitter
/**
* Fetches an array of inboxes for the given item and user
*
* @param array $item Item array
* @param integer $uid User ID
* @param boolean $personal fetch personal inboxes
* @param integer $last_id Last item id for adding receivers
* @param boolean $forum_mode "true" means that we are sending content to a forum
* @param array $item Item array
* @param integer $uid User ID
* @param boolean $personal fetch personal inboxes
* @param integer $last_id Last item id for adding receivers
* @return array with inboxes
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function fetchTargetInboxes($item, $uid, $personal = false, $last_id = 0, $forum_mode = false)
public static function fetchTargetInboxes($item, $uid, $personal = false, $last_id = 0)
{
$permissions = self::createPermissionBlockForItem($item, true, $last_id, $forum_mode);
$permissions = self::createPermissionBlockForItem($item, true, $last_id);
if (empty($permissions)) {
return [];
}
@ -898,6 +922,7 @@ class Transmitter
$mail['title'] = '';
}
$mail['content-warning'] = '';
$mail['author-link'] = $mail['owner-link'] = $mail['from-url'];
$mail['owner-id'] = $mail['author-id'];
$mail['allow_cid'] = '<'.$mail['contact-id'].'>';
@ -1072,20 +1097,6 @@ class Transmitter
return false;
}
// In case of a forum post ensure to return the original post if author and forum are on the same machine
if (($item['gravity'] == GRAVITY_PARENT) && !empty($item['forum_mode'])) {
$author = Contact::getById($item['author-id'], ['nurl']);
if (!empty($author['nurl'])) {
$self = Contact::selectFirst(['uid'], ['nurl' => $author['nurl'], 'self' => true]);
if (!empty($self['uid'])) {
$forum_item = Post::selectFirst(Item::DELIVER_FIELDLIST, ['uri-id' => $item['uri-id'], 'uid' => $self['uid']]);
if (DBA::isResult($forum_item)) {
$item = $forum_item;
}
}
}
}
if (empty($item['uri-id'])) {
Logger::warning('Item without uri-id', ['item' => $item]);
return false;
@ -1408,7 +1419,7 @@ class Transmitter
*/
private static function isSensitive($uri_id)
{
return DBA::exists('tag-view', ['uri-id' => $uri_id, 'name' => 'nsfw']);
return DBA::exists('tag-view', ['uri-id' => $uri_id, 'name' => 'nsfw', 'type' => Tag::HASHTAG]);
}
/**

View file

@ -925,9 +925,9 @@ class DFRN
foreach ($mentioned as $mention) {
$condition = ['uid' => $owner["uid"], 'nurl' => Strings::normaliseLink($mention)];
$contact = DBA::selectFirst('contact', ['forum', 'prv'], $condition);
$contact = DBA::selectFirst('contact', ['contact-type'], $condition);
if (DBA::isResult($contact) && ($contact["forum"] || $contact["prv"])) {
if (DBA::isResult($contact) && ($contact['contact-type'] == Contact::TYPE_COMMUNITY)) {
XML::addElement(
$doc,
$entry,
@ -1547,7 +1547,7 @@ class DFRN
if ($item["thr-parent"] != $item["uri"]) {
$community = false;
if ($importer["page-flags"] == User::PAGE_FLAGS_COMMUNITY || $importer["page-flags"] == User::PAGE_FLAGS_PRVGROUP) {
if ($importer['account-type'] == User::ACCOUNT_TYPE_COMMUNITY) {
$sql_extra = "";
$community = true;
Logger::notice("possible community action");
@ -1557,22 +1557,11 @@ class DFRN
// was the top-level post for this action written by somebody on this site?
// Specifically, the recipient?
$parent = Post::selectFirst(['forum_mode', 'wall'],
$parent = Post::selectFirst(['wall'],
["`uri` = ? AND `uid` = ?" . $sql_extra, $item["thr-parent"], $importer["importer_uid"]]);
$is_a_remote_action = DBA::isResult($parent);
/*
* Does this have the characteristics of a community or private group action?
* If it's an action to a wall post on a community/prvgroup page it's a
* valid community action. Also forum_mode makes it valid for sure.
* If neither, it's not.
*/
if ($is_a_remote_action && $community && (!$parent["forum_mode"]) && (!$parent["wall"])) {
$is_a_remote_action = false;
Logger::notice("not a community action");
}
if ($is_a_remote_action) {
return DFRN::REPLY_RC;
} else {
@ -1679,7 +1668,7 @@ class DFRN
}
if ($activity->match($item["verb"], Activity::UNFRIEND)) {
Logger::notice("Lost sharer");
Contact::removeSharer($importer, $contact, $item);
Contact::removeSharer($contact);
return false;
}
} else {
@ -1780,19 +1769,34 @@ class DFRN
* Checks if an incoming message is wanted
*
* @param array $item
* @param array $imporer
* @return boolean Is the message wanted?
*/
private static function isSolicitedMessage(array $item)
private static function isSolicitedMessage(array $item, array $importer)
{
if (DBA::exists('contact', ["`nurl` = ? AND `uid` != ? AND `rel` IN (?, ?)",
Strings::normaliseLink($item["author-link"]), 0, Contact::FRIEND, Contact::SHARING])) {
Logger::info('Author has got followers - accepted', ['uri' => $item['uri'], 'author' => $item["author-link"]]);
Logger::debug('Author has got followers - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $item["author-link"]]);
return true;
}
$taglist = Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]);
$tags = array_column($taglist, 'name');
return Relay::isSolicitedPost($tags, $item['body'], $item['author-id'], $item['uri'], Protocol::DFRN);
if ($importer['importer_uid'] != 0) {
Logger::debug('Message is directed to a user - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'importer' => $importer['importer_uid']]);
return true;
}
if ($item['uri'] != $item['thr-parent']) {
Logger::debug('Message is no parent - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]);
return true;
}
$tags = array_column(Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]), 'name');
if (Relay::isSolicitedPost($tags, $item['body'], $item['author-id'], $item['uri'], Protocol::DFRN)) {
Logger::debug('Post is accepted because of the relay settings', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $item["author-link"]]);
return true;
} else {
return false;
}
}
/**
@ -1993,11 +1997,9 @@ class DFRN
}
// Check if the message is wanted
if (($importer['importer_uid'] == 0) && ($item['uri'] == $item['thr-parent'])) {
if (!self::isSolicitedMessage($item)) {
DBA::delete('item-uri', ['uri' => $item['uri']]);
return 403;
}
if (!self::isSolicitedMessage($item, $importer)) {
DBA::delete('item-uri', ['uri' => $item['uri']]);
return 403;
}
// Get the type of the item (Top level post, reply or remote reply)
@ -2381,14 +2383,11 @@ class DFRN
return false;
}
$user = DBA::selectFirst('user', ['page-flags', 'nickname'], ['uid' => $uid]);
$user = DBA::selectFirst('user', ['account-type', 'nickname'], ['uid' => $uid]);
if (!DBA::isResult($user)) {
return false;
}
$community_page = ($user['page-flags'] == User::PAGE_FLAGS_COMMUNITY);
$prvgroup = ($user['page-flags'] == User::PAGE_FLAGS_PRVGROUP);
$link = Strings::normaliseLink(DI::baseUrl() . '/profile/' . $user['nickname']);
/*
@ -2411,7 +2410,7 @@ class DFRN
return false;
}
return $community_page || $prvgroup;
return ($user['account-type'] == User::ACCOUNT_TYPE_COMMUNITY);
}
/**

View file

@ -56,6 +56,10 @@ use SimpleXMLElement;
*/
class Diaspora
{
const PUSHED = 0;
const FETCHED = 1;
const FORCED_FETCH = 2;
/**
* Return a list of participating contacts for a thread
*
@ -449,14 +453,14 @@ class Diaspora
/**
* Dispatches public messages and find the fitting receivers
*
* @param array $msg The post that will be dispatched
* @param bool $fetched The message had been fetched (default "false")
* @param array $msg The post that will be dispatched
* @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
*
* @return int The message id of the generated message, "true" or "false" if there was an error
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function dispatchPublic($msg, bool $fetched = false)
public static function dispatchPublic($msg, int $direction)
{
$enabled = intval(DI::config()->get("system", "diaspora_enabled"));
if (!$enabled) {
@ -470,7 +474,7 @@ class Diaspora
}
$importer = ["uid" => 0, "page-flags" => User::PAGE_FLAGS_FREELOVE];
$success = self::dispatch($importer, $msg, $fields, $fetched);
$success = self::dispatch($importer, $msg, $fields, $direction);
return $success;
}
@ -478,16 +482,16 @@ class Diaspora
/**
* Dispatches the different message types to the different functions
*
* @param array $importer Array of the importer user
* @param array $msg The post that will be dispatched
* @param SimpleXMLElement $fields SimpleXML object that contains the message
* @param bool $fetched The message had been fetched (default "false")
* @param array $importer Array of the importer user
* @param array $msg The post that will be dispatched
* @param SimpleXMLElement $fields SimpleXML object that contains the message
* @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
*
* @return int The message id of the generated message, "true" or "false" if there was an error
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function dispatch(array $importer, $msg, SimpleXMLElement $fields = null, bool $fetched = false)
public static function dispatch(array $importer, $msg, SimpleXMLElement $fields = null, int $direction = self::PUSHED)
{
// The sender is the handle of the contact that sent the message.
// This will often be different with relayed messages (for example "like" and "comment")
@ -520,7 +524,7 @@ class Diaspora
return self::receiveAccountDeletion($fields);
case "comment":
return self::receiveComment($importer, $sender, $fields, $msg["message"], $fetched);
return self::receiveComment($importer, $sender, $fields, $msg["message"], $direction);
case "contact":
if (!$private) {
@ -537,7 +541,7 @@ class Diaspora
return self::receiveConversation($importer, $msg, $fields);
case "like":
return self::receiveLike($importer, $sender, $fields, $fetched);
return self::receiveLike($importer, $sender, $fields, $direction);
case "message":
if (!$private) {
@ -551,7 +555,7 @@ class Diaspora
Logger::notice('Message with type ' . $type . ' is not private, quitting.');
return false;
}
return self::receiveParticipation($importer, $fields, $fetched);
return self::receiveParticipation($importer, $fields, $direction);
case "photo": // Not implemented
return self::receivePhoto($importer, $fields);
@ -567,13 +571,13 @@ class Diaspora
return self::receiveProfile($importer, $fields);
case "reshare":
return self::receiveReshare($importer, $fields, $msg["message"], $fetched);
return self::receiveReshare($importer, $fields, $msg["message"], $direction);
case "retraction":
return self::receiveRetraction($importer, $sender, $fields);
case "status_message":
return self::receiveStatusMessage($importer, $fields, $msg["message"], $fetched);
return self::receiveStatusMessage($importer, $fields, $msg["message"], $direction);
default:
Logger::notice("Unknown message type ".$type);
@ -837,8 +841,7 @@ class Diaspora
// It is deactivated by now, due to side effects. See issue https://github.com/friendica/friendica/pull/4033
// It is not removed by now. Possibly the code is needed?
//if (!$is_comment && $contact["rel"] == Contact::FOLLOWER && in_array($importer["page-flags"], array(User::PAGE_FLAGS_FREELOVE))) {
// DBA::update(
// 'contact',
// Contact::update(
// array('rel' => Contact::FRIEND, 'writable' => true),
// array('id' => $contact["id"], 'uid' => $contact["uid"])
// );
@ -858,10 +861,6 @@ class Diaspora
} elseif (($contact["rel"] == Contact::SHARING) || ($contact["rel"] == Contact::FRIEND)) {
// Yes, then it is fine.
return true;
// Is it a post to a community?
} elseif (($contact["rel"] == Contact::FOLLOWER) && in_array($importer["page-flags"], [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_PRVGROUP])) {
// That's good
return true;
// Is the message a global user or a comment?
} elseif (($importer["uid"] == 0) || $is_comment) {
// Messages for the global users and comments are always accepted
@ -996,8 +995,8 @@ class Diaspora
*/
private static function fetchGuidSub($match, $item)
{
if (!self::storeByGuid($match[1], $item["author-link"])) {
self::storeByGuid($match[1], $item["owner-link"]);
if (!self::storeByGuid($match[1], $item["author-link"], true)) {
self::storeByGuid($match[1], $item["owner-link"], true);
}
}
@ -1006,13 +1005,13 @@ class Diaspora
*
* @param string $guid the message guid
* @param string $server The server address
* @param int $uid The user id of the user
* @param bool $force Forced fetch
*
* @return int the message id of the stored message or false
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function storeByGuid($guid, $server, $uid = 0)
private static function storeByGuid($guid, $server, $force)
{
$serverparts = parse_url($server);
@ -1033,7 +1032,7 @@ class Diaspora
Logger::info("Successfully fetched item ".$guid." from ".$server);
// Now call the dispatcher
return self::dispatchPublic($msg, true);
return self::dispatchPublic($msg, $force ? self::FORCED_FETCH : self::FETCHED);
}
/**
@ -1141,7 +1140,7 @@ class Diaspora
}
Logger::info('Fetch GUID from origin', ['guid' => $guid, 'server' => $matches[1]]);
$ret = self::storeByGuid($guid, $matches[1], $uid);
$ret = self::storeByGuid($guid, $matches[1], true);
Logger::info('Result', ['ret' => $ret]);
$item = Post::selectFirst(['id'], ['guid' => $guid, 'uid' => $uid]);
@ -1175,11 +1174,11 @@ class Diaspora
if (!DBA::isResult($item)) {
$person = FContact::getByURL($author);
$result = self::storeByGuid($guid, $person["url"], $uid);
$result = self::storeByGuid($guid, $person["url"], false);
// We don't have an url for items that arrived at the public dispatcher
if (!$result && !empty($contact["url"])) {
$result = self::storeByGuid($guid, $contact["url"], $uid);
$result = self::storeByGuid($guid, $contact["url"], false);
}
if ($result) {
@ -1446,17 +1445,17 @@ class Diaspora
/**
* Processes an incoming comment
*
* @param array $importer Array of the importer user
* @param string $sender The sender of the message
* @param object $data The message object
* @param string $xml The original XML of the message
* @param bool $fetched The message had been fetched and not pushed
* @param array $importer Array of the importer user
* @param string $sender The sender of the message
* @param object $data The message object
* @param string $xml The original XML of the message
* @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
*
* @return int The message id of the generated comment or "false" if there was an error
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function receiveComment(array $importer, $sender, $data, $xml, bool $fetched)
private static function receiveComment(array $importer, $sender, $data, $xml, int $direction)
{
$author = XML::unescape($data->author);
$guid = XML::unescape($data->guid);
@ -1517,7 +1516,7 @@ class Diaspora
$datarray["owner-id"] = Contact::getIdForURL($contact["url"], 0);
// Will be overwritten for sharing accounts in Item::insert
if ($fetched) {
if (in_array($direction, [self::FETCHED, self::FORCED_FETCH])) {
$datarray["post-reason"] = Item::PR_FETCHED;
} elseif ($datarray["uid"] == 0) {
$datarray["post-reason"] = Item::PR_GLOBAL;
@ -1539,7 +1538,7 @@ class Diaspora
$datarray["protocol"] = Conversation::PARCEL_DIASPORA;
$datarray["source"] = $xml;
$datarray["direction"] = $fetched ? Conversation::PULL : Conversation::PUSH;
$datarray["direction"] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH;
$datarray["changed"] = $datarray["created"] = $datarray["edited"] = $created_at;
@ -1703,15 +1702,16 @@ class Diaspora
/**
* Processes "like" messages
*
* @param array $importer Array of the importer user
* @param string $sender The sender of the message
* @param object $data The message object
* @param array $importer Array of the importer user
* @param string $sender The sender of the message
* @param object $data The message object
* @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
*
* @return int The message id of the generated like or "false" if there was an error
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function receiveLike(array $importer, $sender, $data, bool $fetched)
private static function receiveLike(array $importer, $sender, $data, int $direction)
{
$author = XML::unescape($data->author);
$guid = XML::unescape($data->guid);
@ -1764,7 +1764,7 @@ class Diaspora
$datarray = [];
$datarray["protocol"] = Conversation::PARCEL_DIASPORA;
$datarray["direction"] = $fetched ? Conversation::PULL : Conversation::PUSH;
$datarray["direction"] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH;
$datarray["uid"] = $importer["uid"];
$datarray["contact-id"] = $author_contact["cid"];
@ -1890,14 +1890,15 @@ class Diaspora
/**
* Processes participations - unsupported by now
*
* @param array $importer Array of the importer user
* @param object $data The message object
* @param array $importer Array of the importer user
* @param object $data The message object
* @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
*
* @return bool success
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function receiveParticipation(array $importer, $data, bool $fetched)
private static function receiveParticipation(array $importer, $data, int $direction)
{
$author = strtolower(XML::unescape($data->author));
$guid = XML::unescape($data->guid);
@ -1942,7 +1943,7 @@ class Diaspora
$datarray = [];
$datarray["protocol"] = Conversation::PARCEL_DIASPORA;
$datarray["direction"] = $fetched ? Conversation::PULL : Conversation::PUSH;
$datarray["direction"] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH;
$datarray["uid"] = $importer["uid"];
$datarray["contact-id"] = $author_contact["cid"];
@ -2127,8 +2128,7 @@ class Diaspora
private static function receiveRequestMakeFriend(array $importer, array $contact)
{
if ($contact["rel"] == Contact::SHARING) {
DBA::update(
'contact',
Contact::update(
['rel' => Contact::FRIEND, 'writable' => true],
['id' => $contact["id"], 'uid' => $importer["uid"]]
);
@ -2296,12 +2296,12 @@ class Diaspora
$server = "https://".substr($orig_author, strpos($orig_author, "@") + 1);
Logger::notice("1st try: reshared message ".$guid." will be fetched via SSL from the server ".$server);
$stored = self::storeByGuid($guid, $server);
$stored = self::storeByGuid($guid, $server, true);
if (!$stored) {
$server = "http://".substr($orig_author, strpos($orig_author, "@") + 1);
Logger::notice("2nd try: reshared message ".$guid." will be fetched without SSL from the server ".$server);
$stored = self::storeByGuid($guid, $server);
$stored = self::storeByGuid($guid, $server, true);
}
if ($stored) {
@ -2382,15 +2382,16 @@ class Diaspora
/**
* Processes a reshare message
*
* @param array $importer Array of the importer user
* @param object $data The message object
* @param string $xml The original XML of the message
* @param array $importer Array of the importer user
* @param object $data The message object
* @param string $xml The original XML of the message
* @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
*
* @return int the message id
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function receiveReshare(array $importer, $data, $xml, bool $fetched)
private static function receiveReshare(array $importer, $data, $xml, int $direction)
{
$author = XML::unescape($data->author);
$guid = XML::unescape($data->guid);
@ -2444,7 +2445,7 @@ class Diaspora
$datarray["protocol"] = Conversation::PARCEL_DIASPORA;
$datarray["source"] = $xml;
$datarray["direction"] = $fetched ? Conversation::PULL : Conversation::PUSH;
$datarray["direction"] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH;
/// @todo Copy tag data from original post
@ -2615,24 +2616,34 @@ class Diaspora
/**
* Checks if an incoming message is wanted
*
* @param string $url
* @param integer $uriid
* @param array $item
* @param string $author
* @param string $body
* @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
*
* @return boolean Is the message wanted?
*/
private static function isSolicitedMessage(string $url, int $uriid, string $author, string $body)
private static function isSolicitedMessage(array $item, string $author, string $body, int $direction)
{
$contact = Contact::getByURL($author);
if (DBA::exists('contact', ["`nurl` = ? AND `uid` != ? AND `rel` IN (?, ?)",
$contact['nurl'], 0, Contact::FRIEND, Contact::SHARING])) {
Logger::info('Author has got followers - accepted', ['url' => $url, 'author' => $author]);
Logger::debug('Author has got followers - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $author]);
return true;
}
$taglist = Tag::getByURIId($uriid, [Tag::HASHTAG]);
$tags = array_column($taglist, 'name');
return Relay::isSolicitedPost($tags, $body, $contact['id'], $url, Protocol::DIASPORA);
if ($direction == self::FORCED_FETCH) {
Logger::debug('Post is a forced fetch - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $author]);
return true;
}
$tags = array_column(Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]), 'name');
if (Relay::isSolicitedPost($tags, $body, $contact['id'], $item['uri'], Protocol::DIASPORA)) {
Logger::debug('Post is accepted because of the relay settings', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $author]);
return true;
} else {
return false;
}
}
/**
@ -2658,15 +2669,16 @@ class Diaspora
/**
* Receives status messages
*
* @param array $importer Array of the importer user
* @param SimpleXMLElement $data The message object
* @param string $xml The original XML of the message
* @param bool $fetched The message had been fetched and not pushed
* @param array $importer Array of the importer user
* @param SimpleXMLElement $data The message object
* @param string $xml The original XML of the message
* @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
*
* @return int The message id of the newly created item
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function receiveStatusMessage(array $importer, SimpleXMLElement $data, $xml, bool $fetched)
private static function receiveStatusMessage(array $importer, SimpleXMLElement $data, $xml, int $direction)
{
$author = XML::unescape($data->author);
$guid = XML::unescape($data->guid);
@ -2744,9 +2756,9 @@ class Diaspora
$datarray["protocol"] = Conversation::PARCEL_DIASPORA;
$datarray["source"] = $xml;
$datarray["direction"] = $fetched ? Conversation::PULL : Conversation::PUSH;
$datarray["direction"] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH;
if ($fetched) {
if (in_array($direction, [self::FETCHED, self::FORCED_FETCH])) {
$datarray["post-reason"] = Item::PR_FETCHED;
} elseif ($datarray["uid"] == 0) {
$datarray["post-reason"] = Item::PR_GLOBAL;
@ -2758,7 +2770,7 @@ class Diaspora
self::storeMentions($datarray['uri-id'], $text);
Tag::storeRawTagsFromBody($datarray['uri-id'], $datarray["body"]);
if (!$fetched && !self::isSolicitedMessage($datarray["uri"], $datarray['uri-id'], $author, $body)) {
if (!self::isSolicitedMessage($datarray, $author, $body, $direction)) {
DBA::delete('item-uri', ['uri' => $datarray['uri']]);
return false;
}
@ -3473,9 +3485,8 @@ class Diaspora
private static function prependParentAuthorMention($body, $profile_url)
{
$profile = Contact::getByURL($profile_url, false, ['addr', 'name', 'contact-type']);
$profile = Contact::getByURL($profile_url, false, ['addr', 'name']);
if (!empty($profile['addr'])
&& $profile['contact-type'] != Contact::TYPE_COMMUNITY
&& !strstr($body, $profile['addr'])
&& !strstr($body, $profile_url)
) {

View file

@ -27,6 +27,7 @@ use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML;
use Friendica\Model\Item;
use Friendica\Util\Strings;
use \IMAP\Connection;
/**
* Email class
@ -37,7 +38,7 @@ class Email
* @param string $mailbox The mailbox name
* @param string $username The username
* @param string $password The password
* @return resource
* @return Connection|resource
* @throws \Exception
*/
public static function connect($mailbox, $username, $password)
@ -50,7 +51,7 @@ class Email
$errors = imap_errors();
if (!empty($errors)) {
Logger::notice('IMAP Errors occured', ['errora' => $errors]);
Logger::notice('IMAP Errors occured', ['errors' => $errors]);
}
$alerts = imap_alerts();
@ -62,12 +63,12 @@ class Email
}
/**
* @param resource $mbox mailbox
* @param string $email_addr email
* @param Connection|resource $mbox mailbox
* @param string $email_addr email
* @return array
* @throws \Exception
*/
public static function poll($mbox, $email_addr)
public static function poll($mbox, $email_addr): array
{
if (!$mbox || !$email_addr) {
return [];
@ -112,8 +113,8 @@ class Email
}
/**
* @param resource $mbox mailbox
* @param integer $uid user id
* @param Connection|resource $mbox mailbox
* @param integer $uid user id
* @return mixed
*/
public static function messageMeta($mbox, $uid)
@ -123,13 +124,13 @@ class Email
}
/**
* @param resource $mbox mailbox
* @param integer $uid user id
* @param string $reply reply
* @param Connection|resource $mbox mailbox
* @param integer $uid user id
* @param string $reply reply
* @return array
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function getMessage($mbox, $uid, $reply, $item)
public static function getMessage($mbox, $uid, $reply, $item): array
{
$ret = $item;
@ -210,11 +211,11 @@ class Email
/**
* fetch the specified message part number with the specified subtype
*
* @param resource $mbox mailbox
* @param integer $uid user id
* @param object $p parts
* @param integer $partno part number
* @param string $subtype sub type
* @param Connection|resource $mbox mailbox
* @param integer $uid user id
* @param object $p parts
* @param integer $partno part number
* @param string $subtype sub type
* @return string
*/
private static function messageGetPart($mbox, $uid, $p, $partno, $subtype)

View file

@ -197,7 +197,6 @@ class Feed
$author["author-link"] = XML::getFirstNodeValue($xpath, '/rss/channel/link/text()');
$author["author-name"] = XML::getFirstNodeValue($xpath, '/rss/channel/title/text()');
$author["author-avatar"] = XML::getFirstNodeValue($xpath, '/rss/channel/image/url/text()');
if (empty($author["author-name"])) {
$author["author-name"] = XML::getFirstNodeValue($xpath, '/rss/channel/copyright/text()');
@ -207,6 +206,25 @@ class Feed
$author["author-name"] = XML::getFirstNodeValue($xpath, '/rss/channel/description/text()');
}
$author["author-avatar"] = XML::getFirstNodeValue($xpath, '/rss/channel/image/url/text()');
if (empty($author["author-avatar"])) {
$avatar = XML::getFirstAttributes($xpath, "/rss/channel/itunes:image");
if (is_object($avatar)) {
foreach ($avatar as $attribute) {
if ($attribute->name == "href") {
$author["author-avatar"] = $attribute->textContent;
}
}
}
}
$author["author-about"] = HTML::toBBCode(XML::getFirstNodeValue($xpath, '/rss/channel/description/text()'), $basepath);
if (empty($author["author-about"])) {
$author["author-about"] = XML::getFirstNodeValue($xpath, '/rss/channel/itunes:summary/text()');
}
$author["edited"] = $author["created"] = XML::getFirstNodeValue($xpath, '/rss/channel/pubDate/text()');
$author["app"] = XML::getFirstNodeValue($xpath, '/rss/channel/generator/text()');
@ -284,20 +302,23 @@ class Feed
$item["plink"] = XML::getFirstNodeValue($xpath, 'rss:link/text()', $entry);
}
// Add the base path if missing
$item["plink"] = Network::addBasePath($item["plink"], $basepath);
$item["uri"] = XML::getFirstNodeValue($xpath, 'atom:id/text()', $entry);
if (empty($item["uri"])) {
$item["uri"] = XML::getFirstNodeValue($xpath, 'guid/text()', $entry);
$guid = XML::getFirstNodeValue($xpath, 'guid/text()', $entry);
if (!empty($guid)) {
$item["uri"] = $guid;
// Don't use the GUID value directly but instead use it as a basis for the GUID
$item["guid"] = Item::guidFromUri($guid, parse_url($guid, PHP_URL_HOST) ?? parse_url($item["plink"], PHP_URL_HOST));
}
if (empty($item["uri"])) {
$item["uri"] = $item["plink"];
}
// Add the base path if missing
$item["uri"] = Network::addBasePath($item["uri"], $basepath);
$item["plink"] = Network::addBasePath($item["plink"], $basepath);
$orig_plink = $item["plink"];
try {
@ -311,10 +332,15 @@ class Feed
if (empty($item["title"])) {
$item["title"] = XML::getFirstNodeValue($xpath, 'title/text()', $entry);
}
if (empty($item["title"])) {
$item["title"] = XML::getFirstNodeValue($xpath, 'rss:title/text()', $entry);
}
if (empty($item["title"])) {
$item["title"] = XML::getFirstNodeValue($xpath, 'itunes:title/text()', $entry);
}
$item["title"] = html_entity_decode($item["title"], ENT_QUOTES, 'UTF-8');
$published = XML::getFirstNodeValue($xpath, 'atom:published/text()', $entry);
@ -457,6 +483,7 @@ class Feed
}
if ($dryRun) {
$item['attachments'] = $attachments;
$items[] = $item;
break;
} elseif (!Item::isValid($item)) {

View file

@ -494,19 +494,22 @@ class OStatus
if ($initialize && (count(self::$itemlist) > 0)) {
if (self::$itemlist[0]['uri'] == self::$itemlist[0]['thr-parent']) {
$uid = self::$itemlist[0]['uid'];
// We will import it everytime, when it is started by our contacts
$valid = Contact::isSharingByURL(self::$itemlist[0]['author-link'], self::$itemlist[0]['uid']);
$valid = Contact::isSharingByURL(self::$itemlist[0]['author-link'], $uid);
if (!$valid) {
// If not, then it depends on this setting
$valid = ((self::$itemlist[0]['uid'] == 0) || !DI::pConfig()->get(self::$itemlist[0]['uid'], 'system', 'accept_only_sharer', false));
$valid = !$uid || DI::pConfig()->get($uid, 'system', 'accept_only_sharer') != Item::COMPLETION_NONE;
if ($valid) {
Logger::info("Item with uri ".self::$itemlist[0]['uri']." will be imported due to the system settings.");
}
} else {
Logger::info("Item with uri ".self::$itemlist[0]['uri']." belongs to a contact (".self::$itemlist[0]['contact-id']."). It will be imported.");
}
if ($valid) {
if ($valid && DI::pConfig()->get($uid, 'system', 'accept_only_sharer') != Item::COMPLETION_LIKE) {
// Never post a thread when the only interaction by our contact was a like
$valid = false;
$verbs = [Activity::POST, Activity::SHARE];
@ -1728,6 +1731,7 @@ class OStatus
if ($owner['contact-type'] == Contact::TYPE_COMMUNITY) {
$contact = Contact::getByURL($item['author-link']) ?: $owner;
$contact['nickname'] = $contact['nickname'] ?? $contact['nick'];
$author = self::addAuthor($doc, $contact, false);
$entry->appendChild($author);
}

View file

@ -58,5 +58,7 @@ class FriendicaSmarty extends Smarty
// Don't report errors so verbosely
$this->error_reporting = E_ALL & ~E_NOTICE;
$this->muteUndefinedOrNullWarnings();
}
}

View file

@ -627,7 +627,8 @@ class HTTPSignature
if (!empty($created)) {
$current = time();
if ($created > $current) {
// Calculate with a grace period of 60 seconds to avoid slight time differences between the servers
if (($created - 60) > $current) {
Logger::notice('Signature created in the future', ['created' => date(DateTimeFormat::MYSQL, $created), 'expired' => date(DateTimeFormat::MYSQL, $expired), 'current' => date(DateTimeFormat::MYSQL, $current)]);
return false;
}

View file

@ -184,12 +184,14 @@ class Images
return $data;
}
$data = DI::cache()->get($url);
$cacheKey = 'getInfoFromURL:' . sha1($url);
$data = DI::cache()->get($cacheKey);
if (empty($data) || !is_array($data)) {
$data = self::getInfoFromURL($url);
DI::cache()->set($url, $data);
DI::cache()->set($cacheKey, $data);
}
return $data;

View file

@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (C) 2010-2022, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Worker\Contact;
use Friendica\Core\Protocol;
use Friendica\Core\Worker;
use Friendica\Model\Contact;
class RevokeFollow
{
/**
* Issue asynchronous follow revokation message to remote servers.
* The local relationship has already been updated, so we can't use the user-specific contact
*
* @param int $cid Target public contact id
* @param int $uid Source local user id
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function execute(int $cid, int $uid)
{
$contact = Contact::getById($cid);
if (empty($contact)) {
return;
}
$result = Protocol::revokeFollow($contact, $uid);
if ($result === false) {
Worker::defer();
}
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* @copyright Copyright (C) 2010-2022, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Worker\Contact;
use Friendica\Core\Protocol;
use Friendica\Core\Worker;
use Friendica\Model\Contact;
use Friendica\Model\User;
class Unfollow
{
/**
* Issue asynchronous unfollow message to remote servers.
* The local relationship has already been updated, so we can't use the user-specific contact.
*
* @param int $cid Target public contact (uid = 0) id
* @param int $uid Source local user id
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function execute(int $cid, int $uid)
{
$contact = Contact::getById($cid);
if (empty($contact)) {
return;
}
$owner = User::getOwnerDataById($uid, false);
if (empty($owner)) {
return;
}
$result = Protocol::unfollow($contact, $owner);
if ($result === false) {
Worker::defer();
}
}
}

View file

@ -46,7 +46,6 @@ class Delivery
const DELETION = 'drop';
const POST = 'wall-new';
const POKE = 'poke';
const UPLINK = 'uplink';
const REMOVAL = 'removeme';
const PROFILEUPDATE = 'profileupdate';

View file

@ -153,7 +153,7 @@ class Notifier
}
// Should the post be transmitted to Diaspora?
$diaspora_delivery = true;
$diaspora_delivery = ($owner['account-type'] != User::ACCOUNT_TYPE_COMMUNITY);
// If this is a public conversation, notify the feed hub
$public_message = true;
@ -223,10 +223,6 @@ class Notifier
$relay_to_owner = true;
}
if (($cmd === Delivery::UPLINK) && (intval($parent['forum_mode']) == 1) && !$top_level) {
$relay_to_owner = true;
}
// until the 'origin' flag has been in use for several months
// we will just use it as a fallback test
// later we will be able to use it as the primary test of whether or not to relay.
@ -239,13 +235,13 @@ class Notifier
}
// Special treatment for forum posts
if (Item::isForumPost($target_item, $owner)) {
if (Item::isForumPost($target_item['uri-id'])) {
$relay_to_owner = true;
$direct_forum_delivery = true;
}
// Avoid that comments in a forum thread are sent to OStatus
if (Item::isForumPost($parent, $owner)) {
if (Item::isForumPost($parent['uri-id'])) {
$direct_forum_delivery = true;
}
@ -333,15 +329,6 @@ class Notifier
$deny_people = $aclFormatter->expand($parent['deny_cid']);
$deny_groups = Group::expand($uid, $aclFormatter->expand($parent['deny_gid']));
// if our parent is a public forum (forum_mode == 1), uplink to the origional author causing
// a delivery fork. private groups (forum_mode == 2) do not uplink
/// @todo Possibly we should not uplink when the author is the forum itself?
if ((intval($parent['forum_mode']) == 1) && !$top_level && ($cmd !== Delivery::UPLINK)
&& ($target_item['verb'] != Activity::ANNOUNCE)) {
Worker::add($a->getQueueValue('priority'), 'Notifier', Delivery::UPLINK, $post_uriid, $sender_uid);
}
foreach ($items as $item) {
$recipients[] = $item['contact-id'];
// pull out additional tagged people to notify (if public message)
@ -458,7 +445,7 @@ class Notifier
$condition = ['network' => Protocol::DFRN, 'uid' => $owner['uid'], 'blocked' => false,
'pending' => false, 'archive' => false, 'rel' => [Contact::FOLLOWER, Contact::FRIEND]];
$contacts = DBA::toArray(DBA::select('contact', ['id', 'url', 'addr', 'name', 'network', 'protocol'], $condition));
$contacts = DBA::selectToArray('contact', ['id', 'url', 'addr', 'name', 'network', 'protocol'], $condition);
$conversants = array_merge($contacts, $participants);
@ -686,7 +673,7 @@ class Notifier
}
while($contact = DBA::fetch($contacts_stmt)) {
Protocol::terminateFriendship($owner, $contact, true);
Contact::terminateFriendship($contact);
}
DBA::close($contacts_stmt);
@ -742,6 +729,14 @@ class Notifier
$uid = $target_item['contact-uid'] ?: $target_item['uid'];
// Update the locally stored follower list when we deliver to a forum
foreach (Tag::getByURIId($target_item['uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]) as $tag) {
$target_contact = Contact::getByURL(Strings::normaliseLink($tag['url']), null, [], $uid);
if ($target_contact && $target_contact['contact-type'] == Contact::TYPE_COMMUNITY && $target_contact['manually-approve']) {
Group::updateMembersForForum($target_contact['id']);
}
}
if ($target_item['origin']) {
$inboxes = ActivityPub\Transmitter::fetchTargetInboxes($target_item, $uid);
@ -751,9 +746,6 @@ class Notifier
}
Logger::info('Origin item ' . $target_item['id'] . ' with URL ' . $target_item['uri'] . ' will be distributed.');
} elseif (Item::isForumPost($target_item, $owner)) {
$inboxes = ActivityPub\Transmitter::fetchTargetInboxes($target_item, $uid, false, 0, true);
Logger::info('Forum item ' . $target_item['id'] . ' with URL ' . $target_item['uri'] . ' will be distributed.');
} elseif (!DBA::exists('conversation', ['item-uri' => $target_item['uri'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB])) {
Logger::info('Remote item ' . $target_item['id'] . ' with URL ' . $target_item['uri'] . ' is no AP post. It will not be distributed.');
return ['count' => 0, 'contacts' => []];
@ -813,15 +805,4 @@ class Notifier
return ['count' => $delivery_queue_count, 'contacts' => $contacts];
}
/**
* Check if the delivered item is a forum post
*
* @param array $item
* @return boolean
*/
public static function isForumPost(array $item)
{
return ($item['gravity'] == GRAVITY_PARENT) && !empty($item['forum_mode']);
}
}

View file

@ -82,7 +82,7 @@ class PushSubscription
}
}
$message = DI::notificationFactory()->getMessageFromNotification($Notification, DI::baseUrl(), $l10n);
$message = DI::notificationFactory()->getMessageFromNotification($Notification);
$title = $message['plain'] ?: '';
$push = Subscription::create([

View file

@ -52,7 +52,7 @@ class UpdateContacts
$condition = DBA::mergeConditions($base_condition,
["`uid` != ? AND (`last-update` < ? OR (NOT `failed` AND `last-update` < ?))",
0, DateTimeFormat::utc('now - 1 month'), DateTimeFormat::utc('now - 1 week')]);
$ids = self::getContactsToUpdate($condition, [], $limit);
$ids = self::getContactsToUpdate($condition, $limit, []);
Logger::info('Fetched federated user contacts', ['count' => count($ids)]);
$conditions = ["`id` IN (SELECT `author-id` FROM `post` WHERE `author-id` = `contact`.`id`)",
@ -65,7 +65,7 @@ class UpdateContacts
$condition = DBA::mergeConditions($base_condition,
[$contact_condition . " AND (`last-update` < ? OR (NOT `failed` AND `last-update` < ?))",
DateTimeFormat::utc('now - 1 month'), DateTimeFormat::utc('now - 1 week')]);
$ids = self::getContactsToUpdate($condition, $ids, $limit);
$ids = self::getContactsToUpdate($condition, $limit, $ids);
Logger::info('Fetched interacting federated contacts', ['count' => count($ids), 'condition' => $contact_condition]);
}
@ -80,7 +80,7 @@ class UpdateContacts
["(`last-update` < ? OR (NOT `failed` AND `last-update` < ?))",
DateTimeFormat::utc('now - 6 month'), DateTimeFormat::utc('now - 1 month')]);
$previous = count($ids);
$ids = self::getContactsToUpdate($condition, $ids, $limit - $previous);
$ids = self::getContactsToUpdate($condition, $limit - $previous, $ids);
Logger::info('Fetched federated contacts', ['count' => count($ids) - $previous]);
}
@ -98,10 +98,11 @@ class UpdateContacts
* Returns contact ids based on a given condition
*
* @param array $condition
* @param int $limit
* @param array $ids
* @return array contact ids
*/
private static function getContactsToUpdate(array $condition, array $ids = [], int $limit)
private static function getContactsToUpdate(array $condition, int $limit, array $ids = [])
{
$contacts = DBA::select('contact', ['id'], $condition, ['limit' => $limit]);
while ($contact = DBA::fetch($contacts)) {