Jun, Hyunje

Code, specifically for web

Programming languages

Some geeky hobbies

Recently I’ve developed a simple chatbot with which we can query information from Wolfram Alpha. The chatbot platform is the LINE messaging API, because why not. It works well like below:

Sonoda-san is giving some info about Love Live
Sonoda-san is giving some info about Love Live

Because Wolfram Alpha API returns GIF images, the bot just downloads them, converts them into JPEG, smushes the JPEGs into one, and send it back. I’d like to introduce some useful utility functions used in the process.

  • Download a file
  • Run a shell command
  • ImageMagick helpers

Download a file

I used axios for HTTP(S) client. The simplest way to download a file with axios may be just requesting its body as a buffer and write the buffer into a file. However, downloading and buffering a whole file is ineffective. We can do better, with streams.

Axios provides responseType:'stream' where we can get data as a Readable. Now the readable can be directly piped into a file stream. The result code is like below:

async function download(from: string, to: string) {
  const { data } = await axios({
    method: 'get',
    url: from,
    responseType:'stream',
  });

  return new Promise((resolve, reject) => {
    const writable = createWriteStream(to);
    data.pipe(writable);
    writable.on('close', resolve);
    writable.on('error', reject);
  });
}
await download('https://somewhere', 'somefile.png');

Beware that the result promise resolves on writable.on('close'), not on data.on('end'). End of reading data doesn’t mean end of writing file. If it resolves on the wrong timing, a file may be incomplete even after download() finishes.

Run a shell command

The simplest way to run a shell command in Node.js is execSync in the child process module. However, synchronous execution is always pricy in JavaScript. Rather than using execSync, it’s better to implement a handy wrapper for exec.

function exec(command: string): Promise<string> {
  return new Promise((
    resolve: (result: string) => void,
    reject,
  ) => {
    child_process.exec(command, (err, stdout) => {
      if (err) {
        reject(err);
      } else {
        resolve(stdout);
      }
    });
  });
}

The utility function above will run a command asynchronously, and returns a promise of stdout. Wrapping with promises is always better than using callbacks, as we can use async-await.

await exec(`rm ${gif}`);

ImageMagick helpers

With exec introduced above, creating an ImageMagick helpers is a piece of cake.

function convert(from: string, to: string) {
  return exec(`convert ${from} ${to}`);
}

function smush(images: string[], to: string) {
  const ls = images.join(' ');
  return exec(`convert ${ls} -smush 7 ${to}`);
}

function resize(from: string, size: string, to: string) {
  return exec(`convert -resize ${size} ${from} ${to}`);
}

Other utility functions wrapping CLI tools can easily be implemented in the same manner.

Conclusion

The actual source code of the Sonoda-san bot can be found in its GitHub repository.

Thanks for reading and happy coding in TypeScript!

read other posts