]> git.mxchange.org Git - friendica-addons.git/blob - twitter/twitter.php
Avoid upload problems by reducing the picture size
[friendica-addons.git] / twitter / twitter.php
1 <?php
2 /**
3  * Name: Twitter Post Connector
4  * Description: Post to Twitter
5  * Version: 2.0
6  * Author: Tobias Diekershoff <https://f.diekershoff.de/profile/tobias>
7  * Author: Michael Vogel <https://pirati.ca/profile/heluecht>
8  * Maintainer: Hypolite Petovan <https://friendica.mrpetovan.com/profile/hypolite>
9  * Maintainer: Michael Vogel <https://pirati.ca/profile/heluecht>
10  *
11  * Copyright (c) 2011-2023 Tobias Diekershoff, Michael Vogel, Hypolite Petovan
12  * All rights reserved.
13  *
14  * Redistribution and use in source and binary forms, with or without
15  * modification, are permitted provided that the following conditions are met:
16  *    * Redistributions of source code must retain the above copyright notice,
17  *     this list of conditions and the following disclaimer.
18  *    * Redistributions in binary form must reproduce the above
19  *    * copyright notice, this list of conditions and the following disclaimer in
20  *      the documentation and/or other materials provided with the distribution.
21  *    * Neither the name of the <organization> nor the names of its contributors
22  *      may be used to endorse or promote products derived from this software
23  *      without specific prior written permission.
24  *
25  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
26  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
27  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
28  * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
29  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
30  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
32  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
33  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
34  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35  *
36  */
37
38 use Friendica\Content\Text\BBCode;
39 use Friendica\Content\Text\Plaintext;
40 use Friendica\Core\Hook;
41 use Friendica\Core\Logger;
42 use Friendica\Core\Renderer;
43 use Friendica\Core\Worker;
44 use Friendica\DI;
45 use Friendica\Model\Item;
46 use Friendica\Model\Post;
47 use Friendica\Core\Config\Util\ConfigFileManager;
48 use Friendica\Model\Photo;
49 use Friendica\Util\Images;
50 use GuzzleHttp\Client;
51 use GuzzleHttp\HandlerStack;
52 use GuzzleHttp\Subscriber\Oauth\Oauth1;
53
54 function twitter_install()
55 {
56         Hook::register('load_config'            , __FILE__, 'twitter_load_config');
57         Hook::register('connector_settings'     , __FILE__, 'twitter_settings');
58         Hook::register('connector_settings_post', __FILE__, 'twitter_settings_post');
59         Hook::register('hook_fork'              , __FILE__, 'twitter_hook_fork');
60         Hook::register('post_local'             , __FILE__, 'twitter_post_local');
61         Hook::register('notifier_normal'        , __FILE__, 'twitter_post_hook');
62         Hook::register('jot_networks'           , __FILE__, 'twitter_jot_nets');
63 }
64
65 function twitter_load_config(ConfigFileManager $loader)
66 {
67         DI::app()->getConfigCache()->load($loader->loadAddonConfig('twitter'), \Friendica\Core\Config\ValueObject\Cache::SOURCE_STATIC);
68 }
69
70 function twitter_jot_nets(array &$jotnets_fields)
71 {
72         if (!DI::userSession()->getLocalUserId()) {
73                 return;
74         }
75
76         if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'twitter', 'post')) {
77                 $jotnets_fields[] = [
78                         'type' => 'checkbox',
79                         'field' => [
80                                 'twitter_enable',
81                                 DI::l10n()->t('Post to Twitter'),
82                                 DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'twitter', 'post_by_default')
83                         ]
84                 ];
85         }
86 }
87
88 function twitter_settings_post()
89 {
90         if (!DI::userSession()->getLocalUserId() || empty($_POST['twitter-submit'])) {
91                 return;
92         }
93
94         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'twitter', 'post',            (bool)$_POST['twitter-enable']);
95         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'twitter', 'post_by_default', (bool)$_POST['twitter-default']);
96         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'twitter', 'api_key',         $_POST['twitter-api-key']);
97         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'twitter', 'api_secret',      $_POST['twitter-api-secret']);
98         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'twitter', 'access_token',    $_POST['twitter-access-token']);
99         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'twitter', 'access_secret',   $_POST['twitter-access-secret']);
100 }
101
102 function twitter_settings(array &$data)
103 {
104         if (!DI::userSession()->getLocalUserId()) {
105                 return;
106         }
107
108         $enabled      = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'twitter', 'post') ?? false;
109         $def_enabled  = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'twitter', 'post_by_default') ?? false;
110
111         $api_key       = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'twitter', 'api_key');
112         $api_secret    = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'twitter', 'api_secret');
113         $access_token  = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'twitter', 'access_token');
114         $access_secret = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'twitter', 'access_secret');
115
116         $t    = Renderer::getMarkupTemplate('connector_settings.tpl', 'addon/twitter/');
117         $html = Renderer::replaceMacros($t, [
118                 '$enable'        => ['twitter-enable', DI::l10n()->t('Allow posting to Twitter'), $enabled, DI::l10n()->t('If enabled all your <strong>public</strong> postings can be posted to the associated Twitter account. You can choose to do so by default (here) or for every posting separately in the posting options when writing the entry.')],
119                 '$default'       => ['twitter-default', DI::l10n()->t('Send public postings to Twitter by default'), $def_enabled],
120                 '$api_key'       => ['twitter-api-key', DI::l10n()->t('API Key'), $api_key],
121                 '$api_secret'    => ['twitter-api-secret', DI::l10n()->t('API Secret'), $api_secret],
122                 '$access_token'  => ['twitter-access-token', DI::l10n()->t('Access Token'), $access_token],
123                 '$access_secret' => ['twitter-access-secret', DI::l10n()->t('Access Secret'), $access_secret],
124         '$help'          => DI::l10n()->t('Each user needs to register their own app to be able to post to Twitter. Please visit https://developer.twitter.com/en/portal/projects-and-apps to register a project. Inside the project you then have to register an app. You will find the needed data for the connector on the page "Keys and token" in the app settings.'),
125         ]);
126
127         $data = [
128                 'connector' => 'twitter',
129                 'title'     => DI::l10n()->t('Twitter Export'),
130                 'enabled'   => $enabled,
131                 'image'     => 'images/twitter.png',
132                 'html'      => $html,
133         ];
134 }
135
136 function twitter_hook_fork(array &$b)
137 {
138         DI::logger()->debug('twitter_hook_fork', $b);
139
140         if ($b['name'] != 'notifier_normal') {
141                 return;
142         }
143
144         $post = $b['data'];
145
146         if ($post['deleted'] || $post['private'] || ($post['created'] !== $post['edited']) ||
147                 !strstr($post['postopts'], 'twitter') || ($post['gravity'] != Item::GRAVITY_PARENT)) {
148                 $b['execute'] = false;
149                 return;
150         }
151 }
152
153 function twitter_post_local(array &$b)
154 {
155         if (!DI::userSession()->getLocalUserId() || (DI::userSession()->getLocalUserId() != $b['uid'])) {
156                 return;
157         }
158
159         if ($b['edit'] || $b['private'] || $b['parent']) {
160                 return;
161         }
162
163         $twitter_post   = (bool)DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'twitter', 'post');
164         $twitter_enable = (($twitter_post && !empty($_REQUEST['twitter_enable'])) ? (bool)$_REQUEST['twitter_enable'] : false);
165
166         // if API is used, default to the chosen settings
167         if ($b['api_source'] && intval(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'twitter', 'post_by_default'))) {
168                 $twitter_enable = true;
169         }
170
171         if (!$twitter_enable) {
172                 return;
173         }
174
175         if (strlen($b['postopts'])) {
176                 $b['postopts'] .= ',';
177         }
178
179         $b['postopts'] .= 'twitter';
180 }
181
182 function twitter_post_hook(array &$b)
183 {
184         DI::logger()->debug('Invoke post hook', $b);
185
186         if (($b['gravity'] != Item::GRAVITY_PARENT) || !strstr($b['postopts'], 'twitter') || $b['private'] || $b['deleted'] || ($b['created'] !== $b['edited'])) {
187                 return;
188         }
189
190         $b['body'] = Post\Media::addAttachmentsToBody($b['uri-id'], DI::contentItem()->addSharedPost($b));
191
192         Logger::notice('twitter post invoked', ['id' => $b['id'], 'guid' => $b['guid']]);
193
194         DI::pConfig()->load($b['uid'], 'twitter');
195
196         $api_key       = DI::pConfig()->get($b['uid'], 'twitter', 'api_key');
197         $api_secret    = DI::pConfig()->get($b['uid'], 'twitter', 'api_secret');
198         $access_token  = DI::pConfig()->get($b['uid'], 'twitter', 'access_token');
199         $access_secret = DI::pConfig()->get($b['uid'], 'twitter', 'access_secret');
200
201         if (empty($api_key) || empty($api_secret) || empty($access_token) || empty($access_secret)) {
202                 Logger::info('Missing keys, secrets or tokens.');
203                 return;
204         }
205
206         $msgarr = Plaintext::getPost($b, 280, true, BBCode::TWITTER);
207         Logger::debug('Got plaintext', ['id' => $b['id'], 'message' => $msgarr]);
208
209         $media_ids = [];
210
211         if (!empty($msgarr['images']) || !empty($msgarr['remote_images'])) {
212                 Logger::info('Got images', ['id' => $b['id'], 'images' => $msgarr['images'] ?? []]);
213
214                 foreach ($msgarr['images'] ?? [] as $image) {
215                         if (count($media_ids) == 4) {
216                                 continue;
217                         }
218                         try {
219                                 $media_ids[] = twitter_upload_image($b['uid'], $image, $b);
220                         } catch (\Throwable $th) {
221                                 Logger::warning('Error while uploading image', ['image' => $image, 'code' => $th->getCode(), 'message' => $th->getMessage()]);
222                                 // Currently don't defer to avoid a loop.
223                 //Worker::defer();
224                 return;
225             }
226                 }
227         }
228
229         $in_reply_to_tweet_id = 0;
230
231         Logger::debug('Post message', ['id' => $b['id'], 'parts' => count($msgarr['parts'])]);
232         foreach ($msgarr['parts'] as $key => $part) {
233                 try {
234                         $id = twitter_post_status($b['uid'], $part, $media_ids, $in_reply_to_tweet_id);
235                         Logger::info('twitter_post send', ['part' => $key, 'id' => $b['id'], 'result' => $id]);
236                 } catch (\Throwable $th) {
237                         Logger::warning('Error while posting message', ['part' => $key, 'id' => $b['id'], 'code' => $th->getCode(), 'message' => $th->getMessage()]);
238                         Worker::defer();
239                         break;
240                 }
241
242                 $in_reply_to_tweet_id = $id;
243                 $media_ids = [];
244         }
245 }
246
247 function twitter_post_status(int $uid, string $status, array $media_ids = [], string $in_reply_to_tweet_id = ''): string
248 {
249         $parameters = ['text' => $status];
250         if (!empty($media_ids)) {
251                 $parameters['media'] = ['media_ids' => $media_ids];
252         }
253         if (!empty($in_reply_to_tweet_id)) {
254                 $parameters['reply'] = ['in_reply_to_tweet_id' => $in_reply_to_tweet_id];
255         }
256
257         $response = twitter_post($uid, 'https://api.twitter.com/2/tweets', 'json', $parameters);
258
259         return $response->data->id;
260 }
261
262 function twitter_upload_image(int $uid, array $image)
263 {
264         if (!empty($image['id'])) {
265                 $photo = Photo::selectFirst(['resource-id'], ['id' => $image['id']]);
266                 $photo = Photo::selectFirst([], ["`resource-id` = ? AND `scale` > ?", $photo['resource-id'], 0], ['order' => ['scale']]);
267         } else {
268                 $photo = Photo::createPhotoForExternalResource($image['url']);
269         }
270
271         $picturedata = Photo::getImageForPhoto($photo);
272
273         Logger::info('Uploading', ['uid' => $uid, 'size' => strlen($picturedata), 'image' => $image]);
274         $media = twitter_post($uid, 'https://upload.twitter.com/1.1/media/upload.json', 'form_params', ['media' => base64_encode($picturedata)]);
275
276         if (isset($media->media_id_string)) {
277                 $media_id = $media->media_id_string;
278
279                 if (!empty($image['description'])) {
280                         $data = [
281                                 'media_id' => $media->media_id_string,
282                                 'alt_text' => [
283                                         'text' => substr($image['description'], 0, 1000)
284                                 ]
285                         ];
286                         $ret = twitter_post($uid, 'https://upload.twitter.com/1.1/media/metadata/create.json', 'json', $data);
287                         Logger::info('Metadata create', ['uid' => $uid, 'data' => $data, 'return' => $ret]);
288                 }
289         } else {
290                 Logger::error('Failed upload', ['uid' => $uid, 'size' => strlen($picturedata), 'image' => $image['url'], 'return' => $media]);
291                 throw new Exception('Failed upload of ' . $image['url']);
292         }
293
294         return $media_id;
295 }
296
297 function twitter_post(int $uid, string $url, string $type, array $data): stdClass
298 {
299         $stack = HandlerStack::create();
300         
301         $middleware = new Oauth1([
302                 'consumer_key'    => DI::pConfig()->get($uid, 'twitter', 'api_key'),
303                 'consumer_secret' => DI::pConfig()->get($uid, 'twitter', 'api_secret'),
304                 'token'           => DI::pConfig()->get($uid, 'twitter', 'access_token'),
305                 'token_secret'    => DI::pConfig()->get($uid, 'twitter', 'access_secret'),
306         ]);
307
308         $stack->push($middleware);
309         
310         $client = new Client([
311                 'handler' => $stack
312         ]);
313
314         $response = $client->post($url, ['auth' => 'oauth', $type => $data]);
315
316         $content = json_decode($response->getBody()->getContents()) ?? new stdClass;
317         Logger::debug('Success', ['content' => $content]);
318         return $content;
319 }