Skip to content

Commit d4d23ef

Browse files
authored
Merge pull request #7 from janwebdev/3.2.5
updating Twitter provider and config
2 parents ccefa5b + b610d44 commit d4d23ef

File tree

7 files changed

+47
-36
lines changed

7 files changed

+47
-36
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ social_post:
3434
providers:
3535
twitter:
3636
enabled: true
37-
api_key: "%env(TWITTER_API_KEY)%"
38-
api_secret: "%env(TWITTER_API_SECRET)%"
37+
consumer_key: "%env(TWITTER_CONSUMER_KEY)%"
38+
consumer_secret: "%env(TWITTER_CONSUMER_SECRET)%"
3939
access_token: "%env(TWITTER_ACCESS_TOKEN)%"
4040
access_token_secret: "%env(TWITTER_ACCESS_TOKEN_SECRET)%"
4141

@@ -89,8 +89,8 @@ Add to your `.env`:
8989

9090
```env
9191
# Twitter (X) API v2
92-
TWITTER_API_KEY=your_api_key
93-
TWITTER_API_SECRET=your_api_secret
92+
TWITTER_CONSUMER_KEY=your_consumer_key
93+
TWITTER_CONSUMER_SECRET=your_consumer_secret
9494
TWITTER_ACCESS_TOKEN=your_access_token
9595
TWITTER_ACCESS_TOKEN_SECRET=your_access_token_secret
9696

config/services.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ services:
1818
Janwebdev\SocialPostBundle\Provider\Twitter\TwitterClient:
1919
arguments:
2020
$httpClient: '@Janwebdev\SocialPostBundle\Http\ClientInterface'
21-
$apiKey: "%social_post.twitter.api_key%"
22-
$apiSecret: "%social_post.twitter.api_secret%"
21+
$consumerKey: "%social_post.twitter.consumer_key%"
22+
$consumerSecret: "%social_post.twitter.consumer_secret%"
2323
$accessToken: "%social_post.twitter.access_token%"
2424
$accessTokenSecret: "%social_post.twitter.access_token_secret%"
2525

src/DependencyInjection/Configuration.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ public function getConfigTreeBuilder(): TreeBuilder
2727
->arrayNode('twitter')
2828
->canBeEnabled()
2929
->children()
30-
->scalarNode('api_key')->defaultValue('')->end()
31-
->scalarNode('api_secret')->defaultValue('')->end()
30+
->scalarNode('consumer_key')->defaultValue('')->end()
31+
->scalarNode('consumer_secret')->defaultValue('')->end()
3232
->scalarNode('access_token')->defaultValue('')->end()
3333
->scalarNode('access_token_secret')->defaultValue('')->end()
3434
->end()

src/DependencyInjection/SocialPostExtension.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ class SocialPostExtension extends Extension
2525
*/
2626
private const PROVIDER_DEFAULTS = [
2727
'twitter' => [
28-
'api_key' => '',
29-
'api_secret' => '',
28+
'consumer_key' => '',
29+
'consumer_secret' => '',
3030
'access_token' => '',
3131
'access_token_secret' => '',
3232
],

src/Provider/Twitter/TwitterClient.php

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
/**
1212
* Twitter API v2 client with OAuth 1.0a authentication.
1313
*
14-
* @since 3.0.0
14+
* OAuth 1.0a requires four credentials from the Twitter Developer Portal
15+
* (Projects & Apps → Your App → "Keys and Tokens"):
16+
* - Consumer Keys section: API Key ($consumerKey) + API Key Secret ($consumerSecret)
17+
* - Authentication Tokens section: Access Token ($accessToken) + Access Token Secret ($accessTokenSecret)
18+
*
19+
* Note: OAuth 2.0 Client ID / Client Secret are separate and NOT used here.
20+
*
21+
* @since 3.2.1
1522
* @license https://opensource.org/licenses/MIT
1623
*/
1724
readonly class TwitterClient
@@ -20,17 +27,17 @@
2027

2128
public function __construct(
2229
private ClientInterface $httpClient,
23-
private string $apiKey,
24-
private string $apiSecret,
30+
private string $consumerKey,
31+
private string $consumerSecret,
2532
private string $accessToken,
2633
private string $accessTokenSecret,
2734
) {
2835
}
2936

3037
public function isConfigured(): bool
3138
{
32-
return !empty($this->apiKey)
33-
&& !empty($this->apiSecret)
39+
return !empty($this->consumerKey)
40+
&& !empty($this->consumerSecret)
3441
&& !empty($this->accessToken)
3542
&& !empty($this->accessTokenSecret);
3643
}
@@ -91,21 +98,24 @@ private function uploadSingleMedia(AttachmentInterface $attachment): ?string
9198
if (!file_exists($filePath)) {
9299
throw new ProviderException("File not found: {$filePath}");
93100
}
94-
$fileContent = file_get_contents($filePath);
95-
} else {
96-
$fileContent = file_get_contents($filePath);
97101
}
98102

99-
if ($fileContent === false) {
100-
throw new ProviderException("Failed to read file: {$filePath}");
103+
// Must pass a resource (not string) so Symfony HttpClient uses multipart/form-data
104+
$fileHandle = fopen($filePath, 'rb');
105+
if ($fileHandle === false) {
106+
throw new ProviderException("Failed to open file: {$filePath}");
101107
}
102108

103109
$headers = $this->getAuthHeaders('POST', $url);
104110

105-
$response = $this->httpClient->postMultipart($url, $headers, [
106-
'media' => $fileContent,
107-
'media_category' => 'tweet_image',
108-
]);
111+
try {
112+
$response = $this->httpClient->postMultipart($url, $headers, [
113+
'media' => $fileHandle,
114+
'media_category' => 'tweet_image',
115+
]);
116+
} finally {
117+
fclose($fileHandle);
118+
}
109119

110120
if (!$response->isSuccessful()) {
111121
throw new ProviderException("Failed to upload media: {$response->getBody()}");
@@ -125,23 +135,24 @@ private function uploadSingleMedia(AttachmentInterface $attachment): ?string
125135
private function getAuthHeaders(string $method, string $url, array $data = []): array
126136
{
127137
$oauthParams = [
128-
'oauth_consumer_key' => $this->apiKey,
138+
'oauth_consumer_key' => $this->consumerKey,
129139
'oauth_nonce' => bin2hex(random_bytes(16)),
130140
'oauth_signature_method' => 'HMAC-SHA1',
131141
'oauth_timestamp' => (string) time(),
132142
'oauth_token' => $this->accessToken,
133143
'oauth_version' => '1.0',
134144
];
135145

136-
// Create signature base string
146+
// Create signature base string.
147+
// JSON POST body is NOT included per OAuth 1.0a spec (only form-urlencoded bodies are).
137148
$params = array_merge($oauthParams, $method === 'GET' ? $data : []);
138149
ksort($params);
139-
150+
140151
$paramString = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
141152
$baseString = strtoupper($method) . '&' . rawurlencode($url) . '&' . rawurlencode($paramString);
142153

143-
// Generate signature
144-
$signingKey = rawurlencode($this->apiSecret) . '&' . rawurlencode($this->accessTokenSecret);
154+
// Signing key = percent-encode(consumerSecret) + '&' + percent-encode(accessTokenSecret)
155+
$signingKey = rawurlencode($this->consumerSecret) . '&' . rawurlencode($this->accessTokenSecret);
145156
$signature = base64_encode(hash_hmac('sha1', $baseString, $signingKey, true));
146157

147158
$oauthParams['oauth_signature'] = $signature;

tests/Integration/DependencyInjection/ConfigurationTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ public function testTwitterConfiguration(): void
2828
'providers' => [
2929
'twitter' => [
3030
'enabled' => true,
31-
'api_key' => 'test_key',
32-
'api_secret' => 'test_secret',
31+
'consumer_key' => 'test_key',
32+
'consumer_secret' => 'test_secret',
3333
'access_token' => 'test_token',
3434
'access_token_secret' => 'test_token_secret',
3535
],
@@ -39,7 +39,7 @@ public function testTwitterConfiguration(): void
3939
$processedConfig = $this->processor->processConfiguration($this->configuration, [$config]);
4040

4141
$this->assertTrue($processedConfig['providers']['twitter']['enabled']);
42-
$this->assertEquals('test_key', $processedConfig['providers']['twitter']['api_key']);
42+
$this->assertEquals('test_key', $processedConfig['providers']['twitter']['consumer_key']);
4343
}
4444

4545
public function testFacebookConfiguration(): void
@@ -119,7 +119,7 @@ public function testAllProvidersConfiguration(): void
119119
{
120120
$config = [
121121
'providers' => [
122-
'twitter' => ['enabled' => true, 'api_key' => 'key1', 'api_secret' => 's1', 'access_token' => 't1', 'access_token_secret' => 'ts1'],
122+
'twitter' => ['enabled' => true, 'consumer_key' => 'key1', 'consumer_secret' => 's1', 'access_token' => 't1', 'access_token_secret' => 'ts1'],
123123
'facebook' => ['enabled' => true, 'page_id' => 'p1', 'access_token' => 't1'],
124124
'linkedin' => ['enabled' => true, 'organization_id' => 'org1', 'access_token' => 't1'],
125125
'telegram' => ['enabled' => true, 'bot_token' => 'bot1', 'channel_id' => 'ch1'],

tests/Unit/Provider/Twitter/TwitterClientTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ protected function setUp(): void
2121
$this->httpClientMock = $this->createMock(ClientInterface::class);
2222
$this->client = new TwitterClient(
2323
$this->httpClientMock,
24-
'api_key',
25-
'api_secret',
24+
'consumer_key',
25+
'consumer_secret',
2626
'access_token',
2727
'access_token_secret',
2828
);
@@ -87,7 +87,7 @@ public function testUploadMediaSkipsNonImageAttachments(): void
8787
$this->assertEquals([], $result);
8888
}
8989

90-
public function testUploadMediaPropagatesExceptionOnFailure(): void
90+
public function testUploadMediaThrowsIfLocalFileNotFound(): void
9191
{
9292
$attachment = $this->createMock(\Janwebdev\SocialPostBundle\Message\Attachment\AttachmentInterface::class);
9393
$attachment->method('getType')->willReturn('image');

0 commit comments

Comments
 (0)