]> git.mxchange.org Git - friendica.git/blob - src/Model/Post/Engagement.php
Merge remote-tracking branch 'upstream/2023.09-rc' into user-defined-channels
[friendica.git] / src / Model / Post / Engagement.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2023, 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\Relay;
37 use Friendica\Util\DateTimeFormat;
38 use Friendica\Util\Strings;
39
40 // Channel
41
42 class Engagement
43 {
44         /**
45          * Store engagement data from an item array
46          *
47          * @param array $item
48          * @return void
49          */
50         public static function storeFromItem(array $item)
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;
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', 'owner-contact-type', 'owner-nick', 'owner-addr'],
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;
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) {
73                         $tagList = Relay::getSubscribedTags();
74                         foreach (array_column(Tag::getByURIId($item['parent-uri-id'], [Tag::HASHTAG]), 'name') as $tag) {
75                                 if (in_array($tag, $tagList)) {
76                                         $store = true;
77                                         break;
78                                 }
79                         }
80                 }
81
82                 $mediatype = self::getMediaType($item['parent-uri-id']);
83
84                 if (!$store) {
85                         $mediatype = !empty($mediatype);
86                 }
87
88                 $engagement = [
89                         'uri-id'       => $item['parent-uri-id'],
90                         'owner-id'     => $parent['owner-id'],
91                         'contact-type' => $parent['contact-contact-type'],
92                         'media-type'   => $mediatype,
93                         'language'     => $parent['language'],
94                         'searchtext'   => self::getSearchText($parent),
95                         'created'      => $parent['created'],
96                         'restricted'   => !in_array($item['network'], Protocol::FEDERATED) || ($parent['private'] != Item::PUBLIC),
97                         'comments'     => DBA::count('post', ['parent-uri-id' => $item['parent-uri-id'], 'gravity' => Item::GRAVITY_COMMENT]),
98                         'activities'   => DBA::count('post', [
99                                 "`parent-uri-id` = ? AND `gravity` = ? AND NOT `vid` IN (?, ?, ?)",
100                                 $item['parent-uri-id'], Item::GRAVITY_ACTIVITY,
101                                 Verb::getID(Activity::FOLLOW), Verb::getID(Activity::VIEW), Verb::getID(Activity::READ)
102                         ])
103                 ];
104                 if (!$store && ($engagement['comments'] == 0) && ($engagement['activities'] == 0)) {
105                         Logger::debug('No media, follower, subscribed tags, comments or activities. Engagement not stored', ['fields' => $engagement]);
106                         return;
107                 }
108                 $ret = DBA::insert('post-engagement', $engagement, Database::INSERT_UPDATE);
109                 Logger::debug('Engagement stored', ['fields' => $engagement, 'ret' => $ret]);
110         }
111
112         private static function getSearchText(array $item): string
113         {
114                 $body = '[nosmile]network:' . $item['network'];
115
116                 switch ($item['private']) {
117                         case Item::PUBLIC:
118                                 $body .= ' visibility:public';
119                                 break;
120                         case Item::UNLISTED:
121                                 $body .= ' visibility:unlisted';
122                                 break;
123                         case Item::PRIVATE:
124                                 $body .= ' visibility:private';
125                                 break;
126                 }
127
128                 if ($item['author-contact-type'] == Contact::TYPE_COMMUNITY) {
129                         $body .= ' group:' . $item['author-nick'] . ' group:' . $item['author-addr'];
130                 } elseif (in_array($item['author-contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
131                         $body .= ' from:' . $item['author-nick'] . ' from:' . $item['author-addr'];
132                 }
133
134                 if ($item['author-id'] !=  $item['owner-id']) {
135                         if ($item['owner-contact-type'] == Contact::TYPE_COMMUNITY) {
136                                 $body .= ' group:' . $item['owner-nick'] . ' group:' . $item['owner-addr'];
137                         } elseif (in_array($item['owner-contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
138                                 $body .= ' from:' . $item['owner-nick'] . ' from:' . $item['owner-addr'];
139                         }
140                 }
141
142                 foreach (Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION, Tag::AUDIENCE]) as $tag) {
143                         $contact = Contact::getByURL($tag['name'], false, ['nick', 'addr', 'contact-type']);
144                         if (empty($contact)) {
145                                 continue;
146                         }
147
148                         if (($contact['contact-type'] == Contact::TYPE_COMMUNITY) && !strpos($body, 'group:' . $contact['addr'])) {
149                                 $body .= ' group:' . $contact['nick'] . ' group:' . $contact['addr'];
150                         } elseif (in_array($contact['contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
151                                 $body .= ' to:' . $contact['nick'] . ' to:' . $contact['addr'];
152                         }
153                 }
154
155                 foreach (Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]) as $tag) {
156                         $body .= ' tag:' . $tag['name'];
157                 }
158
159                 $body .= ' ' . $item['title'] . ' ' . $item['content-warning'] . ' ' . $item['body'];
160
161                 $body = preg_replace("~\[url\=.*\]https?:.*\[\/url\]~", '', $body);
162
163                 $body = Post\Media::addAttachmentsToBody($item['uri-id'], $body, [Post\Media::IMAGE]);
164                 $text = BBCode::toPlaintext($body, false);
165                 $text = preg_replace(Strings::autoLinkRegEx(), '', $text);
166
167                 do {
168                         $oldtext = $text;
169                         $text = str_replace(['  ', "\n", "\r"], ' ', $text);
170                 } while ($oldtext != $text);
171
172                 return $text;
173         }
174
175         private static function getMediaType(int $uri_id): int
176         {
177                 $media = Post\Media::getByURIId($uri_id);
178                 $type  = 0;
179                 foreach ($media as $entry) {
180                         if ($entry['type'] == Post\Media::IMAGE) {
181                                 $type = $type | 1;
182                         } elseif ($entry['type'] == Post\Media::VIDEO) {
183                                 $type = $type | 2;
184                         } elseif ($entry['type'] == Post\Media::AUDIO) {
185                                 $type = $type | 4;
186                         }
187                 }
188                 return $type;
189         }
190
191         /**
192          * Expire old engagement data
193          *
194          * @return void
195          */
196         public static function expire()
197         {
198                 $limit = self::getCreationDateLimit(true);
199                 if (empty($limit)) {
200                         Logger::notice('Expiration limit not reached');
201                         return;
202                 }
203                 DBA::delete('post-engagement', ["`created` < ?", $limit]);
204                 Logger::notice('Cleared expired engagements', ['limit' => $limit, 'rows' => DBA::affectedRows()]);
205         }
206
207         private static function getCreationDateLimit(bool $forDeletion): string
208         {
209                 $posts = DI::config()->get('channel', 'engagement_post_limit');
210                 if (!empty($posts)) {
211                         $limit = DBA::selectToArray('post-engagement', ['created'], [], ['limit' => [$posts, 1], 'order' => ['created' => true]]);
212                         if (!empty($limit)) {
213                                 return $limit[0]['created'];
214                         } elseif ($forDeletion) {
215                                 return '';
216                         }
217                 }
218
219                 return DateTimeFormat::utc('now - ' . DI::config()->get('channel', 'engagement_hours') . ' hour');
220         }
221 }