]> git.mxchange.org Git - friendica.git/blob - src/Model/Post/Engagement.php
Compare with lowered tags
[friendica.git] / src / Model / Post / Engagement.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2024, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Model\Post;
23
24 use Friendica\Content\Text\BBCode;
25 use Friendica\Core\Logger;
26 use Friendica\Core\Protocol;
27 use Friendica\Database\Database;
28 use Friendica\Database\DBA;
29 use Friendica\DI;
30 use Friendica\Model\Contact;
31 use Friendica\Model\Item;
32 use Friendica\Model\Post;
33 use Friendica\Model\Tag;
34 use Friendica\Model\Verb;
35 use Friendica\Protocol\Activity;
36 use Friendica\Protocol\ActivityPub\Receiver;
37 use Friendica\Protocol\Relay;
38 use Friendica\Util\DateTimeFormat;
39
40 class Engagement
41 {
42         const KEYWORDS = ['source', 'server', 'from', 'to', 'group', 'tag', 'network', 'platform', 'visibility'];
43
44         /**
45          * Store engagement data from an item array
46          *
47          * @param array $item
48          * @return int uri-id of the engagement post if newly inserted, 0 on update
49          */
50         public static function storeFromItem(array $item): int
51         {
52                 if (in_array($item['verb'], [Activity::FOLLOW, Activity::VIEW, Activity::READ])) {
53                         Logger::debug('Technical activities are not stored', ['uri-id' => $item['uri-id'], 'parent-uri-id' => $item['parent-uri-id'], 'verb' => $item['verb']]);
54                         return 0;
55                 }
56
57                 $parent = Post::selectFirst(['uri-id', 'created', 'author-id', 'owner-id', 'uid', 'private', 'contact-contact-type', 'language', 'network',
58                         'title', 'content-warning', 'body', 'author-contact-type', 'author-nick', 'author-addr', 'author-gsid', 'owner-contact-type', 'owner-nick', 'owner-addr', 'owner-gsid'],
59                         ['uri-id' => $item['parent-uri-id']]);
60
61                 if ($parent['created'] < self::getCreationDateLimit(false)) {
62                         Logger::debug('Post is too old', ['uri-id' => $item['uri-id'], 'parent-uri-id' => $item['parent-uri-id'], 'created' => $parent['created']]);
63                         return 0;
64                 }
65
66                 $store = ($item['gravity'] != Item::GRAVITY_PARENT);
67
68                 if (!$store) {
69                         $store = Contact::hasFollowers($parent['owner-id']);
70                 }
71
72                 if (!$store && ($parent['owner-id'] != $parent['author-id'])) {
73                         $store = Contact::hasFollowers($parent['author-id']);
74                 }
75
76                 if (!$store) {
77                         $tagList = Relay::getSubscribedTags();
78                         foreach (array_column(Tag::getByURIId($item['parent-uri-id'], [Tag::HASHTAG]), 'name') as $tag) {
79                                 if (in_array(mb_strtolower($tag), $tagList)) {
80                                         $store = true;
81                                         break;
82                                 }
83                         }
84                 }
85
86                 $mediatype = self::getMediaType($item['parent-uri-id']);
87
88                 if (!$store) {
89                         $store = !empty($mediatype);
90                 }
91
92                 $searchtext = self::getSearchTextForItem($parent);
93                 if (!$store) {
94                         $tags     = array_column(Tag::getByURIId($item['parent-uri-id'], [Tag::HASHTAG]), 'name');
95                         $language = !empty($parent['language']) ? (array_key_first(json_decode($parent['language'], true)) ?? '') : '';
96                         $store    = DI::userDefinedChannel()->match($searchtext, $language, $tags, $mediatype);
97                 }
98
99                 $engagement = [
100                         'uri-id'       => $item['parent-uri-id'],
101                         'owner-id'     => $parent['owner-id'],
102                         'contact-type' => $parent['contact-contact-type'],
103                         'media-type'   => $mediatype,
104                         'language'     => $parent['language'],
105                         'searchtext'   => $searchtext,
106                         'created'      => $parent['created'],
107                         'restricted'   => !in_array($item['network'], Protocol::FEDERATED) || ($parent['private'] != Item::PUBLIC),
108                         'comments'     => DBA::count('post', ['parent-uri-id' => $item['parent-uri-id'], 'gravity' => Item::GRAVITY_COMMENT]),
109                         'activities'   => DBA::count('post', [
110                                 "`parent-uri-id` = ? AND `gravity` = ? AND NOT `vid` IN (?, ?, ?)",
111                                 $item['parent-uri-id'], Item::GRAVITY_ACTIVITY,
112                                 Verb::getID(Activity::FOLLOW), Verb::getID(Activity::VIEW), Verb::getID(Activity::READ)
113                         ])
114                 ];
115                 if (!$store && ($engagement['comments'] == 0) && ($engagement['activities'] == 0)) {
116                         Logger::debug('No media, follower, subscribed tags, comments or activities. Engagement not stored', ['fields' => $engagement]);
117                         return 0;
118                 }
119                 $exists = DBA::exists('post-engagement', ['uri-id' => $engagement['uri-id']]);
120                 if ($exists) {
121                         $ret = DBA::update('post-engagement', $engagement, ['uri-id' => $engagement['uri-id']]);
122                         Logger::debug('Engagement updated', ['uri-id' => $engagement['uri-id'], 'ret' => $ret]);
123                 } else {
124                         $ret = DBA::insert('post-engagement', $engagement);
125                         Logger::debug('Engagement inserted', ['uri-id' => $engagement['uri-id'], 'ret' => $ret]);
126                 }
127                 return ($ret && !$exists) ? $engagement['uri-id'] : 0;
128         }
129
130         public static function getSearchTextForActivity(string $content, int $author_id, array $tags, array $receivers): string
131         {
132                 $author = Contact::getById($author_id);
133
134                 $item = [
135                         'uri-id'              => 0,
136                         'network'             => Protocol::ACTIVITYPUB,
137                         'title'               => '',
138                         'content-warning'     => '',
139                         'body'                => $content,
140                         'private'             => Item::PRIVATE,
141                         'author-id'           => $author_id,
142                         'author-contact-type' => $author['contact-type'],
143                         'author-nick'         => $author['nick'],
144                         'author-addr'         => $author['addr'],
145                         'author-gsid'         => $author['gsid'],
146                         'owner-id'            => $author_id,
147                         'owner-contact-type'  => $author['contact-type'],
148                         'owner-nick'          => $author['nick'],
149                         'owner-addr'          => $author['addr'],
150                         'author-gsid'         => $author['gsid'],
151                 ];
152
153                 foreach ($receivers as $receiver) {
154                         if ($receiver == Receiver::PUBLIC_COLLECTION) {
155                                 $item['private'] = Item::PUBLIC;
156                         }
157                 }
158
159                 return self::getSearchText($item, $receivers, $tags);
160         }
161
162         private static function getSearchTextForItem(array $item): string
163         {
164                 $receivers = array_column(Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION, Tag::AUDIENCE]), 'url');
165                 $tags      = array_column(Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]), 'name');
166                 return self::getSearchText($item, $receivers, $tags);
167         }
168
169         private static function getSearchText(array $item, array $receivers, array $tags): string
170         {
171                 $body = '[nosmile]network:' . $item['network'];
172
173                 if (!empty($item['author-gsid'])) {
174                         $gserver = DBA::selectFirst('gserver', ['platform', 'nurl'], ['id' => $item['author-gsid']]);
175                         $platform = preg_replace( '/[\W]/', '', $gserver['platform'] ?? '');
176                         if (!empty($platform)) {
177                                 $body .= ' platform:' . $platform;
178                         }
179                         $body .= ' server:' . parse_url($gserver['nurl'], PHP_URL_HOST);
180                 }
181
182                 if (($item['owner-contact-type'] == Contact::TYPE_COMMUNITY) && !empty($item['owner-gsid']) && ($item['owner-gsid'] != ($item['author-gsid'] ?? 0))) {
183                         $gserver = DBA::selectFirst('gserver', ['platform', 'nurl'], ['id' => $item['owner-gsid']]);
184                         $platform = preg_replace( '/[\W]/', '', $gserver['platform'] ?? '');
185                         if (!empty($platform) && !strpos($body, 'platform:' . $platform)) {
186                                 $body .= ' platform:' . $platform;
187                         }
188                         $body .= ' server:' . parse_url($gserver['nurl'], PHP_URL_HOST);
189                 }
190
191                 switch ($item['private']) {
192                         case Item::PUBLIC:
193                                 $body .= ' visibility:public';
194                                 break;
195                         case Item::UNLISTED:
196                                 $body .= ' visibility:unlisted';
197                                 break;
198                         case Item::PRIVATE:
199                                 $body .= ' visibility:private';
200                                 break;
201                 }
202
203                 if (in_array(Contact::TYPE_COMMUNITY, [$item['author-contact-type'], $item['owner-contact-type']])) {
204                         $body .= ' source:group';
205                 } elseif ($item['author-contact-type'] == Contact::TYPE_PERSON) {
206                         $body .= ' source:person';
207                 } elseif ($item['author-contact-type'] == Contact::TYPE_NEWS) {
208                         $body .= ' source:service';
209                 } elseif ($item['author-contact-type'] == Contact::TYPE_ORGANISATION) {
210                         $body .= ' source:organization';
211                 } elseif ($item['author-contact-type'] == Contact::TYPE_RELAY) {
212                         $body .= ' source:application';
213                 }
214
215                 if ($item['author-contact-type'] == Contact::TYPE_COMMUNITY) {
216                         $body .= ' group:' . $item['author-nick'] . ' group:' . $item['author-addr'];
217                 } elseif (in_array($item['author-contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
218                         $body .= ' from:' . $item['author-nick'] . ' from:' . $item['author-addr'];
219                 }
220
221                 if ($item['author-id'] !=  $item['owner-id']) {
222                         if ($item['owner-contact-type'] == Contact::TYPE_COMMUNITY) {
223                                 $body .= ' group:' . $item['owner-nick'] . ' group:' . $item['owner-addr'];
224                         } elseif (in_array($item['owner-contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
225                                 $body .= ' from:' . $item['owner-nick'] . ' from:' . $item['owner-addr'];
226                         }
227                 }
228
229                 foreach ($receivers as $receiver) {
230                         $contact = Contact::getByURL($receiver, false, ['nick', 'addr', 'contact-type']);
231                         if (empty($contact)) {
232                                 continue;
233                         }
234
235                         if (($contact['contact-type'] == Contact::TYPE_COMMUNITY) && !strpos($body, 'group:' . $contact['addr'])) {
236                                 $body .= ' group:' . $contact['nick'] . ' group:' . $contact['addr'];
237                         } elseif (in_array($contact['contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
238                                 $body .= ' to:' . $contact['nick'] . ' to:' . $contact['addr'];
239                         }
240                 }
241
242                 foreach ($tags as $tag) {
243                         $body .= ' tag:' . $tag;
244                 }
245
246                 $body .= ' ' . $item['title'] . ' ' . $item['content-warning'] . ' ' . $item['body'];
247
248                 return BBCode::toSearchText($body, $item['uri-id']);
249         }
250
251         private static function getMediaType(int $uri_id): int
252         {
253                 $media = Post\Media::getByURIId($uri_id);
254                 $type  = 0;
255                 foreach ($media as $entry) {
256                         if ($entry['type'] == Post\Media::IMAGE) {
257                                 $type = $type | 1;
258                         } elseif ($entry['type'] == Post\Media::VIDEO) {
259                                 $type = $type | 2;
260                         } elseif ($entry['type'] == Post\Media::AUDIO) {
261                                 $type = $type | 4;
262                         }
263                 }
264                 return $type;
265         }
266
267         /**
268          * Expire old engagement data
269          *
270          * @return void
271          */
272         public static function expire()
273         {
274                 $limit = self::getCreationDateLimit(true);
275                 if (empty($limit)) {
276                         Logger::notice('Expiration limit not reached');
277                         return;
278                 }
279                 DBA::delete('post-engagement', ["`created` < ?", $limit]);
280                 Logger::notice('Cleared expired engagements', ['limit' => $limit, 'rows' => DBA::affectedRows()]);
281         }
282
283         private static function getCreationDateLimit(bool $forDeletion): string
284         {
285                 $posts = DI::config()->get('channel', 'engagement_post_limit');
286                 if (!empty($posts)) {
287                         $limit = DBA::selectToArray('post-engagement', ['created'], [], ['limit' => [$posts, 1], 'order' => ['created' => true]]);
288                         if (!empty($limit)) {
289                                 return $limit[0]['created'];
290                         } elseif ($forDeletion) {
291                                 return '';
292                         }
293                 }
294
295                 return DateTimeFormat::utc('now - ' . DI::config()->get('channel', 'engagement_hours') . ' hour');
296         }
297 }