TwitterService.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. <?php
  2. namespace App\Services\Smm;
  3. use App\Services\Contracts\SmmPlatformInterface;
  4. use Abraham\TwitterOAuth\TwitterOAuth;
  5. use Carbon\Carbon;
  6. use CURLFile;
  7. use Illuminate\Http\Request;
  8. class TwitterService implements SmmPlatformInterface
  9. {
  10. protected $configData;
  11. protected $twitterOAuth;
  12. protected $consumerKey;
  13. protected $consumerSecret;
  14. protected $oauthCallbackUrl;
  15. public function __construct($configData)
  16. {
  17. $this->configData = $configData;
  18. $this->consumerKey = 'ahCsl1rD6ml2Ofma5UKUaMveJ';
  19. $this->consumerSecret = 'CsS5AHR1vIVuuhWHrbV8RR9DrF6JvZjjMOIt7oA9YubpxllDWD';
  20. $this->oauthCallbackUrl = env('DIST_SITE_URL') . '/dist/callback/twitter';
  21. if (isset($configData['accountInfo'])) {
  22. //初始化1.1版本的配置信息
  23. $access_token = $configData['accountInfo']['access_token'];
  24. $backup_field1 = json_decode($configData['accountInfo']['backup_field1'],true);
  25. $access_token_secret = $backup_field1['accessTokenSecret'];
  26. if (!empty($access_token) && !empty($access_token_secret)) {
  27. $this->twitterOAuth = new TwitterOAuth(
  28. $this->consumerKey,
  29. $this->consumerSecret,
  30. $access_token,
  31. $access_token_secret
  32. );
  33. }
  34. }
  35. }
  36. public function login()
  37. {
  38. $connection = new TwitterOAuth($this->consumerKey, $this->consumerSecret);
  39. $requestToken = $connection->oauth('oauth/request_token', ['oauth_callback' => $this->oauthCallbackUrl]);
  40. session(['twitter_oauth_token' => $requestToken['oauth_token']]);
  41. session(['twitter_oauth_token_secret' => $requestToken['oauth_token_secret']]);
  42. $url = $connection->url('oauth/authorize', ['oauth_token' => $requestToken['oauth_token']]);
  43. return [
  44. 'status' => true,
  45. 'data' => ['url' => $url]
  46. ];
  47. }
  48. public function loginCallback(Request $request)
  49. {
  50. $oauthToken = $request->get('oauth_token');
  51. $oauthVerifier = $request->get('oauth_verifier');
  52. $requestToken = session('twitter_oauth_token');
  53. $requestTokenSecret = session('twitter_oauth_token_secret');
  54. $connection = new TwitterOAuth(
  55. $this->consumerKey,
  56. $this->consumerSecret,
  57. $requestToken,
  58. $requestTokenSecret
  59. );
  60. $accessToken = $connection->oauth('oauth/access_token', [
  61. 'oauth_verifier' => $oauthVerifier
  62. ]);
  63. $expiresAt = Carbon::now()->addDays(90)->format('Y-m-d H:i:s');
  64. return [
  65. 'status' => true,
  66. 'data' => [
  67. 'accessToken' => $accessToken['oauth_token'],
  68. 'backupField1' => json_encode(['accessTokenSecret'=>$accessToken['oauth_token_secret']]),
  69. 'userId' => $accessToken['user_id'],
  70. 'userName' => $accessToken['screen_name'],
  71. 'accessToken_expiresAt' => $expiresAt,
  72. ]
  73. ];
  74. }
  75. public function postImage($message, $imagePaths, $accessToken = null)
  76. {
  77. try {
  78. $this->twitterOAuth->setApiVersion('1.1'); // 强制使用 v1.1 API
  79. $this->twitterOAuth->setTimeouts(10, 30);
  80. $mediaIds = [];
  81. foreach ($imagePaths as $imagePath) {
  82. $imagePath = toStoragePath($imagePath);
  83. if (!file_exists($imagePath)) {
  84. throw new \Exception("图片不存在: {$imagePath}");
  85. }
  86. //1.1版本上传媒体文件
  87. $uploadedMedia = $this->twitterOAuth->upload('media/upload', [
  88. 'media' => $imagePath
  89. ]);
  90. if (isset($uploadedMedia->error)) {
  91. throw new \Exception('媒体上传失败: ' . json_encode($uploadedMedia));
  92. }
  93. $mediaIds[] = $uploadedMedia->media_id_string;
  94. }
  95. // 2.0版本发布推文
  96. $this->twitterOAuth->setApiVersion('2');
  97. $tweet = $this->twitterOAuth->post('tweets', [
  98. 'text' => $message,
  99. 'media' => !empty($mediaIds) ? ['media_ids' => $mediaIds] : null
  100. ]);
  101. if (isset($tweet->errors)) {
  102. throw new \Exception('推文发布失败: ' . json_encode($tweet->errors));
  103. }
  104. return ['status' => true,
  105. 'data' => [
  106. 'responseIds' => [$tweet->data->id],
  107. ]];
  108. } catch (\Exception $e) {
  109. return ['status' => false, 'data' => $e->getMessage()];
  110. }
  111. }
  112. public function postVideo($message, $videoPath, $accessToken = null)
  113. {
  114. try {
  115. $this->twitterOAuth->setTimeouts(10, 30);
  116. //转为本地路径
  117. $videoPath = toStoragePath($videoPath);
  118. if (!file_exists($videoPath)) {
  119. throw new \Exception("视频文件不存在: {$videoPath}");
  120. }
  121. // 获取视频 MIME 类型
  122. $mimeType = mime_content_type($videoPath);
  123. if (strpos($mimeType, 'video/') !== 0) {
  124. throw new \Exception("文件不是有效的视频文件。MIME 类型: " . $mimeType);
  125. }
  126. // 获取视频文件大小 (以字节为单位)
  127. $fileSize = filesize($videoPath);
  128. // 1.1版本上传媒体文件 - 初始化上传
  129. $initParams = [
  130. 'command' => 'INIT',
  131. 'media_type' => $mimeType,
  132. 'media_category' => 'tweet_video',
  133. 'total_bytes' => $fileSize,
  134. ];
  135. $uploadedInit = $this->twitterOAuth->upload('media/upload', $initParams, true);
  136. if (!isset($uploadedInit->media_id_string)) {
  137. throw new \Exception('视频上传初始化失败: ' . json_encode($uploadedInit));
  138. }
  139. $mediaId = $uploadedInit->media_id_string;
  140. // 分段上传视频
  141. $fileHandle = fopen($videoPath, 'r');
  142. $segmentId = 0;
  143. while (!feof($fileHandle)) {
  144. $chunk = fread($fileHandle, 1024 * 1024); // 每次上传 1MB
  145. $segmentParams = [
  146. 'command' => 'APPEND',
  147. 'media_id' => $mediaId,
  148. 'segment_index' => $segmentId,
  149. 'media' => $chunk,
  150. ];
  151. $uploadedSegment = $this->twitterOAuth->upload('media/upload', $segmentParams, true);
  152. if (!empty($uploadedSegment->error)) {
  153. fclose($fileHandle);
  154. throw new \Exception('视频分段上传失败: ' . json_encode($uploadedSegment));
  155. }
  156. $segmentId++;
  157. }
  158. fclose($fileHandle);
  159. // 1.1版本上传媒体文件 - 完成上传
  160. $finalizeParams = [
  161. 'command' => 'FINALIZE',
  162. 'media_id' => $mediaId,
  163. ];
  164. $uploadedFinalize = $this->twitterOAuth->upload('media/upload', $finalizeParams, true);
  165. if (isset($uploadedFinalize->processing_info)) {
  166. $processingInfo = $uploadedFinalize->processing_info;
  167. $checkStatusSecs = $processingInfo->check_after_secs;
  168. $status = $processingInfo->state;
  169. while ($status !== 'succeeded' && $status !== 'failed') {
  170. sleep($checkStatusSecs);
  171. $statusParams = [
  172. 'command' => 'STATUS',
  173. 'media_id' => $mediaId,
  174. ];
  175. $uploadedStatus = $this->twitterOAuth->upload('media/upload', $statusParams, true);
  176. if (!empty($uploadedStatus->error)) {
  177. throw new \Exception('获取视频上传状态失败: ' . json_encode($uploadedStatus));
  178. }
  179. $processingInfo = $uploadedStatus->processing_info;
  180. $checkStatusSecs = $processingInfo->check_after_secs;
  181. $status = $processingInfo->state;
  182. if ($status === 'failed') {
  183. throw new \Exception('视频上传失败,Twitter 处理失败: ' . json_encode($uploadedStatus));
  184. }
  185. }
  186. } elseif (isset($uploadedFinalize->error)) {
  187. throw new \Exception('视频上传完成步骤失败: ' . json_encode($uploadedFinalize));
  188. }
  189. // 2.0版本发布推文
  190. $this->twitterOAuth->setApiVersion('2');
  191. $tweet = $this->twitterOAuth->post('tweets', [
  192. 'text' => $message,
  193. 'media' => ['media_ids' => [$mediaId]],
  194. ]);
  195. if (isset($tweet->errors)) {
  196. throw new \Exception('推文发布失败: ' . json_encode($tweet->errors));
  197. }
  198. return ['status' => true,
  199. 'data' => [
  200. 'responseIds' => [$tweet->data->id],
  201. ]
  202. ];
  203. } catch (\Exception $e) {
  204. return ['status' => false, 'data' => $e->getMessage()];
  205. }
  206. }
  207. // 保持空实现
  208. public function getComments($postId) {}
  209. public function replyToComment($commentId) {}
  210. public function deleteComment($commentId) {}
  211. }