Publishing posts on X (Twitter) programmatically using Node (Part 2)

This is a 2 part tutorial which explains the setup and publishing of posts on X(Twitter) programmatically using Node and Angular. The frontend can be switched with any other SPA framework like React, Vue, Next. This 2 part series will cover

Part 1: Create a twitter admin application in developer.twitter.com

Part 2: Publish posts using X V2 API

By the end of this tutorial you will be able to successfully setup publishing of posts using X V2 API

In order to post a tweet with media, the media should be uploaded separately using X Media Upload API and generate the media id. The media id should then be used along with the tweet content in the publish API to create tweet in X

Following are the steps to publish post to X. X accepts any image, video or gif types to be uploaded.

We will be using the twitter library. The following code shows the steps to install the twitter library and initialize the twitter client.

                     npm i twitter
                     var Twitter = require('twitter')
                     var twitterClient = new Twitter({
                         consumer_key: //twitter client key,
                         consumer_secret: //twitter client secret,
                         access_token_key: //OAuth1 token,
                         access_token_secret: //OAuth1 token secret
                     });

The process to upload Media is a three step process. The first step is to initialize the media upload

                    const fs = require("fs");
                    const mime = require("mime");
		    fs.readFile(mediaPath, async function (err, data) {
                    let stats = fs.statSync(mediaPath);
                    let mimeType = mime.getType(mediaPath);
                    let fileSizeInBytes = stats.size;
                    let params = {
                      command: "INIT",
                      total_bytes: fileSizeInBytes,
                      media_type: mimeType,
                    };
                    fs.stat(mediaPath, function (err, stats) {
                      if (err) {
                        return console.error(err);
                      }
                      fs.unlink(mediaPath, function (err) {
                        if (err) return console.log(err);
                      });
                    });
                    twitterClient.post(
                      "media/upload",
                      params,
                      async (error, data, response) => {
                        if (error) {
                          console(error);
                        } else {
                          let mediaId = data.media_id_string // store this media id to be used in subsequent steps 
                        }
                      }
                    );
                  });

Second step is to upload the media to the media id received in the first step

              let chunkSize = 4 * 1024 * 1024;
              let buffer = Buffer.alloc(chunkSize);
              let segmentIndex = 0;
              let mediaPath = path.normalize(
                path.join(__dirname, "../../../tmp/", mediaUrl)
              );
                fs.open(mediaPath, "r", function (err, fd) {
                  if (err) reject(err);
                  function readChunk() {
                    fs.read(
                      fd,
                      buffer,
                      0,
                      chunkSize,
                      null,
                      function (err, nread) {
                        if (err) reject(err);
                        if (nread === 0) {
                          // done reading file, delete the file
                            fs.stat(mediaPath, function (err, stats) {
                              if (err) {
                                return console.error(err);
                              }
                              fs.unlink(mediaPath, function (err) {
                                if (err) return console.log(err);
                              });
                            });
                            return;

                        } else {
                          let data;
                          if (nread < chunkSize) data = buffer.slice(0, nread);
                          else data = buffer;
                          let params = {
                            command: "APPEND",
                            media_id: mediaId,
                            media: data,
                            segment_index: segmentIndex,
                          };
                          twitterClient.post(
                            "media/upload",
                            params,
                            async (error, data, response) => {
                              if (error) {
                                reject(error);
                              } else {
                                segmentIndex = segmentIndex + 1;
                                readChunk();
                              }
                            }
                          );
                        }
                      }
                    );
                  }
                  readChunk();
                });

The last step in file upload is to notify that file upload is complete by calling the below API

              let params = {
                command: "FINALIZE",
                media_id: mediaId,
              };
              twitterClient.post(
                "media/upload",
                params,
                async (error, data, response) => {
                  if (error) {
                    console.log(error);
                  } else {
                    console.log('success')
                  }
                }
              );

Now that file upload is complete, next step is to call the publish API to post the content

	const crypto = require("crypto");
	const OAuth = require("oauth-1.0a");

	function twitterNonce() {
	  return crypto.randomBytes(16).toString("hex");
	}

	function twitterSign(baseStr, key) {
	  return crypto.createHmac("sha1", key).update(baseStr).digest("base64");
	}

	function twitterPercentEncode(str) {
	  const notEscapedRe = /[!'()*]/g;
	  return encodeURIComponent(str).replace(
		notEscapedRe,
		(c) => `%${c.charCodeAt(0).toString(16)}`
	  );
	}

	function twitterMakeObjStr(parameters, quote = '"', split = ",") {
	  const ordered = Object.fromEntries(Object.entries(parameters).sort());
	  return Object.entries(ordered)
		.map(
		  ([key, value]) =>
			`${twitterPercentEncode(key)}=${quote}${twitterPercentEncode(value)}${quote}`
		)
		.join(split);
	}

	function authHeader(parameters) {
	  return { Authorization: `OAuth ${twitterMakeObjStr(parameters)}` };
	}

	function makeHeader(consumer, token, request) {
	  let oauthData = {};
		oauthData = {
		  oauth_consumer_key: consumer.key,
		  oauth_token: token.key,
		  oauth_nonce: twitterNonce(),
		  oauth_signature_method: "HMAC-SHA1",
		  oauth_timestamp: Math.floor(Date.now() / 1000),
		  oauth_version: "1.0",
		};


	  const baseStr = [
		request.method.toUpperCase(),
		twitterPercentEncode(request.url),
		twitterPercentEncode(twitterMakeObjStr({ ...oauthData }, "", "&")),
	  ].join("&");
	  const signingKey = [
		twitterPercentEncode(consumer.secret),
		twitterPercentEncode(token.secret),
	  ].join("&");
	  return authHeader({
		...oauthData,
		oauth_signature: twitterSign(baseStr, signingKey),
	  });
	}     
	let twitterUrl = "https://api.twitter.com/2/tweets";
	let payload = {};
	const request_data = {
		url: "https://api.twitter.com/2/tweets",
		method: "POST",
		};
		payload["text"] = mediaContent
		payload["media"] = {
			media_ids: [mediaId],
		};
	let configHeader = {
		"Content-Type": "application/json",
		headers: makeHeader(
			{ key: twitterApiKey, secret: twitterApiSecret },
			{ key: twitterToken, secret: twitterAccessTokenSecret },
			request_data
			),
	};
	let twitterResponse = await axios.post(
					twitterUrl,
					payload,
					configHeader
				  );

Leave a Reply