Issues (45)

src/services/Api.php (6 issues)

1
<?php
2
/**
3
 * @link      https://dukt.net/twitter/
4
 * @copyright Copyright (c) Dukt
5
 * @license   https://github.com/dukt/twitter/blob/master/LICENSE.md
6
 */
7
8
namespace dukt\twitter\services;
9
10
use Craft;
11
use craft\helpers\FileHelper;
12
use dukt\twitter\helpers\TwitterHelper;
13
use dukt\twitter\models\Tweet;
14
use dukt\twitter\Plugin;
15
use League\OAuth1\Client\Credentials\TokenCredentials;
16
use yii\base\Component;
17
use GuzzleHttp\Client;
18
use GuzzleHttp\HandlerStack;
19
use GuzzleHttp\Subscriber\Oauth\Oauth1;
20
use DateTime;
21
use GuzzleHttp\Exception\GuzzleException;
22
23
/**
24
 * Api Service
25
 *
26
 * @author Dukt <[email protected]>
27
 * @since  3.0
28
 */
29
class Api extends Component
30
{
31
    // Public Methods
32
    // =========================================================================
33
34
    /**
35
     * Performs a get request on the Twitter API.
36
     *
37
     * @param string     $uri
38
     * @param array|null $query
39
     * @param array|null $headers
40
     * @param array      $options
41
     * @param bool|null  $enableCache
42
     * @param null|int   $cacheExpire
43
     *
44
     * @return array|null
45
     * @throws GuzzleException
46
     */
47
    public function get($uri, array $query = null, array $headers = null, array $options = [], $enableCache = null, $cacheExpire = null)
48
    {
49
        // Add query to the request’s options
50
51
        if ($query) {
52
            $options['query'] = $query;
53
        }
54
55
        // Enable Cache
56
57
        if (null === $enableCache) {
58
            $enableCache = Plugin::getInstance()->getSettings()->enableCache;
59
        }
60
61
62
        // Try to get response from cache
63
64
        if ($enableCache) {
65
            $response = Plugin::getInstance()->getCache()->get([$uri, $headers, $options]);
66
67
            if ($response) {
68
                return $response;
69
            }
70
        }
71
72
73
        // Otherwise request the API
74
75
        $client = $this->getClient();
76
77
        $url = $uri.'.json';
78
79
        if ($headers) {
80
            $options['headers'] = $headers;
81
        }
82
83
        $response = $client->request('GET', $url, $options);
84
85
        $jsonResponse = json_decode($response->getBody(), true);
86
87
        if ($enableCache) {
88
            Plugin::getInstance()->getCache()->set([$uri, $headers, $options], $jsonResponse, $cacheExpire);
0 ignored issues
show
It seems like $cacheExpire can also be of type integer; however, parameter $expire of dukt\twitter\services\Cache::set() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
            Plugin::getInstance()->getCache()->set([$uri, $headers, $options], $jsonResponse, /** @scrutinizer ignore-type */ $cacheExpire);
Loading history...
89
        }
90
91
        return $jsonResponse;
92
    }
93
94
    /**
95
     * Returns a tweet by its URL or ID.
96
     *
97
     * @param string|int $urlOrId
98
     * @param array|null $query
99
     *
100
     * @return Tweet|null
101
     * @throws GuzzleException
102
     * @throws \yii\base\Exception
103
     */
104
    public function getTweet($urlOrId, array $query = null)
105
    {
106
        $tweetId = TwitterHelper::extractTweetId($urlOrId);
107
108
        if ($tweetId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tweetId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
109
            return $this->getTweetById($tweetId, $query);
110
        }
111
112
        return null;
113
    }
114
115
    /**
116
     * Returns a user by their ID.
117
     *
118
     * @param int|string $userId
119
     * @param array      $query
120
     *
121
     * @return array|null
122
     * @throws GuzzleException
123
     */
124
    public function getUserById($userId, $query = [])
125
    {
126
        $userId = (int)$userId;
127
128
        $query = array_merge($query, ['user_id' => $userId]);
129
130
        return $this->get('users/show', $query);
131
    }
132
133
    /**
134
     * Parses tweet data and returns a tweet model.
135
     *
136
     * @param array $data
137
     * @return Tweet
138
     */
139
    public function parseTweetData(array $data): Tweet
140
    {
141
        $tweet = new Tweet();
142
143
        $this->populateTweetFromData($tweet, $data);
144
145
        return $tweet;
146
    }
147
148
    /**
149
     * Populate tweet object from data array.
150
     *
151
     * @param Tweet $tweet
152
     * @param array $data
153
     *
154
     * @return Tweet
155
     */
156
    public function populateTweetFromData(Tweet $tweet, array $data): Tweet
157
    {
158
        if (isset($data['created_at'])) {
159
            $tweet->createdAt = DateTime::createFromFormat('D M d H:i:s O Y', $data['created_at']);
0 ignored issues
show
Documentation Bug introduced by
It seems like DateTime::createFromForm...', $data['created_at']) of type DateTime is incompatible with the declared type dukt\twitter\models\The of property $createdAt.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
160
        }
161
162
        $tweet->data = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data of type array is incompatible with the declared type dukt\twitter\models\Raw of property $data.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
163
        $tweet->text = $data['full_text'] ?? null;
164
        $tweet->remoteId = $data['id'] ?? null;
165
        $tweet->remoteUserId = $data['user']['id'] ?? null;
166
        $tweet->username = $data['user']['name'] ?? null;
167
        $tweet->userProfileRemoteImageSecureUrl = $data['user']['profile_image_url_https'] ?? null;
168
        $tweet->userProfileRemoteImageUrl = $data['user']['profile_image_url'] ?? null;
169
        $tweet->userScreenName = $data['user']['screen_name'] ?? null;
170
171
        return $tweet;
172
    }
173
174
    /**
175
     * Saves the original user profile image for a twitter user ID
176
     *
177
     * @param $userId
178
     * @param $remoteImageUrl
179
     *
180
     * @return string|null
181
     * @throws GuzzleException
182
     * @throws \yii\base\Exception
183
     */
184
    public function saveOriginalUserProfileImage($userId, $remoteImageUrl)
185
    {
186
        if (!$userId || !$remoteImageUrl) {
187
            return null;
188
        }
189
190
        $originalFolderPath = Craft::$app->path->getRuntimePath().'/twitter/userimages/'.$userId.'/original/';
0 ignored issues
show
The method getRuntimePath() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

190
        $originalFolderPath = Craft::$app->path->/** @scrutinizer ignore-call */ getRuntimePath().'/twitter/userimages/'.$userId.'/original/';

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
191
192
        if (!is_dir($originalFolderPath)) {
193
            FileHelper::createDirectory($originalFolderPath);
194
        }
195
196
        $files = FileHelper::findFiles($originalFolderPath);
197
198
        if (count($files) > 0) {
199
            $imagePath = $files[0];
200
201
            return $imagePath;
202
        }
203
204
        $remoteImageUrl = str_replace('_normal', '', $remoteImageUrl);
205
        $fileName = pathinfo($remoteImageUrl, PATHINFO_BASENAME);
206
        $imagePath = $originalFolderPath.$fileName;
0 ignored issues
show
Are you sure $fileName of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

206
        $imagePath = $originalFolderPath./** @scrutinizer ignore-type */ $fileName;
Loading history...
207
208
        $client = Craft::createGuzzleClient();
209
        $response = $client->request('GET', $remoteImageUrl, [
210
            'sink' => $imagePath
211
        ]);
212
213
        if (!$response->getStatusCode() != 200) {
214
            return null;
215
        }
216
217
        return $imagePath;
218
    }
219
220
    // Private Methods
221
    // =========================================================================
222
223
    /**
224
     * Returns the authenticated client.
225
     *
226
     * @return Client
227
     */
228
    private function getClient()
229
    {
230
        $options = [
231
            'base_uri' => 'https://api.twitter.com/1.1/'
232
        ];
233
234
        $token = Plugin::getInstance()->getOauth()->getToken();
235
236
        if ($token) {
237
            $stack = $this->getStack($token);
238
239
            $options['auth'] = 'oauth';
240
            $options['handler'] = $stack;
241
        }
242
243
        return Craft::createGuzzleClient($options);
244
    }
245
246
    /**
247
     * Get stack
248
     *
249
     * @param TokenCredentials $token
250
     *
251
     * @return HandlerStack
252
     */
253
    private function getStack(TokenCredentials $token)
254
    {
255
        $stack = HandlerStack::create();
256
257
        $oauthConsumerKey = Plugin::getInstance()->getConsumerKey();
258
        $oauthConsumerSecret = Plugin::getInstance()->getConsumerSecret();
259
260
        $middleware = new Oauth1([
261
            'consumer_key' => $oauthConsumerKey,
262
            'consumer_secret' => $oauthConsumerSecret,
263
            'token' => $token->getIdentifier(),
264
            'token_secret' => $token->getSecret(),
265
            'signature_method' => 'HMAC-SHA1'
266
        ]);
267
268
        $stack->push($middleware);
269
270
        return $stack;
271
    }
272
273
    /**
274
     * Returns a tweet by its ID.
275
     *
276
     * @param            $tweetId
277
     * @param array|null $query
278
     *
279
     * @return Tweet|null
280
     * @throws GuzzleException
281
     * @throws \yii\base\Exception
282
     */
283
    private function getTweetById($tweetId, array $query = null)
284
    {
285
        $tweetId = (int)$tweetId;
286
287
        if(!$query) {
288
            $query = [];
289
        }
290
291
        $query = array_merge($query, [
292
            'id' => $tweetId,
293
            'tweet_mode' => 'extended'
294
        ]);
295
296
        $data = $this->get('statuses/show', $query);
297
298
        if (!$data) {
299
            return null;
300
        }
301
302
        $tweet = new Tweet();
303
        $this->populateTweetFromData($tweet, $data);
304
305
        // generate user profile image
306
        $this->saveOriginalUserProfileImage($tweet->remoteUserId, $tweet->userProfileRemoteImageSecureUrl);
307
308
        return $tweet;
309
    }
310
}