diff --git a/.htaccess-dist b/.htaccess-dist index 3c90982515..404137168a 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 69886557e3..d334a4307a 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 da4dc5b2c3..acac6ed2c4 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 45131d447c..85a2198ff5 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 5a88e44cd3..e81783404b 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 d8d93b63f2..cc3a0db5fd 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 c0e14fe525..fa20188253 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 b7a923618d..39e40c044a 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 cec90bea4c..2e1d952094 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 578cffe7ca..ce8412547a 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 3e1e911095..95b33f380c 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 47f4f9e785..8bbfdd99ce 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 628e84e211..f64434b886 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 1892de3e40..ad7fa4a3dd 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 250734e56c..88d814c040 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 7307dc78d6..0b74837411 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 3381ef48f1..32e69a2fd7 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 73905567cd..6d814edcb4 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 1186458198..da41c8656e 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 7cf2e53d49..855e7de3f2 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 81aa9ec620..0000000000 --- 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 bd11e0971a..1ccd74b7f1 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 111e523302..642229de0b 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 0aa8a87b50..10830bd103 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 e80cc66147..e3e9e22cb3 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 0a38f607f4..e32983a68a 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 11f7f87ced..f051d870a2 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 af96deebdb..e441e8ba4b 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 13c9b31427..d594891b85 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 5037be80e2..f6906b7962 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 e676673fc1..2c17663c1e 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 b5ccd6f14d..8633992870 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 d230afd303..7f75c6c9c0 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 a4acf58bad..40612fadfc 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 a58936cfc9..a3a5cf7c84 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 1b26265255..c141bbc54e 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 de0c80b3de..16bc2360ef 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 d43d8c7281..5c23d64456 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 708fb7d836..835f1ffed7 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 cc7f754ee6..88d8d7d0f6 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 ff60fff7be..6d744a1ba1 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 002a48e6aa..ee74c257df 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 30cdb8bcf7..ed138e2bdb 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 b519655e13..55dd756638 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 0af28a7401..e1152b4081 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 c4138cc2d0..b739c74208 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 17e0a18e2e..fa41d26467 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 bf1f2585a1..5047af5986 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 79a0b5f72b..e82a01fbce 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 07cad9cf6f..254b88115c 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 ad7b9c4904..0873646a41 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 cb7fa65479..d779355202 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 17a68f120f..1cc48bd2f7 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 fbf5bf282e..54584de182 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 c085d86836..f3a37b7dad 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 1d87b81dd0..bf49de8250 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 1104212f6b..d56e020615 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 2bac3d79ed..4c5c38ea54 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 25724dc728..dc6bf58184 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 132a35b67b..439e78e642 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 9677ce30f2..cf0ddcfc6e 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 3c892b3030..de37c9c747 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 594f85d418..257c9f06f4 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 d150827694..89c257e73d 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 ba87081d45..b4b6a39970 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 08cfe82fb9..65ef9654b8 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 94c0e3712a..41d0a6f5e3 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 db1e049db9..29aa82b49e 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 4115e0e6b6..e5e0a95796 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 6dcde12b7d..413cacae29 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 7bf40c837e..50e0eaa725 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 f33b88728a..95a072889c 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 9dc9e73f90..103fac04af 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 828741a195..ea7ca42d5c 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 a9c6c962d5..6544fe470c 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 0c25e9d188..0d3046afb1 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 e2e0dd70c5..b730f06636 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 378159c3c2..d34d79fae0 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 e5aca1ad5c..c3ff0030b4 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 2bf27697b4..177c5fd0ba 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 5da579b7ce..db25c4eb96 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 be08483532..e54166b990 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 41314bb1b7..a607d4c950 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 e9bbb93bdf..800a91110b 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 af0436b4a2..96469fb437 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 aba330a658..a247e6688f 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 1d67443b1c..d89c2300b8 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 64533d0bc9..16bf462678 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 45e56cea85..0041c03a70 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 b6dcd86c91..05aa079624 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 d7319fa4ea..1190e046df 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 d571016f19..c7b2870ece 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 ec77a19cb9..cd03e2533a 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 e02a6a3dc6..8ab8dae60e 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 59b3bdafae..35cb481495 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 908d0a63b4..2978ecbb9d 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 5de4b93b74..ec7fee3f46 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 498d1b13ed..3b10493618 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 3a0a9fa34e..8d7b8611b6 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 d72e3c2502..a28b5d6f65 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 e090aa5a38..90ea9c28ba 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 0000000000..2f79b91f0d --- /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 71ce2beee2..58b6df086d 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 c67520b554..3d3110fd8b 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 2bb7b6eaa5..bb4537d6cc 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 d415abf74c..9c09baef82 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 adc4fa68db..49130e8c57 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 8dad2f6af2..0b907caf7d 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 3dd3775d04..c1ad8bbbf7 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 15cf3be537..0f12bfe84f 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 bf47bdafc6..1070440073 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 0000000000..710f25fa33 --- /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 0000000000..d8d6dc029d --- /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 f1df3c173b..d4aba639c5 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 6578238343..ac3a2560e8 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 fbc6144702..d6c777e6f2 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 c4035663b0..08ca1f095b 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 9b195a753b..9773b6446a 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 0000000000..d2fae060aa --- /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 09d41fa876..ac0db65ad2 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 70184c3131..0ae96c972f 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 e93c1c07b3..759cd6e4e0 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 5061c5729a..40a55c6d7d 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 987cf00e6a..12fef7ac8f 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 9c1d5d3746..54b4fa2d3b 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 0561f5506c..253b4b4f18 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 76f8cdb850..5651343cb5 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 a28cc96170..98d40137a9 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 0b7159ec90..2cab827bb1 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 3a14a0db9a..0fb7394579 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 45879df2ae..9f7781d13d 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 09792935e0..db22b76973 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 048e9974ce..cde81394d3 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 86716e9937..a2c9a84473 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 44a741d7c8..50dda5eba1 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 7603b622b7..68f6cb1dd7 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 077509d3cb..d7a58b52aa 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 0000000000..726a69b8da --- /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 0000000000..a6d8c59445 --- /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 3be49a38fe..c09181d3e6 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 f46b1b0d9a..80628d7dba 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 7b9f5acbd9..45ecb62291 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 d7348b2586..2beb890918 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 2e1a2191c0..8963b1df60 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 4188c7726d..014973de26 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 3cde0bfd49..97e4b0b54b 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 59271cb652..a83b85b52b 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 758462ca03..c08319eb73 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 24fce52f21..ea7558e5f7 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 6438fc9f9e..ace928b090 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 dd7925ed92..eae6227251 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 4e8509b4eb..3c17471b03 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 6ad57c247d..078a88f96f 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 7b5f6778be..016fe8a2ec 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 e3db53df41..45c00641fd 100644 --- a/view/js/friendica-tagsinput/friendica-tagsinput.js +++ b/view/js/friendica-tagsinput/friendica-tagsinput.js @@ -165,7 +165,7 @@ // add