diff --git a/.htaccess-dist b/.htaccess-dist index 3c9098251..404137168 100644 --- a/.htaccess-dist +++ b/.htaccess-dist @@ -6,6 +6,7 @@ AddType application/x-java-archive .jar AddType audio/ogg .oga #AddHandler php53-cgi .php +# deny access to log files (friendica.log or php.out) #Apache 2.4 @@ -17,6 +18,18 @@ AddType audio/ogg .oga +# deny access to backup files + + + #Apache 2.4 + Require all denied + + + #Apache 2.2 + Deny from all + + + RewriteEngine on # Protect repository directory from browsing diff --git a/.woodpecker/.continuous-deployment.yml b/.woodpecker/.continuous-deployment.yml index 69886557e..d334a4307 100644 --- a/.woodpecker/.continuous-deployment.yml +++ b/.woodpecker/.continuous-deployment.yml @@ -62,7 +62,7 @@ pipeline: - export RELEASE="friendica-full-$VERSION" - export ARTIFACT="$RELEASE.tar.gz" - tar - --transform "s,^,$RELEASE/," + --transform "s,^,$RELEASE/,S" -X mods/release-list-exclude.txt -T mods/release-list-include.txt -cvzf ./build/$ARTIFACT diff --git a/.woodpecker/.releaser.yml b/.woodpecker/.releaser.yml index da4dc5b2c..acac6ed2c 100644 --- a/.woodpecker/.releaser.yml +++ b/.woodpecker/.releaser.yml @@ -60,7 +60,7 @@ pipeline: - export RELEASE="friendica-full-$VERSION" - export ARTIFACT="$RELEASE.tar.gz" - tar - --transform "s,^,$RELEASE/," + --transform "s,^,$RELEASE/,S" -X mods/release-list-exclude.txt -T mods/release-list-include.txt -cvzf ./build/$ARTIFACT diff --git a/CHANGELOG b/CHANGELOG index 45131d447..85a2198ff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,39 @@ +Version 2022.05 (unreleased) + Friendica Core + + Friendica Addons + + Closed Issues + +Version 2022.03 (2022-03-07) + Friendica Core + Updates to the translations AR, DE, HU [translation teams] + Updates to the documentation [bkil, tobiasd] + General code cleanup [annando, MrPetovan] + Enhanced the federation statistics page in the admin panel [annando] + Enhanced handling of database errors [annando] + Enhanced the thread completion [annando, MrPetovan] + Enhanced the handling of unfollow/revoke actions [MrPetovan] + Enhanced the API [annando] + Fixed a bug that caused wrong categories were displayed in a users profile [MrPetovan] + Fixed a bug that lead to private messages being send to the wrong recipient [MrPetovan] + Added display of post receivers [annando] + Added pleroma like version to the API results [MrPetovan] + Added advanced configuration option to automatically re-use the abstract field from AP conversations [annando] + Switched to SMARTY-4 templating engine [MrPetovan] + Breaking: The distribution of _private forums_ was moved to ActivityPub, + making them incompatible with older versions of Friendica [annando] + Breaking: The Twitter-/Friendica-/Statusnet-API now uses the same base + for the id as the Mastodon API (uri-id instead of id). To still + receive new posts with (for example) Friendiqa you have to remove + the account and add it again. [annando] + + Friendica Addons + Added S3 Storage Backend addon [nupplaphil] + + Closed Issues + 11220, 11222, 11232, 11234, 11248, 11245, 11264, 11274 + Version 2022.02 (2022-02-06) Friendica Core Updates to the translations AR, DE, ET, FR, GB_EN, GB_US, HU, IT, RU, SV [translation teams] diff --git a/CREDITS.txt b/CREDITS.txt index 5a88e44cd..e81783404 100644 --- a/CREDITS.txt +++ b/CREDITS.txt @@ -45,6 +45,7 @@ ben-utzer Beringer Zsolt BinkaDroid Bjoessi +bkil bob lebonche Boris Daniel Martinez Millàn bufalo1973 @@ -143,6 +144,7 @@ Josef Moravek juanman julia.domagalska Julio Cova +k-alin Karel Karolina Kastal András diff --git a/composer.json b/composer.json index d8d93b63f..cc3a0db5f 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "pragmarx/recovery": "^0.2", "psr/container": "^1.0", "seld/cli-prompt": "^1.0", - "smarty/smarty": "^3.1", + "smarty/smarty": "^4", "ua-parser/uap-php": "^3.9", "xemlock/htmlpurifier-html5": "^0.1.11", "fxp/composer-asset-plugin": "^1.4", diff --git a/composer.lock b/composer.lock index c0e14fe52..fa2018825 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3d221e30c9cb7e3f34d8d8141b6fea6c", + "content-hash": "f5922f03b367e68a5930df6ed80c5c2f", "packages": [ { "name": "asika/simple-console", @@ -1152,6 +1152,24 @@ "html", "markdown" ], + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://www.patreon.com/colinodell", + "type": "patreon" + } + ], "time": "2020-07-01T00:34:03+00:00" }, { @@ -1481,6 +1499,12 @@ "mobile detector", "php mobile detect" ], + "funding": [ + { + "url": "https://github.com/serbanghita", + "type": "github" + } + ], "time": "2021-02-19T21:22:57+00:00" }, { @@ -1553,6 +1577,16 @@ "logging", "psr-3" ], + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], "time": "2021-05-28T08:32:12+00:00" }, { @@ -3647,29 +3681,29 @@ }, { "name": "smarty/smarty", - "version": "v3.1.43", + "version": "v4.1.0", "source": { "type": "git", "url": "https://github.com/smarty-php/smarty.git", - "reference": "273f7e00fec034f6d61112552e9caf08d19565b7" + "reference": "9e0536de18b53ba193364291ef0303b0ab9903e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/smarty-php/smarty/zipball/273f7e00fec034f6d61112552e9caf08d19565b7", - "reference": "273f7e00fec034f6d61112552e9caf08d19565b7", + "url": "https://api.github.com/repos/smarty-php/smarty/zipball/9e0536de18b53ba193364291ef0303b0ab9903e1", + "reference": "9e0536de18b53ba193364291ef0303b0ab9903e1", "shasum": "" }, "require": { - "php": ">=5.2" + "php": "^7.1 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^7.5 || ^6.5 || ^5.7 || ^4.8", + "phpunit/phpunit": "^8.5 || ^7.5", "smarty/smarty-lexer": "^3.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1.x-dev" + "dev-master": "4.0.x-dev" } }, "autoload": { @@ -3693,14 +3727,18 @@ { "name": "Rodney Rehm", "email": "rodney.rehm@medialize.de" + }, + { + "name": "Simon Wisselink", + "homepage": "https://www.iwink.nl/" } ], "description": "Smarty - the compiling PHP template engine", - "homepage": "http://www.smarty.net", + "homepage": "https://smarty-php.github.io/smarty/", "keywords": [ "templating" ], - "time": "2022-01-10T09:52:40+00:00" + "time": "2022-02-06T20:34:27+00:00" }, { "name": "spomky-labs/base64url", @@ -3751,6 +3789,16 @@ "safe", "url" ], + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], "time": "2020-11-03T09:10:25+00:00" }, { @@ -4613,6 +4661,20 @@ "constructor", "instantiate" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], "time": "2020-11-10T18:47:58+00:00" }, { @@ -4822,6 +4884,12 @@ "object", "object graph" ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], "time": "2020-11-13T09:40:50+00:00" }, { @@ -6547,6 +6615,20 @@ "polyfill", "portable" ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2021-02-19T12:13:01+00:00" }, { @@ -6587,6 +6669,12 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], "time": "2021-07-28T10:34:58+00:00" }, { diff --git a/database.sql b/database.sql index b7a923618..39e40c044 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2022.05-dev (Siberian Iris) --- DB_UPDATE_VERSION 1450 +-- DB_UPDATE_VERSION 1452 -- ------------------------------------------ @@ -644,10 +644,13 @@ CREATE TABLE IF NOT EXISTS `group` ( `uid` mediumint unsigned NOT NULL DEFAULT 0 COMMENT 'Owner User id', `visible` boolean NOT NULL DEFAULT '0' COMMENT '1 indicates the member list is not private', `deleted` boolean NOT NULL DEFAULT '0' COMMENT '1 indicates the group has been deleted', + `cid` int unsigned COMMENT 'Contact id of forum. When this field is filled then the members are synced automatically.', `name` varchar(255) NOT NULL DEFAULT '' COMMENT 'human readable name of group', PRIMARY KEY(`id`), INDEX `uid` (`uid`), - FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE + INDEX `cid` (`cid`), + FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`cid`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='privacy groups, group info'; -- @@ -879,7 +882,7 @@ CREATE TABLE IF NOT EXISTS `notify` ( FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`parent-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE -) DEFAULT COLLATE utf8mb4_general_ci COMMENT='notifications'; +) DEFAULT COLLATE utf8mb4_general_ci COMMENT='[Deprecated] User notifications'; -- -- TABLE notify-threads @@ -1274,7 +1277,7 @@ CREATE TABLE IF NOT EXISTS `post-thread-user` ( `wall` boolean NOT NULL DEFAULT '0' COMMENT 'This item was posted to the wall of uid', `mention` boolean NOT NULL DEFAULT '0' COMMENT '', `pubmail` boolean NOT NULL DEFAULT '0' COMMENT '', - `forum_mode` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '', + `forum_mode` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'Deprecated', `contact-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'contact.id', `unseen` boolean NOT NULL DEFAULT '1' COMMENT 'post has not been seen', `hidden` boolean NOT NULL DEFAULT '0' COMMENT 'Marker to hide the post from the user', @@ -1609,7 +1612,6 @@ CREATE VIEW `post-user-view` AS SELECT `post-user`.`deleted` AS `deleted`, `post-user`.`origin` AS `origin`, `post-thread-user`.`origin` AS `parent-origin`, - `post-thread-user`.`forum_mode` AS `forum_mode`, `post-thread-user`.`mention` AS `mention`, `post-user`.`global` AS `global`, `post-user`.`network` AS `network`, @@ -1770,7 +1772,6 @@ CREATE VIEW `post-thread-user-view` AS SELECT `post-thread-user`.`unseen` AS `unseen`, `post-user`.`deleted` AS `deleted`, `post-thread-user`.`origin` AS `origin`, - `post-thread-user`.`forum_mode` AS `forum_mode`, `post-thread-user`.`mention` AS `mention`, `post-user`.`global` AS `global`, `post-thread-user`.`network` AS `network`, diff --git a/doc/API-Mastodon.md b/doc/API-Mastodon.md index cec90bea4..2e1d95209 100644 --- a/doc/API-Mastodon.md +++ b/doc/API-Mastodon.md @@ -74,7 +74,7 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en - [`GET /api/v1/instance`](https://docs.joinmastodon.org/methods/instance#fetch-instance) -- GET /api/v1/instance/rules Undocumented, returns Terms of Service +- `GET /api/v1/instance/rules` Undocumented, returns Terms of Service - [`GET /api/v1/instance/peers`](https://docs.joinmastodon.org/methods/instance#list-of-connected-domains) - [`GET /api/v1/lists`](https://docs.joinmastodon.org/methods/timelines/lists/) - [`POST /api/v1/lists`](https://docs.joinmastodon.org/methods/timelines/lists/) @@ -102,6 +102,7 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en - [`GET /api/v1/scheduled_statuses/:id`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) - [`GET /api/v1/search`](https://docs.joinmastodon.org/methods/search/) - [`POST /api/v1/statuses`](https://docs.joinmastodon.org/methods/statuses/) + - Additionally to the static values `public`, `unlisted` and `private`, the `visibility` parameter can contain a numeric value with a group id. - [`GET /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/) - [`DELETE /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/) - [`GET /api/v1/statuses/:id/card`](https://docs.joinmastodon.org/methods/statuses/) @@ -150,7 +151,7 @@ They refer to features that don't exist in Friendica yet. These endpoints won't be implemented at the moment. They refer to features or data that don't exist in Friendica yet. -- POST /api/meta Misskey API endpoint. +- `POST /api/meta` Misskey API endpoint. - [`POST /api/v1/accounts`](https://docs.joinmastodon.org/methods/accounts/) - [`GET /api/v1/accounts/:id/featured_tags`](https://docs.joinmastodon.org/methods/accounts/) - [`POST /api/v1/accounts/:id/pin`](https://docs.joinmastodon.org/methods/accounts/) diff --git a/doc/Addons.md b/doc/Addons.md index 578cffe7c..ce8412547 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -626,7 +626,8 @@ Hook data: Called when unfollowing a remote contact on a non-native network (like Twitter) Hook data: -- **contact** (input): the remote contact (uid = local unfollowing user id) array. +- **contact** (input): the target public contact (uid = 0) array. +- **uid** (input): the id of the source local user. - **result** (output): wether the unfollowing is successful or not. ### revoke_follow @@ -634,7 +635,8 @@ Hook data: Called when making a remote contact on a non-native network (like Twitter) unfollow you. Hook data: -- **contact** (input): the remote contact (uid = local revoking user id) array. +- **contact** (input): the target public contact (uid = 0) array. +- **uid** (input): the id of the source local user. - **result** (output): a boolean value indicating wether the operation was successful or not. ### block @@ -717,10 +719,6 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep- Hook::callAll('personal_xrd', $arr); -### mod/ping.php - - Hook::callAll('network_ping', $arr); - ### mod/parse_url.php Hook::callAll("parse_link", $arr); @@ -863,6 +861,10 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep- Hook::callAll('register_account', $uid); Hook::callAll('remove_user', $user); +### src/Module/Notifications/Ping.php + + Hook::callAll('network_ping', $arr); + ### src/Module/PermissionTooltip.php Hook::callAll('lockview_content', $item); diff --git a/doc/Export-Import-Contacts.md b/doc/Export-Import-Contacts.md index 3e1e91109..95b33f380 100644 --- a/doc/Export-Import-Contacts.md +++ b/doc/Export-Import-Contacts.md @@ -3,7 +3,7 @@ * [Home](help) In addition to [move your account](help/Move-Account) you can export and import the list of accounts you follow. -The exported list is stored as CSV file that is compatible to the format used by other platforms as e.g. Mastodon or Pleroma. +The exported list is stored as CSV file that is compatible to the format used by other platforms as e.g. Mastodon, Misskey or Pleroma. ## Export of followed Contacts diff --git a/doc/Install.md b/doc/Install.md index 47f4f9e78..8bbfdd99c 100644 --- a/doc/Install.md +++ b/doc/Install.md @@ -449,3 +449,58 @@ section: sql_mode = ''; After that, restart mysql and try again. + +### Your worker never or rarely runs + +Friendica is coded to always play nice. It checks whether the host machine is idle enough and if it _seems_ to be overloaded, it intermittently refuses to process the worker queue. + +Such checks originate from the days of single-user single-core machines and involves thresholds that you should adjust based on the number of exclusive CPU cores you have. See this issue for more information: + +* https://github.com/friendica/friendica/issues/10131 + +If you want to be neighborly and are using a shared web hosting PaaS provider, especially within the free tier, you need to set `maxloadavg` to say twice the maximum value of `/proc/loadavg` during peak hours. + +If you have the whole (virtual) machine for yourself such as in case of an IaaS VPS, you can set it to orders of magnitude higher than its commonly observed value, such as 1000. + +You should instead enact limits in your web server configuration based on the number of entry processes to cap the concurrent memory usage of your PHP processes. +See `RLimitMEM`, `RLimitCPU`, `RLimitNPROC`, `StartServers`, `ServerLimit`, `MaxRequestsPerChild`, `pm.max_children`, `pm.start_servers` and related options in your server. + +### Error uploading even small image files + +You tried to upload an image up to 100kB and it failed. + +You may not have the ownership or file mode set correctly if you are using the file system storage backend. + +Change the backend to database. If this solves it, that is what needs to be fixed. + +### Error uploading large files + +You may find `413 Request Entity Too Large` or `500 Internal Error` in the network inspector of the browser if the file is too large, for example if it is a video. + +First try to upload a very small file, up to 100kB. If that succeeds, you will need to increase limits at multiple places, including on any web proxy that you are using. + +In your PHP ini: + +* `upload_max_filesize`: defaults to 2MB +* `post_max_size`: defaults to 8MB, must be greater than `upload_max_filesize` +* `memory_limit`: defaults to 128MB, must be greater than `post_max_size` + +You should verify whether you changed them in the _right file_ by checking the web interface at the end of the overview on the `Admin` panel. + +For Apache2: + +* `LimitRequestBody`: defaults to unlimited +* `SSLRenegBufferSize`: defaults to 128kB, only if your site uses TLS and perhaps only when using `SSLVerifyClient` or `SSLVerifyDepth` + +For nginx: + +* `client_max_body_size`: defaults to 1MB + +If you are using the database backend for storage, increase this in your SQL configuration: + +* `max_allowed_packet`: defaults to 32MB + +If you use the ModSecurity WAF: + +* `SecRequestBodyLimit`: defaults to 12MB +* `SecRequestBodyNoFilesLimit`: defaults to 128kB, should not apply to Friendica diff --git a/doc/database.md b/doc/database.md index 628e84e21..f64434b88 100644 --- a/doc/database.md +++ b/doc/database.md @@ -37,7 +37,7 @@ Database Tables | [mailacct](help/database/db_mailacct) | Mail account data for fetching mails | | [manage](help/database/db_manage) | table of accounts that can manage each other | | [notification](help/database/db_notification) | notifications | -| [notify](help/database/db_notify) | notifications | +| [notify](help/database/db_notify) | [Deprecated] User notifications | | [notify-threads](help/database/db_notify-threads) | | | [oembed](help/database/db_oembed) | cache for OEmbed queries | | [openwebauth-token](help/database/db_openwebauth-token) | Store OpenWebAuth token to verify contacts | diff --git a/doc/database/db_group.md b/doc/database/db_group.md index 1892de3e4..ad7fa4a3d 100644 --- a/doc/database/db_group.md +++ b/doc/database/db_group.md @@ -6,13 +6,14 @@ privacy groups, group info Fields ------ -| Field | Description | Type | Null | Key | Default | Extra | -| ------- | ------------------------------------------ | ------------------ | ---- | --- | ------- | -------------- | -| id | sequential ID | int unsigned | NO | PRI | NULL | auto_increment | -| uid | Owner User id | mediumint unsigned | NO | | 0 | | -| visible | 1 indicates the member list is not private | boolean | NO | | 0 | | -| deleted | 1 indicates the group has been deleted | boolean | NO | | 0 | | -| name | human readable name of group | varchar(255) | NO | | | | +| Field | Description | Type | Null | Key | Default | Extra | +| ------- | ----------------------------------------------------------------------------------------- | ------------------ | ---- | --- | ------- | -------------- | +| id | sequential ID | int unsigned | NO | PRI | NULL | auto_increment | +| uid | Owner User id | mediumint unsigned | NO | | 0 | | +| visible | 1 indicates the member list is not private | boolean | NO | | 0 | | +| deleted | 1 indicates the group has been deleted | boolean | NO | | 0 | | +| cid | Contact id of forum. When this field is filled then the members are synced automatically. | int unsigned | YES | | NULL | | +| name | human readable name of group | varchar(255) | NO | | | | Indexes ------------ @@ -21,6 +22,7 @@ Indexes | ------- | ------ | | PRIMARY | id | | uid | uid | +| cid | cid | Foreign Keys ------------ @@ -28,5 +30,6 @@ Foreign Keys | Field | Target Table | Target Field | |-------|--------------|--------------| | uid | [user](help/database/db_user) | uid | +| cid | [contact](help/database/db_contact) | id | Return to [database documentation](help/database) diff --git a/doc/database/db_notify.md b/doc/database/db_notify.md index 250734e56..88d814c04 100644 --- a/doc/database/db_notify.md +++ b/doc/database/db_notify.md @@ -1,7 +1,7 @@ Table notify =========== -notifications +[Deprecated] User notifications Fields ------ diff --git a/doc/database/db_post-thread-user.md b/doc/database/db_post-thread-user.md index 7307dc78d..0b7483741 100644 --- a/doc/database/db_post-thread-user.md +++ b/doc/database/db_post-thread-user.md @@ -24,7 +24,7 @@ Fields | wall | This item was posted to the wall of uid | boolean | NO | | 0 | | | mention | | boolean | NO | | 0 | | | pubmail | | boolean | NO | | 0 | | -| forum_mode | | tinyint unsigned | NO | | 0 | | +| forum_mode | Deprecated | tinyint unsigned | NO | | 0 | | | contact-id | contact.id | int unsigned | NO | | 0 | | | unseen | post has not been seen | boolean | NO | | 1 | | | hidden | Marker to hide the post from the user | boolean | NO | | 0 | | diff --git a/doc/de/Addons.md b/doc/de/Addons.md index 3381ef48f..32e69a2fd 100644 --- a/doc/de/Addons.md +++ b/doc/de/Addons.md @@ -236,10 +236,6 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap Hook::callAll('personal_xrd', $arr); -### mod/ping.php - - Hook::callAll('network_ping', $arr); - ### mod/parse_url.php Hook::callAll("parse_link", $arr); @@ -426,6 +422,10 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap Hook::callAll('storage_instance', $data); Hook::callAll('storage_config', $data); +### src/Module/Notifications/Ping.php + + Hook::callAll('network_ping', $arr); + ### src/Module/PermissionTooltip.php Hook::callAll('lockview_content', $item); diff --git a/doc/de/Export-Import-Contacts.md b/doc/de/Export-Import-Contacts.md index 73905567c..6d814edcb 100644 --- a/doc/de/Export-Import-Contacts.md +++ b/doc/de/Export-Import-Contacts.md @@ -3,7 +3,7 @@ * [Home](help) Zusätzlich zum [Umziehen des Accounts](help/Move-Account) kannst du die Liste der von dir gefolgten Kontakte exportieren und importieren. -Die exportierte Liste wird als CSV Datei in einem zu anderen Plattformen, z.B. Mastodon oder Pleroma, kompatiblen Format gespeichert. +Die exportierte Liste wird als CSV Datei in einem zu anderen Plattformen, z.B. Mastodon, Misskey oder Pleroma, kompatiblen Format gespeichert. ## Export der gefolgten Kontakte diff --git a/mod/display.php b/mod/display.php index 118645819..da41c8656 100644 --- a/mod/display.php +++ b/mod/display.php @@ -23,7 +23,6 @@ use Friendica\App; use Friendica\Content\Text\BBCode; use Friendica\Content\Widget; use Friendica\Core\Logger; -use Friendica\Core\Protocol; use Friendica\Core\Renderer; use Friendica\Core\Session; use Friendica\Database\DBA; @@ -36,6 +35,7 @@ use Friendica\Module\ActivityPub\Objects; use Friendica\Network\HTTPException; use Friendica\Protocol\ActivityPub; use Friendica\Protocol\DFRN; +use Friendica\Protocol\Diaspora; function display_init(App $a) { @@ -108,55 +108,23 @@ function display_init(App $a) $item = $parent ?: $item; } - $profiledata = display_fetchauthor($item); - - DI::page()['aside'] = Widget\VCard::getHTML($profiledata); + DI::page()['aside'] = Widget\VCard::getHTML(display_fetchauthor($item)); } function display_fetchauthor($item) { - $profiledata = Contact::getByURLForUser($item['author-link'], local_user()); - - // Check for a repeated message - $shared = Item::getShareArray($item); - if (!empty($shared) && empty($shared['comment'])) { - $profiledata = [ - 'uid' => 0, - 'id' => -1, - 'nickname' => '', - 'name' => '', - 'picdate' => '', - 'photo' => '', - 'url' => '', - 'network' => '', - ]; - - if (!empty($shared['author'])) { - $profiledata['name'] = $shared['author']; - } - + if (Diaspora::isReshare($item['body'], true)) { + $shared = Item::getShareArray($item); if (!empty($shared['profile'])) { - $profiledata['url'] = $shared['profile']; + $contact = Contact::getByURLForUser($shared['profile'], local_user()); } - - if (!empty($shared['avatar'])) { - $profiledata['photo'] = $shared['avatar']; - } - - $profiledata['nickname'] = $profiledata['name']; - $profiledata['network'] = Protocol::PHANTOM; - - $profiledata['address'] = ''; - $profiledata['about'] = ''; - - $profiledata = Contact::getByURLForUser($profiledata['url'], local_user()) ?: $profiledata; } - if (!empty($profiledata['photo'])) { - $profiledata['photo'] = DI::baseUrl()->remove($profiledata['photo']); + if (empty($contact)) { + $contact = Contact::getById($item['author-id']); } - return $profiledata; + return $contact; } function display_content(App $a, $update = false, $update_uid = 0) diff --git a/mod/item.php b/mod/item.php index 7cf2e53d4..855e7de3f 100644 --- a/mod/item.php +++ b/mod/item.php @@ -29,7 +29,6 @@ */ use Friendica\App; -use Friendica\Content\Item as ItemHelper; use Friendica\Content\PageInfo; use Friendica\Content\Text\BBCode; use Friendica\Core\Hook; @@ -40,11 +39,11 @@ use Friendica\Core\System; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; -use Friendica\Model\APContact; use Friendica\Model\Attach; use Friendica\Model\Contact; use Friendica\Model\Conversation; use Friendica\Model\FileTag; +use Friendica\Model\Group; use Friendica\Model\Item; use Friendica\Model\ItemURI; use Friendica\Model\Notification; @@ -384,80 +383,34 @@ function item_post(App $a) { $contact_record = DBA::selectFirst('contact', [], ['uid' => $profile_uid, 'self' => true]) ?: []; } - // Look for any tags and linkify them - $inform = ''; - $private_forum = false; - $private_id = null; - $only_to_forum = false; - $forum_contact = []; - // Personal notes must never be altered to a forum post. if ($posttype != Item::PT_PERSONAL_NOTE) { - // Convert mentions in the body to a unified format - $body = BBCode::setMentions($body, local_user() ? local_user() : $profile_uid, $network); + // Look for any tags and linkify them + $item = [ + 'uid' => local_user() ? local_user() : $profile_uid, + 'gravity' => $toplevel_item_id ? GRAVITY_COMMENT : GRAVITY_PARENT, + 'network' => $network, + 'body' => $body, + 'postopts' => $postopts, + 'private' => $private, + 'allow_cid' => $str_contact_allow, + 'allow_gid' => $str_group_allow, + 'deny_cid' => $str_contact_deny, + 'deny_gid' => $str_group_deny, + ]; - // Search for forum mentions - foreach (Tag::getFromBody($body, Tag::TAG_CHARACTER[Tag::MENTION] . Tag::TAG_CHARACTER[Tag::EXCLUSIVE_MENTION]) as $tag) { - $contact = Contact::getByURLForUser($tag[2], $profile_uid); - if (!empty($inform)) { - $inform .= ','; - } - $inform .= 'cid:' . $contact['id']; + $item = DI::contentItem()->expandTags($item); - if (!$toplevel_item_id || 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 ($str_contact_allow == '<' . $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' => $inform]); - } - - $original_contact_id = $contact_id; - - if (!$toplevel_item_id && !empty($forum_contact) && ($private_forum || $only_to_forum)) { - // we tagged a forum in a top level post. Now we change the post - $private = $private_forum ? Item::PRIVATE : Item::UNLISTED; - - if ($only_to_forum) { - $postopts = ''; - } - - if (!$private_forum) { - $str_contact_allow = ''; - $str_group_allow = ''; - $str_contact_deny = ''; - $str_group_deny = ''; - } - - if ($private_forum || !APContact::getByURL($forum_contact['url'])) { - $str_group_allow = ''; - $str_contact_deny = ''; - $str_group_deny = ''; - if ($private_forum) { - $str_contact_allow = '<' . $private_id . '>'; - } else { - $str_contact_allow = ''; - } - $contact_id = $private_id; - $contact_record = $forum_contact; - $_REQUEST['origin'] = false; - $wall = 0; - } + $body = $item['body']; + $inform = $item['inform']; + $postopts = $item['postopts']; + $private = $item['private']; + $str_contact_allow = $item['allow_cid']; + $str_group_allow = $item['allow_gid']; + $str_contact_deny = $item['deny_cid']; + $str_group_deny = $item['deny_gid']; + } else { + $inform = ''; } /* @@ -472,7 +425,7 @@ function item_post(App $a) { $match = null; - if (!$preview && Photo::setPermissionFromBody($body, $uid, $original_contact_id, $str_contact_allow, $str_group_allow, $str_contact_deny, $str_group_deny)) { + if (!$preview && Photo::setPermissionFromBody($body, $uid, $contact_id, $str_contact_allow, $str_group_allow, $str_contact_deny, $str_group_deny)) { $objecttype = Activity\ObjectType::IMAGE; } @@ -487,7 +440,7 @@ function item_post(App $a) { if (count($attaches)) { foreach ($attaches as $attach) { // Ensure to only modify attachments that you own - $srch = '<' . intval($original_contact_id) . '>'; + $srch = '<' . intval($contact_id) . '>'; $condition = ['allow_cid' => $srch, 'allow_gid' => '', 'deny_cid' => '', 'deny_gid' => '', 'id' => $attach]; @@ -809,12 +762,6 @@ function item_post(App $a) { } } - // When we are doing some forum posting via ! we have to start the notifier manually. - // These kind of posts don't initiate the notifier call in the item class. - if ($only_to_forum) { - Worker::add(['priority' => PRIORITY_HIGH, 'dont_fork' => false], "Notifier", Delivery::POST, (int)$datarray['uri-id'], (int)$datarray['uid']); - } - Logger::info('post_complete'); if ($api_source) { diff --git a/mod/ping.php b/mod/ping.php deleted file mode 100644 index 81aa9ec62..000000000 --- a/mod/ping.php +++ /dev/null @@ -1,531 +0,0 @@ -. - * - */ - -use Friendica\App; -use Friendica\Content\ForumManager; -use Friendica\Content\Text\BBCode; -use Friendica\Core\Cache\Enum\Duration; -use Friendica\Core\Hook; -use Friendica\Database\DBA; -use Friendica\DI; -use Friendica\Model\Contact; -use Friendica\Model\Group; -use Friendica\Model\Notification; -use Friendica\Model\Post; -use Friendica\Model\Verb; -use Friendica\Protocol\Activity; -use Friendica\Util\DateTimeFormat; -use Friendica\Util\Proxy; -use Friendica\Util\Temporal; -use Friendica\Util\XML; - -/** - * Outputs the counts and the lists of various notifications - * - * The output format can be controlled via the GET parameter 'format'. It can be - * - xml (deprecated legacy default) - * - json (outputs JSONP with the 'callback' GET parameter) - * - * Expected JSON structure: - * { - * "result": { - * "intro": 0, - * "mail": 0, - * "net": 0, - * "home": 0, - * "register": 0, - * "all-events": 0, - * "all-events-today": 0, - * "events": 0, - * "events-today": 0, - * "birthdays": 0, - * "birthdays-today": 0, - * "groups": [ ], - * "forums": [ ], - * "notification": 0, - * "notifications": [ ], - * "sysmsgs": { - * "notice": [ ], - * "info": [ ] - * } - * } - * } - * - * @param App $a The Friendica App instance - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function ping_init(App $a) -{ - $format = 'xml'; - - if (isset($_GET['format']) && $_GET['format'] == 'json') { - $format = 'json'; - } - - $regs = []; - $notifications = []; - - $intro_count = 0; - $mail_count = 0; - $home_count = 0; - $network_count = 0; - $register_count = 0; - $sysnotify_count = 0; - $groups_unseen = []; - $forums_unseen = []; - - $all_events = 0; - $all_events_today = 0; - $events = 0; - $events_today = 0; - $birthdays = 0; - $birthdays_today = 0; - - $data = []; - $data['intro'] = $intro_count; - $data['mail'] = $mail_count; - $data['net'] = $network_count; - $data['home'] = $home_count; - $data['register'] = $register_count; - - $data['all-events'] = $all_events; - $data['all-events-today'] = $all_events_today; - $data['events'] = $events; - $data['events-today'] = $events_today; - $data['birthdays'] = $birthdays; - $data['birthdays-today'] = $birthdays_today; - - if (local_user()) { - // Different login session than the page that is calling us. - if (!empty($_GET['uid']) && intval($_GET['uid']) != local_user()) { - $data = ['result' => ['invalid' => 1]]; - - if ($format == 'json') { - if (isset($_GET['callback'])) { - // JSONP support - header("Content-type: application/javascript"); - echo $_GET['callback'] . '(' . json_encode($data) . ')'; - } else { - header("Content-type: application/json"); - echo json_encode($data); - } - } else { - header("Content-type: text/xml"); - echo XML::fromArray($data, $xml); - } - exit(); - } - - $notifications = ping_get_notifications(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; - } - } - } - } - - $intros1 = DBA::toArray(DBA::p( - "SELECT `intro`.`id`, `intro`.`datetime`, - `contact`.`name`, `contact`.`url`, `contact`.`photo` - FROM `intro` INNER JOIN `contact` ON `intro`.`suggest-cid` = `contact`.`id` - WHERE `intro`.`uid` = ? AND NOT `intro`.`blocked` AND NOT `intro`.`ignore` AND `intro`.`suggest-cid` != 0", - local_user() - )); - $intros2 = DBA::toArray(DBA::p( - "SELECT `intro`.`id`, `intro`.`datetime`, - `contact`.`name`, `contact`.`url`, `contact`.`photo` - FROM `intro` INNER JOIN `contact` ON `intro`.`contact-id` = `contact`.`id` - WHERE `intro`.`uid` = ? AND NOT `intro`.`blocked` AND NOT `intro`.`ignore` AND `intro`.`contact-id` != 0 AND (`intro`.`suggest-cid` = 0 OR `intro`.`suggest-cid` IS NULL)", - local_user() - )); - - $intro_count = count($intros1) + count($intros2); - $intros = $intros1 + $intros2; - - $myurl = DI::baseUrl() . '/profile/' . $a->getLoggedInUserNickname(); - $mail_count = DBA::count('mail', ["`uid` = ? AND NOT `seen` AND `from-url` != ?", local_user(), $myurl]); - - if (intval(DI::config()->get('config', 'register_policy')) === \Friendica\Module\Register::APPROVE && $a->isSiteAdmin()) { - $regs = Friendica\Model\Register::getPending(); - - if (DBA::isResult($regs)) { - $register_count = count($regs); - } - } - - $cachekey = "ping_init:".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') { - $birthdays ++; - $bd = true; - } else { - $events ++; - } - if (DateTimeFormat::local($x['start'], 'Y-m-d') === $str_now) { - $all_events_today ++; - if ($bd) { - $birthdays_today ++; - } else { - $events_today ++; - } - } - } - } - } - - $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['all-events'] = $all_events; - $data['all-events-today'] = $all_events_today; - $data['events'] = $events; - $data['events-today'] = $events_today; - $data['birthdays'] = $birthdays; - $data['birthdays-today'] = $birthdays_today; - - if (DBA::isResult($notifications)) { - foreach ($notifications as $notif) { - if ($notif['seen'] == 0) { - $sysnotify_count ++; - } - } - } - - // merge all notification types in one array - if (DBA::isResult($intros)) { - foreach ($intros as $intro) { - $notif = [ - 'id' => 0, - 'href' => DI::baseUrl() . '/notifications/intros/' . $intro['id'], - 'name' => BBCode::convert($intro['name']), - 'url' => $intro['url'], - 'photo' => $intro['photo'], - 'date' => $intro['datetime'], - 'seen' => false, - 'message' => DI::l10n()->t('{0} wants to be your friend'), - ]; - $notifications[] = $notif; - } - } - - if (DBA::isResult($regs)) { - if (count($regs) <= 1 || DI::pConfig()->get(local_user(), 'system', 'detailed_notif')) { - foreach ($regs as $reg) { - $notif = [ - 'id' => 0, - 'href' => DI::baseUrl() . '/admin/users/pending', - 'name' => $reg['name'], - 'url' => $reg['url'], - 'photo' => $reg['micro'], - 'date' => $reg['created'], - 'seen' => false, - 'message' => DI::l10n()->t('{0} requested registration'), - ]; - $notifications[] = $notif; - } - } else { - $notif = [ - 'id' => 0, - 'href' => DI::baseUrl() . '/admin/users/pending', - 'name' => $regs[0]['name'], - 'url' => $regs[0]['url'], - 'photo' => $regs[0]['micro'], - 'date' => $regs[0]['created'], - 'seen' => false, - 'message' => DI::l10n()->t('{0} and %d others requested registration', count($regs) - 1), - ]; - $notifications[] = $notif; - } - } - - // sort notifications by $[]['date'] - $sort_function = function ($a, $b) { - $adate = strtotime($a['date']); - $bdate = strtotime($b['date']); - - // Unseen messages are kept at the top - // The value 31536000 means one year. This should be enough :-) - if (!$a['seen']) { - $adate += 31536000; - } - if (!$b['seen']) { - $bdate += 31536000; - } - - if ($adate == $bdate) { - return 0; - } - return ($adate < $bdate) ? 1 : -1; - }; - usort($notifications, $sort_function); - - array_walk($notifications, function (&$notification) { - $notification['photo'] = Contact::getAvatarUrlForUrl($notification['url'], local_user(), Proxy::SIZE_MICRO); - $notification['timestamp'] = DateTimeFormat::local($notification['date']); - $notification['date'] = Temporal::getRelativeDate($notification['date']); - }); - } - - $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']); - } - - if ($format == 'json') { - $notification_count = $sysnotify_count + $intro_count + $register_count; - - $data['groups'] = $groups_unseen; - $data['forums'] = $forums_unseen; - $data['notification'] = ($notification_count < 50) ? $notification_count : '49+'; - $data['notifications'] = $notifications; - $data['sysmsgs'] = [ - 'notice' => $sysmsgs, - 'info' => $sysmsgs_info - ]; - - $json_payload = json_encode(["result" => $data]); - - if (isset($_GET['callback'])) { - // JSONP support - header("Content-type: application/javascript"); - echo $_GET['callback'] . '(' . $json_payload . ')'; - } else { - header("Content-type: application/json"); - echo $json_payload; - } - } else { - // Legacy slower XML format output - $data = ping_format_xml_data($data, $sysnotify_count, $notifications, $sysmsgs, $sysmsgs_info, $groups_unseen, $forums_unseen); - - header("Content-type: text/xml"); - echo XML::fromArray(["result" => $data], $xml); - } - - exit(); -} - -/** - * Retrieves the notifications array for the given user ID - * - * @param int $uid User id - * @return array Associative array of notifications - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function ping_get_notifications($uid) -{ - $result = []; - $offset = 0; - $seen = false; - $seensql = "NOT"; - $order = "DESC"; - $quit = false; - - do { - $r = DBA::toArray(DBA::p( - "SELECT `notify`.*, `post`.`visible`, `post`.`deleted` - FROM `notify` LEFT JOIN `post` ON `post`.`uri-id` = `notify`.`uri-id` - WHERE `notify`.`uid` = ? AND `notify`.`msg` != '' - AND NOT (`notify`.`type` IN (?, ?)) - AND $seensql `notify`.`seen` ORDER BY `notify`.`date` $order LIMIT ?, 50", - $uid, - Notification\Type::INTRO, - Notification\Type::MAIL, - $offset - )); - - if (!$r && !$seen) { - $seen = true; - $seensql = ""; - $order = "DESC"; - $offset = 0; - } elseif (!$r) { - $quit = true; - } else { - $offset += 50; - } - - foreach ($r as $notification) { - if (is_null($notification["visible"])) { - $notification["visible"] = true; - } - - if (is_null($notification["deleted"])) { - $notification["deleted"] = 0; - } - - if ($notification["msg_cache"]) { - $notification["name"] = $notification["name_cache"]; - $notification["message"] = $notification["msg_cache"]; - } else { - $notification["name"] = strip_tags(BBCode::convert($notification["name"])); - $notification["message"] = \Friendica\Navigation\Notifications\Entity\Notify::formatMessage($notification["name"], BBCode::toPlaintext($notification["msg"])); - - // @todo Replace this with a call of the Notify model class - DBA::update('notify', ['name_cache' => $notification["name"], 'msg_cache' => $notification["message"]], ['id' => $notification["id"]]); - } - - $notification["href"] = DI::baseUrl() . "/notification/" . $notification["id"]; - - if ($notification["visible"] - && !$notification["deleted"] - && empty($result['p:' . $notification['parent']]) - ) { - // Should we condense the notifications or show them all? - if (($notification['verb'] != Activity::POST) || DI::pConfig()->get(local_user(), 'system', 'detailed_notif')) { - $result[] = $notification; - } else { - $result['p:' . $notification['parent']] = $notification; - } - } - } - } while ((count($result) < 50) && !$quit); - - return($result); -} - -/** - * Backward-compatible XML formatting for ping.php output - * @deprecated - * - * @param array $data The initial ping data array - * @param int $sysnotify_count Number of unseen system notifications - * @param array $notifs Complete list of notification - * @param array $sysmsgs List of system notice messages - * @param array $sysmsgs_info List of system info messages - * @param array $groups_unseen List of unseen group items - * @param array $forums_unseen List of unseen forum items - * - * @return array XML-transform ready data array - */ -function ping_format_xml_data($data, $sysnotify_count, $notifs, $sysmsgs, $sysmsgs_info, $groups_unseen, $forums_unseen) -{ - $notifications = []; - foreach ($notifs as $key => $notif) { - $notifications[$key . ':note'] = $notif['message']; - - $notifications[$key . ':@attributes'] = [ - 'id' => $notif['id'], - 'href' => $notif['href'], - 'name' => $notif['name'], - 'url' => $notif['url'], - 'photo' => $notif['photo'], - 'date' => $notif['date'], - 'seen' => $notif['seen'], - 'timestamp' => $notif['timestamp'] - ]; - } - - $sysmsg = []; - foreach ($sysmsgs as $key => $m) { - $sysmsg[$key . ':notice'] = $m; - } - foreach ($sysmsgs_info as $key => $m) { - $sysmsg[$key . ':info'] = $m; - } - - $data['notif'] = $notifications; - $data['@attributes'] = ['count' => $sysnotify_count + $data['intro'] + $data['mail'] + $data['register']]; - $data['sysmsgs'] = $sysmsg; - - if ($data['register'] == 0) { - unset($data['register']); - } - - $groups = []; - if (count($groups_unseen)) { - foreach ($groups_unseen as $key => $item) { - $groups[$key . ':group'] = $item['count']; - $groups[$key . ':@attributes'] = ['id' => $item['id']]; - } - $data['groups'] = $groups; - } - - $forums = []; - if (count($forums_unseen)) { - foreach ($forums_unseen as $key => $item) { - $forums[$key . ':forum'] = $item['count']; - $forums[$key . ':@attributes'] = ['id' => $item['id']]; - } - $data['forums'] = $forums; - } - - return $data; -} diff --git a/mod/poco.php b/mod/poco.php index bd11e0971..1ccd74b7f 100644 --- a/mod/poco.php +++ b/mod/poco.php @@ -27,6 +27,7 @@ use Friendica\Core\Protocol; use Friendica\Core\Renderer; use Friendica\Database\DBA; use Friendica\DI; +use Friendica\Util\DateTimeFormat; use Friendica\Util\Strings; use Friendica\Util\XML; @@ -121,10 +122,12 @@ function poco_init(App $a) { if (isset($contact['account-type'])) { $contact['contact-type'] = $contact['account-type']; } - $about = DI::cache()->get("about:" . $contact['updated'] . ":" . $contact['nurl']); + + $cacheKey = 'about:' . $contact['nick'] . ':' . DateTimeFormat::utc($contact['updated'], DateTimeFormat::ATOM); + $about = DI::cache()->get($cacheKey); if (is_null($about)) { $about = BBCode::convertForUriId($contact['uri-id'], $contact['about']); - DI::cache()->set("about:" . $contact['updated'] . ":" . $contact['nurl'], $about); + DI::cache()->set($cacheKey, $about); } // Non connected persons can only see the keywords of a Diaspora account diff --git a/mod/settings.php b/mod/settings.php index 111e52330..642229de0 100644 --- a/mod/settings.php +++ b/mod/settings.php @@ -31,11 +31,14 @@ use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Group; +use Friendica\Model\Item; use Friendica\Model\Notification; use Friendica\Model\Profile; use Friendica\Model\User; +use Friendica\Model\Verb; use Friendica\Module\BaseSettings; use Friendica\Module\Security\Login; +use Friendica\Protocol\Activity; use Friendica\Protocol\Email; use Friendica\Util\Temporal; use Friendica\Worker\Delivery; @@ -107,7 +110,7 @@ function settings_post(App $a) 'port' => $mail_port, 'ssltype' => $mail_ssl, 'user' => $mail_user, - `action` => $mail_action, + 'action' => $mail_action, 'movetofolder' => $mail_movetofolder, 'mailbox' => 'INBOX', 'reply_to' => $mail_replyto, @@ -239,7 +242,6 @@ function settings_post(App $a) $allow_location = ((!empty($_POST['allow_location']) && (intval($_POST['allow_location']) == 1)) ? 1: 0); $publish = ((!empty($_POST['profile_in_directory']) && (intval($_POST['profile_in_directory']) == 1)) ? 1: 0); $net_publish = ((!empty($_POST['profile_in_netdirectory']) && (intval($_POST['profile_in_netdirectory']) == 1)) ? 1: 0); - $old_visibility = ((!empty($_POST['visibility']) && (intval($_POST['visibility']) == 1)) ? 1 : 0); $account_type = ((!empty($_POST['account-type']) && (intval($_POST['account-type']))) ? intval($_POST['account-type']) : 0); $page_flags = ((!empty($_POST['page-flags']) && (intval($_POST['page-flags']))) ? intval($_POST['page-flags']) : 0); $blockwall = ((!empty($_POST['blockwall']) && (intval($_POST['blockwall']) == 1)) ? 0: 1); // this setting is inverted! @@ -352,7 +354,18 @@ function settings_post(App $a) DI::pConfig()->set(local_user(), 'expire', 'photos', $expire_photos); DI::pConfig()->set(local_user(), 'expire', 'network_only', $expire_network_only); + // Reset like notifications when they are going to be shown again + if (!DI::pConfig()->get(local_user(), 'system', 'notify_like') && $notify_like) { + DI::notification()->setAllSeenForUser(local_user(), ['vid' => Verb::getID(Activity::LIKE)]); + } + DI::pConfig()->set(local_user(), 'system', 'notify_like', $notify_like); + + // Reset share notifications when they are going to be shown again + if (!DI::pConfig()->get(local_user(), 'system', 'notify_announce') && $notify_announce) { + DI::notification()->setAllSeenForUser(local_user(), ['vid' => Verb::getID(Activity::ANNOUNCE)]); + } + DI::pConfig()->set(local_user(), 'system', 'notify_announce', $notify_announce); DI::pConfig()->set(local_user(), 'system', 'email_textonly', $email_textonly); @@ -361,16 +374,21 @@ function settings_post(App $a) DI::pConfig()->set(local_user(), 'system', 'unlisted', $unlisted); DI::pConfig()->set(local_user(), 'system', 'accessible-photos', $accessiblephotos); + if ($account_type == User::ACCOUNT_TYPE_COMMUNITY) { + $str_group_allow = ''; + $str_contact_allow = ''; + $str_group_deny = ''; + $str_contact_deny = ''; + + DI::pConfig()->set(local_user(), 'system', 'unlisted', true); + + $blockwall = true; + $blocktags = true; + $hide_friends = true; + } + if ($page_flags == User::PAGE_FLAGS_PRVGROUP) { - $hidewall = 1; - if (!$str_contact_allow && !$str_group_allow && !$str_contact_deny && !$str_group_deny) { - if ($def_gid) { - info(DI::l10n()->t('Private forum has no privacy permissions. Using default privacy group.')); - $str_group_allow = '<' . $def_gid . '>'; - } else { - notice(DI::l10n()->t('Private forum has no privacy permissions and no default privacy group.')); - } - } + $str_group_allow = '<' . Group::FOLLOWERS . '>'; } $fields = ['username' => $username, 'email' => $email, 'timezone' => $timezone, @@ -570,7 +588,17 @@ function settings_content(App $a) '$ostat_enabled' => $ostat_enabled, '$general_settings' => DI::l10n()->t('General Social Media Settings'), - '$accept_only_sharer' => ['accept_only_sharer', DI::l10n()->t('Accept only top level posts by contacts you follow'), $accept_only_sharer, DI::l10n()->t('The system does an auto completion of threads when a comment arrives. This has got the side effect that you can receive posts that had been started by a non-follower but had been commented by someone you follow. This setting deactivates this behaviour. When activated, you strictly only will receive posts from people you really do follow.')], + '$accept_only_sharer' => [ + 'accept_only_sharer', + DI::l10n()->t('Followed content scope'), + $accept_only_sharer, + DI::l10n()->t('By default, conversations in which your follows participated but didn\'t start will be shown in your timeline. You can turn this behavior off, or expand it to the conversations in which your follows liked a post.'), + [ + Item::COMPLETION_NONE => DI::l10n()->t('Only conversations my follows started'), + Item::COMPLETION_COMMENT => DI::l10n()->t('Conversations my follows started or commented on (default)'), + Item::COMPLETION_LIKE => DI::l10n()->t('Any conversation my follows interacted with, including likes'), + ] + ], '$enable_cw' => ['enable_cw', DI::l10n()->t('Enable Content Warning'), $enable_cw, DI::l10n()->t('Users on networks like Mastodon or Pleroma are able to set a content warning field which collapse their post by default. This enables the automatic collapsing instead of setting the content warning as the post title. Doesn\'t affect any other content filtering you eventually set up.')], '$enable_smart_shortening' => ['enable_smart_shortening', DI::l10n()->t('Enable intelligent shortening'), $enable_smart_shortening, DI::l10n()->t('Normally the system tries to find the best link to add to shortened posts. If disabled, every shortened post will always point to the original friendica post.')], '$simple_shortening' => ['simple_shortening', DI::l10n()->t('Enable simple text shortening'), $simple_shortening, DI::l10n()->t('Normally the system shortens posts at the next line feed. If this option is enabled then the system will shorten the text at the maximum character limit.')], @@ -756,7 +784,7 @@ function settings_content(App $a) '$allowloc' => ['allow_location', DI::l10n()->t('Use Browser Location:'), ($user['allow_location'] == 1), ''], '$h_prv' => DI::l10n()->t('Security and Privacy Settings'), - '$visibility' => $profile['net-publish'], + '$is_community' => ($user['account-type'] == User::ACCOUNT_TYPE_COMMUNITY), '$maxreq' => ['maxreq', DI::l10n()->t('Maximum Friend Requests/Day:'), $maxreq , DI::l10n()->t("\x28to prevent spam abuse\x29")], '$profile_in_dir' => $profile_in_dir, '$profile_in_net_dir' => ['profile_in_netdirectory', DI::l10n()->t('Allow your profile to be searchable globally?'), $profile['net-publish'], DI::l10n()->t("Activate this setting if you want others to easily find and follow you. Your profile will be searchable on remote systems. This setting also determines whether Friendica will inform search engines that your profile should be indexed or not.") . $net_pub_desc], diff --git a/mod/unfollow.php b/mod/unfollow.php index 0aa8a87b5..10830bd10 100644 --- a/mod/unfollow.php +++ b/mod/unfollow.php @@ -122,8 +122,7 @@ function unfollow_process(string $url) $owner = User::getOwnerDataById($uid); if (!$owner) { - (new \Friendica\Module\Security\Logout())->init(); - // NOTREACHED + throw new \Friendica\Network\HTTPException\NotFoundException(); } $condition = ["`uid` = ? AND (`rel` = ? OR `rel` = ?) AND (`nurl` = ? OR `alias` = ? OR `alias` = ?)", @@ -140,15 +139,10 @@ function unfollow_process(string $url) $return_path = $base_return_path . '/' . $contact['id']; try { - $result = Contact::terminateFriendship($owner, $contact); - - if ($result === false) { - $notice_message = DI::l10n()->t('Unable to unfollow this contact, please retry in a few minutes or contact your administrator.'); - } else { - $notice_message = DI::l10n()->t('Contact was successfully unfollowed'); - } + Contact::unfollow($contact); + $notice_message = DI::l10n()->t('Contact was successfully unfollowed'); } catch (Exception $e) { - DI::logger()->error($e->getMessage(), ['owner' => $owner, 'contact' => $contact]); + DI::logger()->error($e->getMessage(), ['contact' => $contact]); $notice_message = DI::l10n()->t('Unable to unfollow this contact, please contact your administrator'); } diff --git a/src/Console/AutomaticInstallation.php b/src/Console/AutomaticInstallation.php index e80cc6614..e3e9e22cb 100644 --- a/src/Console/AutomaticInstallation.php +++ b/src/Console/AutomaticInstallation.php @@ -64,6 +64,7 @@ Options -s|--savedb Save the DB credentials to the file (if environment variables is used) -H|--dbhost The host of the mysql/mariadb database (env MYSQL_HOST) -p|--dbport The port of the mysql/mariadb database (env MYSQL_PORT) + -s|--dbsocket The socket of the mysql/mariadb database (env MYSQL_SOCKET) -d|--dbdata The name of the mysql/mariadb database (env MYSQL_DATABASE) -u|--dbuser The username of the mysql/mariadb database login (env MYSQL_USER or MYSQL_USERNAME) -P|--dbpass 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'], diff --git a/src/Console/Config.php b/src/Console/Config.php index 0a38f607f..e32983a68 100644 --- a/src/Console/Config.php +++ b/src/Console/Config.php @@ -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')); } } diff --git a/src/Console/Contact.php b/src/Console/Contact.php index 11f7f87ce..f051d870a 100644 --- a/src/Console/Contact.php +++ b/src/Console/Contact.php @@ -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'); } } diff --git a/src/Content/Conversation.php b/src/Content/Conversation.php index af96deebd..e441e8ba4 100644 --- a/src/Content/Conversation.php +++ b/src/Content/Conversation.php @@ -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.', '' . htmlentities($row['causer-name']) . ''); diff --git a/src/Content/Feature.php b/src/Content/Feature.php index 13c9b3142..d594891b8 100644 --- a/src/Content/Feature.php +++ b/src/Content/Feature.php @@ -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 diff --git a/src/Content/Item.php b/src/Content/Item.php index 5037be80e..f6906b796 100644 --- a/src/Content/Item.php +++ b/src/Content/Item.php @@ -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; + } } diff --git a/src/Content/Text/BBCode.php b/src/Content/Text/BBCode.php index e676673fc..2c17663c1 100644 --- a/src/Content/Text/BBCode.php +++ b/src/Content/Text/BBCode.php @@ -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; diff --git a/src/Content/Widget.php b/src/Content/Widget.php index b5ccd6f14..863399287 100644 --- a/src/Content/Widget.php +++ b/src/Content/Widget.php @@ -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]; } diff --git a/src/Content/Widget/VCard.php b/src/Content/Widget/VCard.php index d230afd30..7f75c6c9c 100644 --- a/src/Content/Widget/VCard.php +++ b/src/Content/Widget/VCard.php @@ -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'), diff --git a/src/Core/ACL.php b/src/Core/ACL.php index a4acf58ba..40612fadf 100644 --- a/src/Core/ACL.php +++ b/src/Core/ACL.php @@ -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 * diff --git a/src/Core/Cache/Type/RedisCache.php b/src/Core/Cache/Type/RedisCache.php index a58936cfc..a3a5cf7c8 100644 --- a/src/Core/Cache/Type/RedisCache.php +++ b/src/Core/Cache/Type/RedisCache.php @@ -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); } diff --git a/src/Core/Protocol.php b/src/Core/Protocol.php index 1b2626525..c141bbc54 100644 --- a/src/Core/Protocol.php +++ b/src/Core/Protocol.php @@ -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); diff --git a/src/Core/System.php b/src/Core/System.php index de0c80b3d..16bc2360e 100644 --- a/src/Core/System.php +++ b/src/Core/System.php @@ -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); diff --git a/src/Core/Worker.php b/src/Core/Worker.php index d43d8c728..5c23d6445 100644 --- a/src/Core/Worker.php +++ b/src/Core/Worker.php @@ -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']; diff --git a/src/DI.php b/src/DI.php index 708fb7d83..835f1ffed 100644 --- a/src/DI.php +++ b/src/DI.php @@ -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); } // diff --git a/src/Database/Database.php b/src/Database/Database.php index cc7f754ee..88d8d7d0f 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -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); + } + } } diff --git a/src/Database/PostUpdate.php b/src/Database/PostUpdate.php index ff60fff7b..6d744a1ba 100644 --- a/src/Database/PostUpdate.php +++ b/src/Database/PostUpdate.php @@ -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; + } } diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index 002a48e6a..ee74c257d 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -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(); diff --git a/src/Factory/Api/Twitter/Status.php b/src/Factory/Api/Twitter/Status.php index 30cdb8bcf..ed138e2bd 100644 --- a/src/Factory/Api/Twitter/Status.php +++ b/src/Factory/Api/Twitter/Status.php @@ -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]); diff --git a/src/Model/APContact.php b/src/Model/APContact.php index b519655e1..55dd75663 100644 --- a/src/Model/APContact.php +++ b/src/Model/APContact.php @@ -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]); diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 0af28a740..e1152b408 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -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); } /** diff --git a/src/Model/GServer.php b/src/Model/GServer.php index c4138cc2d..b739c7420 100644 --- a/src/Model/GServer.php +++ b/src/Model/GServer.php @@ -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])) { diff --git a/src/Model/Group.php b/src/Model/Group.php index 17e0a18e2..fa41d2646 100644 --- a/src/Model/Group.php +++ b/src/Model/Group.php @@ -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])]); + } } diff --git a/src/Model/Item.php b/src/Model/Item.php index bf1f2585a..5047af598 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -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; } /** diff --git a/src/Model/Mail.php b/src/Model/Mail.php index 79a0b5f72..e82a01fbc 100644 --- a/src/Model/Mail.php +++ b/src/Model/Mail.php @@ -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(); } diff --git a/src/Model/Post/Media.php b/src/Model/Post/Media.php index 07cad9cf6..254b88115 100644 --- a/src/Model/Post/Media.php +++ b/src/Model/Post/Media.php @@ -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']]); } /** diff --git a/src/Model/Post/UserNotification.php b/src/Model/Post/UserNotification.php index ad7b9c490..0873646a4 100644 --- a/src/Model/Post/UserNotification.php +++ b/src/Model/Post/UserNotification.php @@ -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; diff --git a/src/Model/Profile.php b/src/Model/Profile.php index cb7fa6547..d77935520 100644 --- a/src/Model/Profile.php +++ b/src/Model/Profile.php @@ -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:'); diff --git a/src/Model/Tag.php b/src/Model/Tag.php index 17a68f120..1cc48bd2f 100644 --- a/src/Model/Tag.php +++ b/src/Model/Tag.php @@ -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); diff --git a/src/Module/ActivityPub/Followers.php b/src/Module/ActivityPub/Followers.php index fbf5bf282..54584de18 100644 --- a/src/Module/ActivityPub/Followers.php +++ b/src/Module/ActivityPub/Followers.php @@ -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); diff --git a/src/Module/ActivityPub/Objects.php b/src/Module/ActivityPub/Objects.php index c085d8683..f3a37b7da 100644 --- a/src/Module/ActivityPub/Objects.php +++ b/src/Module/ActivityPub/Objects.php @@ -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(); } diff --git a/src/Module/Admin/Federation.php b/src/Module/Admin/Federation.php index 1d87b81dd..bf49de825 100644 --- a/src/Module/Admin/Federation.php +++ b/src/Module/Admin/Federation.php @@ -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)), ]); } diff --git a/src/Module/Admin/Logs/View.php b/src/Module/Admin/Logs/View.php index 1104212f6..d56e02061 100644 --- a/src/Module/Admin/Logs/View.php +++ b/src/Module/Admin/Logs/View.php @@ -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'), diff --git a/src/Module/Admin/Site.php b/src/Module/Admin/Site.php index 2bac3d79e..4c5c38ea5 100644 --- a/src/Module/Admin/Site.php +++ b/src/Module/Admin/Site.php @@ -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 - Change default theme settings'), $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 - Change default theme settings', 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).')], diff --git a/src/Module/Admin/Themes/Details.php b/src/Module/Admin/Themes/Details.php index 25724dc72..dc6bf5818 100644 --- a/src/Module/Admin/Themes/Details.php +++ b/src/Module/Admin/Themes/Details.php @@ -76,7 +76,7 @@ class Details extends BaseAdmin require_once "view/theme/$theme/config.php"; if (function_exists('theme_admin')) { - $admin_form = ''; + $admin_form = ''; } } diff --git a/src/Module/Admin/Themes/Embed.php b/src/Module/Admin/Themes/Embed.php index 132a35b67..439e78e64 100644 --- a/src/Module/Admin/Themes/Embed.php +++ b/src/Module/Admin/Themes/Embed.php @@ -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"), ]); diff --git a/src/Module/Admin/Themes/Index.php b/src/Module/Admin/Themes/Index.php index 9677ce30f..cf0ddcfc6 100644 --- a/src/Module/Admin/Themes/Index.php +++ b/src/Module/Admin/Themes/Index.php @@ -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': diff --git a/src/Module/Api/Friendica/Activity.php b/src/Module/Api/Friendica/Activity.php index 3c892b303..de37c9c74 100644 --- a/src/Module/Api/Friendica/Activity.php +++ b/src/Module/Api/Friendica/Activity.php @@ -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); } diff --git a/src/Module/Api/Friendica/Events/Index.php b/src/Module/Api/Friendica/Events/Index.php index 594f85d41..257c9f06f 100644 --- a/src/Module/Api/Friendica/Events/Index.php +++ b/src/Module/Api/Friendica/Events/Index.php @@ -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]; diff --git a/src/Module/Api/Friendica/Group/Show.php b/src/Module/Api/Friendica/Group/Show.php index d15082769..89c257e73 100644 --- a/src/Module/Api/Friendica/Group/Show.php +++ b/src/Module/Api/Friendica/Group/Show.php @@ -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(); diff --git a/src/Module/Api/Friendica/Photo.php b/src/Module/Api/Friendica/Photo.php index ba87081d4..b4b6a3997 100644 --- a/src/Module/Api/Friendica/Photo.php +++ b/src/Module/Api/Friendica/Photo.php @@ -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(); diff --git a/src/Module/Api/GNUSocial/Statusnet/Conversation.php b/src/Module/Api/GNUSocial/Statusnet/Conversation.php index 08cfe82fb..65ef9654b 100644 --- a/src/Module/Api/GNUSocial/Statusnet/Conversation.php +++ b/src/Module/Api/GNUSocial/Statusnet/Conversation.php @@ -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)) { diff --git a/src/Module/Api/Mastodon/Accounts/Block.php b/src/Module/Api/Mastodon/Accounts/Block.php index 94c0e3712..41d0a6f5e 100644 --- a/src/Module/Api/Mastodon/Accounts/Block.php +++ b/src/Module/Api/Mastodon/Accounts/Block.php @@ -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()); } diff --git a/src/Module/Api/Mastodon/Accounts/Unfollow.php b/src/Module/Api/Mastodon/Accounts/Unfollow.php index db1e049db..29aa82b49 100644 --- a/src/Module/Api/Mastodon/Accounts/Unfollow.php +++ b/src/Module/Api/Mastodon/Accounts/Unfollow.php @@ -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()); } diff --git a/src/Module/Api/Mastodon/Instance.php b/src/Module/Api/Mastodon/Instance.php index 4115e0e6b..e5e0a9579 100644 --- a/src/Module/Api/Mastodon/Instance.php +++ b/src/Module/Api/Mastodon/Instance.php @@ -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)); } } diff --git a/src/Module/Api/Mastodon/Lists/Accounts.php b/src/Module/Api/Mastodon/Lists/Accounts.php index 6dcde12b7..413cacae2 100644 --- a/src/Module/Api/Mastodon/Lists/Accounts.php +++ b/src/Module/Api/Mastodon/Lists/Accounts.php @@ -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']); } /** diff --git a/src/Module/Api/Mastodon/Statuses.php b/src/Module/Api/Mastodon/Statuses.php index 7bf40c837..50e0eaa72 100644 --- a/src/Module/Api/Mastodon/Statuses.php +++ b/src/Module/Api/Mastodon/Statuses.php @@ -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; diff --git a/src/Module/Api/Mastodon/Statuses/Bookmark.php b/src/Module/Api/Mastodon/Statuses/Bookmark.php index f33b88728..95a072889 100644 --- a/src/Module/Api/Mastodon/Statuses/Bookmark.php +++ b/src/Module/Api/Mastodon/Statuses/Bookmark.php @@ -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()); diff --git a/src/Module/Api/Mastodon/Statuses/Unbookmark.php b/src/Module/Api/Mastodon/Statuses/Unbookmark.php index 9dc9e73f9..103fac04a 100644 --- a/src/Module/Api/Mastodon/Statuses/Unbookmark.php +++ b/src/Module/Api/Mastodon/Statuses/Unbookmark.php @@ -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()); diff --git a/src/Module/Api/Twitter/Favorites.php b/src/Module/Api/Twitter/Favorites.php index 828741a19..ea7ca42d5 100644 --- a/src/Module/Api/Twitter/Favorites.php +++ b/src/Module/Api/Twitter/Favorites.php @@ -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; } diff --git a/src/Module/Api/Twitter/Favorites/Create.php b/src/Module/Api/Twitter/Favorites/Create.php index a9c6c962d..6544fe470 100644 --- a/src/Module/Api/Twitter/Favorites/Create.php +++ b/src/Module/Api/Twitter/Favorites/Create.php @@ -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); } diff --git a/src/Module/Api/Twitter/Favorites/Destroy.php b/src/Module/Api/Twitter/Favorites/Destroy.php index 0c25e9d18..0d3046afb 100644 --- a/src/Module/Api/Twitter/Favorites/Destroy.php +++ b/src/Module/Api/Twitter/Favorites/Destroy.php @@ -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); } diff --git a/src/Module/Api/Twitter/Friendships/Destroy.php b/src/Module/Api/Twitter/Friendships/Destroy.php index e2e0dd70c..b730f0663 100644 --- a/src/Module/Api/Twitter/Friendships/Destroy.php +++ b/src/Module/Api/Twitter/Friendships/Destroy.php @@ -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'); } diff --git a/src/Module/Api/Twitter/Friendships/Incoming.php b/src/Module/Api/Twitter/Friendships/Incoming.php index 378159c3c..d34d79fae 100644 --- a/src/Module/Api/Twitter/Friendships/Incoming.php +++ b/src/Module/Api/Twitter/Friendships/Incoming.php @@ -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); diff --git a/src/Module/Api/Twitter/Lists/Ownership.php b/src/Module/Api/Twitter/Lists/Ownership.php index e5aca1ad5..c3ff0030b 100644 --- a/src/Module/Api/Twitter/Lists/Ownership.php +++ b/src/Module/Api/Twitter/Lists/Ownership.php @@ -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 = []; diff --git a/src/Module/Api/Twitter/Lists/Statuses.php b/src/Module/Api/Twitter/Lists/Statuses.php index 2bf27697b..177c5fd0b 100644 --- a/src/Module/Api/Twitter/Lists/Statuses.php +++ b/src/Module/Api/Twitter/Lists/Statuses.php @@ -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 = []; diff --git a/src/Module/Api/Twitter/Search/Tweets.php b/src/Module/Api/Twitter/Search/Tweets.php index 5da579b7c..db25c4eb9 100644 --- a/src/Module/Api/Twitter/Search/Tweets.php +++ b/src/Module/Api/Twitter/Search/Tweets.php @@ -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; } } diff --git a/src/Module/Api/Twitter/Statuses/Destroy.php b/src/Module/Api/Twitter/Statuses/Destroy.php index be0848353..e54166b99 100644 --- a/src/Module/Api/Twitter/Statuses/Destroy.php +++ b/src/Module/Api/Twitter/Statuses/Destroy.php @@ -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)); } diff --git a/src/Module/Api/Twitter/Statuses/HomeTimeline.php b/src/Module/Api/Twitter/Statuses/HomeTimeline.php index 41314bb1b..a607d4c95 100644 --- a/src/Module/Api/Twitter/Statuses/HomeTimeline.php +++ b/src/Module/Api/Twitter/Statuses/HomeTimeline.php @@ -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 = []; diff --git a/src/Module/Api/Twitter/Statuses/Mentions.php b/src/Module/Api/Twitter/Statuses/Mentions.php index e9bbb93bd..800a91110 100644 --- a/src/Module/Api/Twitter/Statuses/Mentions.php +++ b/src/Module/Api/Twitter/Statuses/Mentions.php @@ -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 = []; diff --git a/src/Module/Api/Twitter/Statuses/NetworkPublicTimeline.php b/src/Module/Api/Twitter/Statuses/NetworkPublicTimeline.php index af0436b4a..96469fb43 100644 --- a/src/Module/Api/Twitter/Statuses/NetworkPublicTimeline.php +++ b/src/Module/Api/Twitter/Statuses/NetworkPublicTimeline.php @@ -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 = []; diff --git a/src/Module/Api/Twitter/Statuses/PublicTimeline.php b/src/Module/Api/Twitter/Statuses/PublicTimeline.php index aba330a65..a247e6688 100644 --- a/src/Module/Api/Twitter/Statuses/PublicTimeline.php +++ b/src/Module/Api/Twitter/Statuses/PublicTimeline.php @@ -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); } diff --git a/src/Module/Api/Twitter/Statuses/Retweet.php b/src/Module/Api/Twitter/Statuses/Retweet.php index 1d67443b1..d89c2300b 100644 --- a/src/Module/Api/Twitter/Statuses/Retweet.php +++ b/src/Module/Api/Twitter/Statuses/Retweet.php @@ -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); } diff --git a/src/Module/Api/Twitter/Statuses/Show.php b/src/Module/Api/Twitter/Statuses/Show.php index 64533d0bc..16bf46267 100644 --- a/src/Module/Api/Twitter/Statuses/Show.php +++ b/src/Module/Api/Twitter/Statuses/Show.php @@ -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 = []; } diff --git a/src/Module/Api/Twitter/Statuses/Update.php b/src/Module/Api/Twitter/Statuses/Update.php index 45e56cea8..0041c03a7 100644 --- a/src/Module/Api/Twitter/Statuses/Update.php +++ b/src/Module/Api/Twitter/Statuses/Update.php @@ -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'])) { diff --git a/src/Module/Api/Twitter/Statuses/UserTimeline.php b/src/Module/Api/Twitter/Statuses/UserTimeline.php index b6dcd86c9..05aa07962 100644 --- a/src/Module/Api/Twitter/Statuses/UserTimeline.php +++ b/src/Module/Api/Twitter/Statuses/UserTimeline.php @@ -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 = []; diff --git a/src/Module/BaseNotifications.php b/src/Module/BaseNotifications.php index d7319fa4e..1190e046d 100644 --- a/src/Module/BaseNotifications.php +++ b/src/Module/BaseNotifications.php @@ -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 */ diff --git a/src/Module/Contact.php b/src/Module/Contact.php index d571016f1..c7b2870ec 100644 --- a/src/Module/Contact.php +++ b/src/Module/Contact.php @@ -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']), diff --git a/src/Module/Contact/Hovercard.php b/src/Module/Contact/Hovercard.php index ec77a19cb..cd03e2533 100644 --- a/src/Module/Contact/Hovercard.php +++ b/src/Module/Contact/Hovercard.php @@ -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, ], ]); diff --git a/src/Module/Contact/Profile.php b/src/Module/Contact/Profile.php index e02a6a3dc..8ab8dae60 100644 --- a/src/Module/Contact/Profile.php +++ b/src/Module/Contact/Profile.php @@ -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']), diff --git a/src/Module/Contact/Revoke.php b/src/Module/Contact/Revoke.php index 59b3bdafa..35cb48149 100644 --- a/src/Module/Contact/Revoke.php +++ b/src/Module/Contact/Revoke.php @@ -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']); } diff --git a/src/Module/Conversation/Network.php b/src/Module/Conversation/Network.php index 908d0a63b..2978ecbb9 100644 --- a/src/Module/Conversation/Network.php +++ b/src/Module/Conversation/Network.php @@ -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']; diff --git a/src/Module/Debug/ActivityPubConversion.php b/src/Module/Debug/ActivityPubConversion.php index 5de4b93b7..ec7fee3f4 100644 --- a/src/Module/Debug/ActivityPubConversion.php +++ b/src/Module/Debug/ActivityPubConversion.php @@ -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)) diff --git a/src/Module/Diaspora/Receive.php b/src/Module/Diaspora/Receive.php index 498d1b13e..3b1049361 100644 --- a/src/Module/Diaspora/Receive.php +++ b/src/Module/Diaspora/Receive.php @@ -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.'); diff --git a/src/Module/Directory.php b/src/Module/Directory.php index 3a0a9fa34..8d7b8611b 100644 --- a/src/Module/Directory.php +++ b/src/Module/Directory.php @@ -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'], diff --git a/src/Module/Notifications/Notification.php b/src/Module/Notifications/Notification.php index d72e3c250..a28b5d6f6 100644 --- a/src/Module/Notifications/Notification.php +++ b/src/Module/Notifications/Notification.php @@ -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(); + } } diff --git a/src/Module/Notifications/Notifications.php b/src/Module/Notifications/Notifications.php index e090aa5a3..90ea9c28b 100644 --- a/src/Module/Notifications/Notifications.php +++ b/src/Module/Notifications/Notifications.php @@ -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(); diff --git a/src/Module/Notifications/Ping.php b/src/Module/Notifications/Ping.php new file mode 100644 index 000000000..2f79b91f0 --- /dev/null +++ b/src/Module/Notifications/Ping.php @@ -0,0 +1,295 @@ +. + * + */ + +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]); + } + } +} diff --git a/src/Module/PermissionTooltip.php b/src/Module/PermissionTooltip.php index 71ce2beee..58b6df086 100644 --- a/src/Module/PermissionTooltip.php +++ b/src/Module/PermissionTooltip.php @@ -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[] = '' . $contact['name'] . ''; } - 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('To: %s
', implode(', ', $receiver)); + break; + case Tag::CC: + $output .= DI::l10n()->t('CC: %s
', implode(', ', $receiver)); + break; + case Tag::BCC: + $output .= DI::l10n()->t('BCC: %s
', implode(', ', $receiver)); + break; + } + } + + return $output; + } } diff --git a/src/Module/Photo.php b/src/Module/Photo.php index c67520b55..3d3110fd8 100644 --- a/src/Module/Photo.php +++ b/src/Module/Photo.php @@ -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']); diff --git a/src/Module/Profile/Status.php b/src/Module/Profile/Status.php index 2bb7b6eaa..bb4537d6c 100644 --- a/src/Module/Profile/Status.php +++ b/src/Module/Profile/Status.php @@ -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']]); } diff --git a/src/Module/Register.php b/src/Module/Register.php index d415abf74..9c09baef8 100644 --- a/src/Module/Register.php +++ b/src/Module/Register.php @@ -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 ]); diff --git a/src/Module/Settings/Profile/Index.php b/src/Module/Settings/Profile/Index.php index adc4fa68d..49130e8c5 100644 --- a/src/Module/Settings/Profile/Index.php +++ b/src/Module/Settings/Profile/Index.php @@ -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, [ diff --git a/src/Navigation/Notifications/Collection/FormattedNotifications.php b/src/Navigation/Notifications/Collection/FormattedNotifies.php similarity index 79% rename from src/Navigation/Notifications/Collection/FormattedNotifications.php rename to src/Navigation/Notifications/Collection/FormattedNotifies.php index 8dad2f6af..0b907caf7 100644 --- a/src/Navigation/Notifications/Collection/FormattedNotifications.php +++ b/src/Navigation/Notifications/Collection/FormattedNotifies.php @@ -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(); } diff --git a/src/Navigation/Notifications/Collection/Notifications.php b/src/Navigation/Notifications/Collection/Notifications.php index 3dd3775d0..c1ad8bbbf 100644 --- a/src/Navigation/Notifications/Collection/Notifications.php +++ b/src/Navigation/Notifications/Collection/Notifications.php @@ -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); + } } diff --git a/src/Navigation/Notifications/Entity/Notification.php b/src/Navigation/Notifications/Entity/Notification.php index 15cf3be53..0f12bfe84 100644 --- a/src/Navigation/Notifications/Entity/Notification.php +++ b/src/Navigation/Notifications/Entity/Notification.php @@ -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() diff --git a/src/Navigation/Notifications/Entity/Notify.php b/src/Navigation/Notifications/Entity/Notify.php index bf47bdafc..107044007 100644 --- a/src/Navigation/Notifications/Entity/Notify.php +++ b/src/Navigation/Notifications/Entity/Notify.php @@ -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}', '' . strip_tags(BBCode::convert($name)) . '', $message); } } diff --git a/src/Navigation/Notifications/Exception/NoMessageException.php b/src/Navigation/Notifications/Exception/NoMessageException.php new file mode 100644 index 000000000..710f25fa3 --- /dev/null +++ b/src/Navigation/Notifications/Exception/NoMessageException.php @@ -0,0 +1,26 @@ +. + * + */ + +namespace Friendica\Navigation\Notifications\Exception; + +class NoMessageException extends \Exception +{ +} diff --git a/src/Navigation/Notifications/Factory/FormattedNavNotification.php b/src/Navigation/Notifications/Factory/FormattedNavNotification.php new file mode 100644 index 000000000..d8d6dc029 --- /dev/null +++ b/src/Navigation/Notifications/Factory/FormattedNavNotification.php @@ -0,0 +1,140 @@ +. + * + */ + +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) + ); + } +} diff --git a/src/Navigation/Notifications/Factory/FormattedNotification.php b/src/Navigation/Notifications/Factory/FormattedNotify.php similarity index 91% rename from src/Navigation/Notifications/Factory/FormattedNotification.php rename to src/Navigation/Notifications/Factory/FormattedNotify.php index f1df3c173..d4aba639c 100644 --- a/src/Navigation/Notifications/Factory/FormattedNotification.php +++ b/src/Navigation/Notifications/Factory/FormattedNotify.php @@ -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); diff --git a/src/Navigation/Notifications/Factory/Notification.php b/src/Navigation/Notifications/Factory/Notification.php index 657823834..ac3a2560e 100644 --- a/src/Navigation/Notifications/Factory/Notification.php +++ b/src/Navigation/Notifications/Factory/Notification.php @@ -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; diff --git a/src/Navigation/Notifications/Factory/Notify.php b/src/Navigation/Notifications/Factory/Notify.php index fbc614470..d6c777e6f 100644 --- a/src/Navigation/Notifications/Factory/Notify.php +++ b/src/Navigation/Notifications/Factory/Notify.php @@ -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 diff --git a/src/Navigation/Notifications/Repository/Notification.php b/src/Navigation/Notifications/Repository/Notification.php index c4035663b..08ca1f095 100644 --- a/src/Navigation/Notifications/Repository/Notification.php +++ b/src/Navigation/Notifications/Repository/Notification.php @@ -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); + } } diff --git a/src/Navigation/Notifications/Repository/Notify.php b/src/Navigation/Notifications/Repository/Notify.php index 9b195a753..9773b6446 100644 --- a/src/Navigation/Notifications/Repository/Notify.php +++ b/src/Navigation/Notifications/Repository/Notify.php @@ -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; diff --git a/src/Navigation/Notifications/ValueObject/FormattedNavNotification.php b/src/Navigation/Notifications/ValueObject/FormattedNavNotification.php new file mode 100644 index 000000000..d2fae060a --- /dev/null +++ b/src/Navigation/Notifications/ValueObject/FormattedNavNotification.php @@ -0,0 +1,61 @@ +. + * + */ + +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; + } +} diff --git a/src/Navigation/Notifications/ValueObject/FormattedNotification.php b/src/Navigation/Notifications/ValueObject/FormattedNotify.php similarity index 91% rename from src/Navigation/Notifications/ValueObject/FormattedNotification.php rename to src/Navigation/Notifications/ValueObject/FormattedNotify.php index 09d41fa87..ac0db65ad 100644 --- a/src/Navigation/Notifications/ValueObject/FormattedNotification.php +++ b/src/Navigation/Notifications/ValueObject/FormattedNotify.php @@ -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'; diff --git a/src/Object/Api/Mastodon/Instance.php b/src/Object/Api/Mastodon/Instance.php index 70184c313..0ae96c972 100644 --- a/src/Object/Api/Mastodon/Instance.php +++ b/src/Object/Api/Mastodon/Instance.php @@ -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; } } diff --git a/src/Object/Api/Mastodon/ScheduledStatus.php b/src/Object/Api/Mastodon/ScheduledStatus.php index e93c1c07b..759cd6e4e 100644 --- a/src/Object/Api/Mastodon/ScheduledStatus.php +++ b/src/Object/Api/Mastodon/ScheduledStatus.php @@ -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, diff --git a/src/Object/Api/Mastodon/Stats.php b/src/Object/Api/Mastodon/Stats.php index 5061c5729..40a55c6d7 100644 --- a/src/Object/Api/Mastodon/Stats.php +++ b/src/Object/Api/Mastodon/Stats.php @@ -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; } } diff --git a/src/Object/Api/Mastodon/Status.php b/src/Object/Api/Mastodon/Status.php index 987cf00e6..12fef7ac8 100644 --- a/src/Object/Api/Mastodon/Status.php +++ b/src/Object/Api/Mastodon/Status.php @@ -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']]; diff --git a/src/Object/Api/Twitter/Status.php b/src/Object/Api/Twitter/Status.php index 9c1d5d374..54b4fa2d3 100644 --- a/src/Object/Api/Twitter/Status.php +++ b/src/Object/Api/Twitter/Status.php @@ -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; diff --git a/src/Object/Post.php b/src/Object/Post.php index 0561f5506..253b4b4f1 100644 --- a/src/Object/Post.php +++ b/src/Object/Post.php @@ -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']) { diff --git a/src/Protocol/ActivityPub/Processor.php b/src/Protocol/ActivityPub/Processor.php index 76f8cdb85..5651343cb 100644 --- a/src/Protocol/ActivityPub/Processor.php +++ b/src/Protocol/ActivityPub/Processor.php @@ -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'])) { diff --git a/src/Protocol/ActivityPub/Receiver.php b/src/Protocol/ActivityPub/Receiver.php index a28cc9617..98d40137a 100644 --- a/src/Protocol/ActivityPub/Receiver.php +++ b/src/Protocol/ActivityPub/Receiver.php @@ -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']); diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 0b7159ec9..2cab827bb 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -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]); } /** diff --git a/src/Protocol/DFRN.php b/src/Protocol/DFRN.php index 3a14a0db9..0fb739457 100644 --- a/src/Protocol/DFRN.php +++ b/src/Protocol/DFRN.php @@ -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); } /** diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index 45879df2a..9f7781d13 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -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) ) { diff --git a/src/Protocol/Email.php b/src/Protocol/Email.php index 09792935e..db22b7697 100644 --- a/src/Protocol/Email.php +++ b/src/Protocol/Email.php @@ -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) diff --git a/src/Protocol/Feed.php b/src/Protocol/Feed.php index 048e9974c..cde81394d 100644 --- a/src/Protocol/Feed.php +++ b/src/Protocol/Feed.php @@ -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)) { diff --git a/src/Protocol/OStatus.php b/src/Protocol/OStatus.php index 86716e993..a2c9a8447 100644 --- a/src/Protocol/OStatus.php +++ b/src/Protocol/OStatus.php @@ -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); } diff --git a/src/Render/FriendicaSmarty.php b/src/Render/FriendicaSmarty.php index 44a741d7c..50dda5eba 100644 --- a/src/Render/FriendicaSmarty.php +++ b/src/Render/FriendicaSmarty.php @@ -58,5 +58,7 @@ class FriendicaSmarty extends Smarty // Don't report errors so verbosely $this->error_reporting = E_ALL & ~E_NOTICE; + + $this->muteUndefinedOrNullWarnings(); } } diff --git a/src/Util/HTTPSignature.php b/src/Util/HTTPSignature.php index 7603b622b..68f6cb1dd 100644 --- a/src/Util/HTTPSignature.php +++ b/src/Util/HTTPSignature.php @@ -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; } diff --git a/src/Util/Images.php b/src/Util/Images.php index 077509d3c..d7a58b52a 100644 --- a/src/Util/Images.php +++ b/src/Util/Images.php @@ -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; diff --git a/src/Worker/Contact/RevokeFollow.php b/src/Worker/Contact/RevokeFollow.php new file mode 100644 index 000000000..726a69b8d --- /dev/null +++ b/src/Worker/Contact/RevokeFollow.php @@ -0,0 +1,51 @@ +. + * + */ + +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(); + } + } +} diff --git a/src/Worker/Contact/Unfollow.php b/src/Worker/Contact/Unfollow.php new file mode 100644 index 000000000..a6d8c5944 --- /dev/null +++ b/src/Worker/Contact/Unfollow.php @@ -0,0 +1,57 @@ +. + * + */ + +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(); + } + } +} diff --git a/src/Worker/Delivery.php b/src/Worker/Delivery.php index 3be49a38f..c09181d3e 100644 --- a/src/Worker/Delivery.php +++ b/src/Worker/Delivery.php @@ -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'; diff --git a/src/Worker/Notifier.php b/src/Worker/Notifier.php index f46b1b0d9..80628d7db 100644 --- a/src/Worker/Notifier.php +++ b/src/Worker/Notifier.php @@ -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']); - } } diff --git a/src/Worker/PushSubscription.php b/src/Worker/PushSubscription.php index 7b9f5acbd..45ecb6229 100644 --- a/src/Worker/PushSubscription.php +++ b/src/Worker/PushSubscription.php @@ -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([ diff --git a/src/Worker/UpdateContacts.php b/src/Worker/UpdateContacts.php index d7348b258..2beb89091 100644 --- a/src/Worker/UpdateContacts.php +++ b/src/Worker/UpdateContacts.php @@ -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)) { diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 2e1a2191c..8963b1df6 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -55,7 +55,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1450); + define('DB_UPDATE_VERSION', 1452); } return [ @@ -704,11 +704,13 @@ return [ "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "foreign" => ["user" => "uid"], "comment" => "Owner User id"], "visible" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "1 indicates the member list is not private"], "deleted" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "1 indicates the group has been deleted"], + "cid" => ["type" => "int unsigned", "foreign" => ["contact" => "id"], "comment" => "Contact id of forum. When this field is filled then the members are synced automatically."], "name" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "human readable name of group"], ], "indexes" => [ "PRIMARY" => ["id"], "uid" => ["uid"], + "cid" => ["cid"], ] ], "group_member" => [ @@ -902,7 +904,7 @@ return [ ] ], "notify" => [ - "comment" => "notifications", + "comment" => "[Deprecated] User notifications", "fields" => [ "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"], "type" => ["type" => "smallint unsigned", "not null" => "1", "default" => "0", "comment" => ""], @@ -1299,7 +1301,7 @@ return [ "wall" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "This item was posted to the wall of uid"], "mention" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "pubmail" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], - "forum_mode" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""], + "forum_mode" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "Deprecated"], "contact-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "foreign" => ["contact" => "id"], "comment" => "contact.id"], "unseen" => ["type" => "boolean", "not null" => "1", "default" => "1", "comment" => "post has not been seen"], "hidden" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Marker to hide the post from the user"], diff --git a/static/dbview.config.php b/static/dbview.config.php index 4188c7726..014973de2 100644 --- a/static/dbview.config.php +++ b/static/dbview.config.php @@ -91,7 +91,6 @@ "deleted" => ["post-user", "deleted"], "origin" => ["post-user", "origin"], "parent-origin" => ["post-thread-user", "origin"], - "forum_mode" => ["post-thread-user", "forum_mode"], "mention" => ["post-thread-user", "mention"], "global" => ["post-user", "global"], "network" => ["post-user", "network"], @@ -250,7 +249,6 @@ "unseen" => ["post-thread-user", "unseen"], "deleted" => ["post-user", "deleted"], "origin" => ["post-thread-user", "origin"], - "forum_mode" => ["post-thread-user", "forum_mode"], "mention" => ["post-thread-user", "mention"], "global" => ["post-user", "global"], "network" => ["post-thread-user", "network"], diff --git a/static/defaults.config.php b/static/defaults.config.php index 3cde0bfd4..97e4b0b54 100644 --- a/static/defaults.config.php +++ b/static/defaults.config.php @@ -37,6 +37,11 @@ return [ // Can be used instead of adding a port number to the hostname 'port' => null, + // socket (String) + // Socket of the database server. + // Can be used instead of adding a socket location to the hostname + 'socket' => '', + // user (String) // Database user name. Please don't use "root". 'username' => '', @@ -616,10 +621,6 @@ return [ // Logs every call to /inbox as a JSON file in Friendica's temporary directory 'ap_inbox_log' => false, - // show_direction (Boolean) - // Display if a post had been fetched or had been pushed towards our server - 'show_direction' => false, - // total_ap_delivery (Boolean) // Deliver via AP to every possible receiver and we suppress the delivery to these contacts with other protocols 'total_ap_delivery' => false, diff --git a/static/env.config.php b/static/env.config.php index 59271cb65..a83b85b52 100644 --- a/static/env.config.php +++ b/static/env.config.php @@ -26,6 +26,7 @@ return [ 'MYSQL_USERNAME' => ['database', 'username'], 'MYSQL_USER' => ['database', 'username'], 'MYSQL_PORT' => ['database', 'port'], + 'MYSQL_SOCKET' => ['database', 'socket'], 'MYSQL_PASSWORD' => ['database', 'password'], 'MYSQL_DATABASE' => ['database', 'database'], diff --git a/static/routes.config.php b/static/routes.config.php index 758462ca0..c08319eb7 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -77,6 +77,7 @@ $apiRoutes = [ => [Module\Api\Friendica\Activity::class, [ R::POST]], '/notification/seen[.{extension:json|xml|rss|atom}]' => [Module\Api\Friendica\Notification\Seen::class, [ R::POST]], '/notification[.{extension:json|xml|rss|atom}]' => [Module\Api\Friendica\Notification::class, [R::GET ]], + '/notifications[.{extension:json|xml|rss|atom}]' => [Module\Api\Friendica\Notification::class, [R::GET ]], '/direct_messages_setseen[.{extension:json|xml|rss|atom}]' => [Module\Api\Friendica\DirectMessages\Setseen::class, [ R::POST]], '/direct_messages_search[.{extension:json|xml|rss|atom}]' => [Module\Api\Friendica\DirectMessages\Search ::class, [R::GET ]], '/events[.{extension:json|xml|rss|atom}]' => [Module\Api\Friendica\Events\Index::class, [R::GET ]], @@ -448,6 +449,8 @@ return [ '/{id:\d+}' => [Module\Notifications\Notification::class, [R::GET, R::POST]], ], + '/notify/{notify_id:\d+}' => [Module\Notifications\Notification::class, [R::GET]], + '/oauth' => [ '/acknowledge' => [Module\OAuth\Acknowledge::class, [R::GET, R::POST]], '/authorize' => [Module\OAuth\Authorize::class, [R::GET]], @@ -486,6 +489,7 @@ return [ '/{type}/{customsize:\d+}/{nickname_ext}' => [Module\Photo::class, [R::GET]], ], + '/ping' => [Module\Notifications\Ping::class, [R::GET]], '/pretheme' => [Module\ThemeDetails::class, [R::GET]], '/probe' => [Module\Debug\Probe::class, [R::GET]], diff --git a/static/settings.config.php b/static/settings.config.php index 24fce52f2..ea7558e5f 100644 --- a/static/settings.config.php +++ b/static/settings.config.php @@ -140,6 +140,10 @@ return [ // If you don't want to set a maximum length, set to -1. 'max_image_length' => -1, + // max_receivers (Integer) + // The maximum number of displayed receivers of posts + 'max_receivers' => 10, + // maximagesize (Integer) // Maximum size in bytes of an uploaded photo. 'maximagesize' => 800000, diff --git a/tests/datasets/api.fixture.php b/tests/datasets/api.fixture.php index 6438fc9f9..ace928b09 100644 --- a/tests/datasets/api.fixture.php +++ b/tests/datasets/api.fixture.php @@ -141,6 +141,7 @@ return [ 'self' => 1, 'nurl' => 'http://localhost/profile/selfcontact', 'url' => 'http://localhost/profile/selfcontact', + 'notify' => 'http://localhost/friendica/inbox', 'about' => 'User used in tests', 'prvkey' => "-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDVqxF9kIgtgRL0+q+jTi578FA1r1+crEmlYc0pdxcbmmrhjuRc\nrK1gX3r0mnP25fkHzG+6CAjgbDBRFM1/RXBCyp/KHVks7eQ4yr4MxTRlsxo5qf2o\nnbyNzM7Q+LZhFhe/yIoGN/fuEjlqBE98IfPOrUjsQPX240vGNXIkfLiAWwIDAQAB\nAoGBAIwuiPIdggqAtWQ+mD8HCx5LQwSFw6/xpPu5F7ZNqL52aAsGCbL3o2QoIG4c\na1qf9Ot16BNgNBqxQF3hzRTkBMrKYlmNTUkwJXun/zjQJq2JvOlcrSuXlIucUjs4\nXekVN25aYPHrX9m2FEIUwZTb4UYXbR80KbIDI53BkQ6EwSbpAkEA7aO49CR2Hf1Y\n1d2GaUI/Z0wvbj//+t0Kg0bPt16ca8KVjEQQA5ylsDaiw510jDz9NBQxSOk6If23\nUeRixc1RDQJBAOYtN4YnPM1Zfp6IxXlqMCc+xUWRTPEPFt+WpG+v79koNamAeA6o\nZzTl92hl58IqSdbgojeE2zXWQRvlimFMLQcCQQCV6jND0byyLqFcSeQBg0l8YROK\n+dUC7W80YfeoNod3c8nkMwvnO2tLPyxvO2XLEq6prBNra7bAus5rWyj0oBIBAkEA\n1EvUMFm0TLpEfLgtWuTD8Q6GKLnxO0ztjd+FXrXpBGN/ywyArxRHzJRmctW6wmz6\nmcOqGobhIHCysKYv0bnOtQJAc2M5RwlASHH4jGJzXgt3nboyiJfufM0RV9iry3ho\nCXQRWAONKoLqnsfC6qNP8OzY8FMJcwmPWj7Q/6z6yLBFTA==\n-----END RSA PRIVATE KEY-----", 'pubkey' => "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVqxF9kIgtgRL0+q+jTi578FA1\nr1+crEmlYc0pdxcbmmrhjuRcrK1gX3r0mnP25fkHzG+6CAjgbDBRFM1/RXBCyp/K\nHVks7eQ4yr4MxTRlsxo5qf2onbyNzM7Q+LZhFhe/yIoGN/fuEjlqBE98IfPOrUjs\nQPX240vGNXIkfLiAWwIDAQAB\n-----END PUBLIC KEY-----", @@ -161,6 +162,7 @@ return [ 'self' => 0, 'nurl' => 'http://localhost/profile/othercontact', 'url' => 'http://localhost/profile/othercontact', + 'notify' => 'http://localhost/friendica/inbox', 'pending' => 0, 'blocked' => 0, 'rel' => Contact::NOTHING, @@ -176,6 +178,7 @@ return [ 'self' => 0, 'nurl' => 'http://localhost/profile/friendcontact', 'url' => 'http://localhost/profile/friendcontact', + 'notify' => 'http://localhost/friendica/inbox', 'pending' => 0, 'blocked' => 0, 'rel' => Contact::SHARING, @@ -191,6 +194,7 @@ return [ 'self' => 0, 'nurl' => 'http://localhost/profile/friendcontact', 'url' => 'http://localhost/profile/friendcontact', + 'notify' => 'http://localhost/friendica/inbox', 'pending' => 0, 'blocked' => 0, 'rel' => Contact::SHARING, @@ -206,6 +210,7 @@ return [ 'self' => 0, 'nurl' => 'http://localhost/profile/mutualcontact', 'url' => 'http://localhost/profile/mutualcontact', + 'notify' => 'http://localhost/friendica/inbox', 'pending' => 0, 'blocked' => 0, 'rel' => Contact::FRIEND, @@ -221,6 +226,7 @@ return [ 'self' => 0, 'nurl' => 'http://localhost/profile/mutualcontact', 'url' => 'http://localhost/profile/mutualcontact', + 'notify' => 'http://localhost/friendica/inbox', 'pending' => 0, 'blocked' => 0, 'rel' => Contact::SHARING, @@ -236,6 +242,7 @@ return [ 'self' => 0, 'nurl' => 'http://localhost/profile/selfcontact', 'url' => 'http://localhost/profile/selfcontact', + 'notify' => 'http://localhost/friendica/inbox', 'about' => 'User used in tests', 'pending' => 0, 'blocked' => 0, @@ -896,18 +903,18 @@ return [ [ 'id' => 1, 'type' => 8, - 'name' => 'Reply to', - 'url' => 'http://localhost/display/1', + 'name' => 'Friend contact', + 'url' => 'http://localhost/profile/friendcontact', 'photo' => 'http://localhost/', 'date' => '2020-01-01 12:12:02', 'msg' => 'A test reply from an item', 'uid' => 42, - 'link' => 'http://localhost/notification/1', + 'link' => 'http://localhost/display/1', 'iid' => 4, 'seen' => 0, 'verb' => \Friendica\Protocol\Activity::POST, 'otype' => Notification\ObjectType::ITEM, - 'name_cache' => 'Reply to', + 'name_cache' => 'Friend contact', 'msg_cache' => 'A test reply from an item', ], ], diff --git a/tests/src/Console/ConfigConsoleTest.php b/tests/src/Console/ConfigConsoleTest.php index dd7925ed9..eae622725 100644 --- a/tests/src/Console/ConfigConsoleTest.php +++ b/tests/src/Console/ConfigConsoleTest.php @@ -102,7 +102,7 @@ class ConfigConsoleTest extends ConsoleTest $console->setArgument(0, 'config'); $console->setArgument(1, 'test'); $txt = $this->dumpExecute($console); - self::assertEquals("config.test => \n", $txt); + self::assertEquals("config.test => NULL\n", $txt); } public function testSetArrayValue() diff --git a/tests/src/Module/Api/Friendica/NotificationTest.php b/tests/src/Module/Api/Friendica/NotificationTest.php index 4e8509b4e..3c17471b0 100644 --- a/tests/src/Module/Api/Friendica/NotificationTest.php +++ b/tests/src/Module/Api/Friendica/NotificationTest.php @@ -62,7 +62,7 @@ class NotificationTest extends ApiTest $assertXml = << - + XML; diff --git a/tests/src/Module/Api/Twitter/Statuses/UserTimelineTest.php b/tests/src/Module/Api/Twitter/Statuses/UserTimelineTest.php index 6ad57c247..078a88f96 100644 --- a/tests/src/Module/Api/Twitter/Statuses/UserTimelineTest.php +++ b/tests/src/Module/Api/Twitter/Statuses/UserTimelineTest.php @@ -41,7 +41,7 @@ class UserTimelineTest extends ApiTest 'user_id' => 42, 'max_id' => 10, 'exclude_replies' => true, - 'conversation_id' => 7, + 'conversation_id' => 1, ]); $json = $this->toJson($response); diff --git a/update.php b/update.php index 7b5f6778b..016fe8a2e 100644 --- a/update.php +++ b/update.php @@ -55,6 +55,7 @@ use Friendica\Model\Notification; use Friendica\Model\Photo; use Friendica\Model\Post; use Friendica\Model\Profile; +use Friendica\Model\User; use Friendica\Security\PermissionSet\Repository\PermissionSet; use Friendica\Worker\Delivery; @@ -1087,3 +1088,12 @@ function update_1446() return Update::SUCCESS; } + +function update_1451() +{ + DBA::update('user', ['account-type' => User::ACCOUNT_TYPE_COMMUNITY], ['page-flags' => [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_PRVGROUP]]); + DBA::update('contact', ['contact-type' => Contact::TYPE_COMMUNITY], ["`forum` OR `prv`"]); + DBA::update('contact', ['manually-approve' => true], ['prv' => true]); + + return Update::SUCCESS; +} diff --git a/view/js/friendica-tagsinput/friendica-tagsinput.js b/view/js/friendica-tagsinput/friendica-tagsinput.js index e3db53df4..45c00641f 100644 --- a/view/js/friendica-tagsinput/friendica-tagsinput.js +++ b/view/js/friendica-tagsinput/friendica-tagsinput.js @@ -165,7 +165,7 @@ // add