CurateBot Devlog 10: Scheduled Firebase function to send tweets
Tags: devjournal, showdev, javascript, typescript
Published: 2021-01-08 18:53:09

The only backend we need is a cloud function that runs every hour, checking for tweets to be scanned. The commit is [here](https://github.com/meseta/curatebot/tree/f43549c1494690066472f3a3b9911e7ffcb0f367), and the main change is in the [`functions/src/index.ts` file](https://github.com/meseta/curatebot/blob/f43549c1494690066472f3a3b9911e7ffcb0f367/functions/src/index.ts)

# Firebase Functions
Firebase functions are serverless functions - you provide firebase CLI with your javascript function, and it does the work to upload it, and run it in an environment that you never have to maintain or set up.

These functions run on triggers, which can be things like HTTP triggers, triggers on changes to the database, or via Pubsub. Firebase also provides integration with other services via Pubsub, one of them is the [Cloud Scheduler service](https://cloud.google.com/scheduler), which you can think of as a managed `cron` service that can post to pubsub.

When we specify to firebase to use a `pubsub.schedule()` trigger, it's fundamentally just a pubsub function trigger, but the CLI sets up a schedule on the Cloud Scheduler services for us.

```ts

export const tweetScan = functions.runWith(runtimeOpts).pubsub.schedule('every 1 hours').onRun(async (context) => {
  const currentHour = new Date().getUTCHours();
  const currentDay = new Date().getUTCDay();
  const currentIdx = currentDay*24+currentHour;

  logger.info("Starting scan", {currentHour, currentDay, currentIdx});

  await firestore.collection('users').where('isActive', '==', true)
  .where('scheduleEnabled', 'array-contains', currentIdx.toString()).get()
  .then((query: admin.firestore.QuerySnapshot) => {

    const promises: Array<Promise<any>> = [];
    query.forEach((doc: admin.firestore.DocumentSnapshot ) => {
      promises.push(processUser(doc));
    })

    return Promise.all(promises);
  })
  .then(() => {
    return firestore.collection('system').doc('stats').update({
      lastRun: admin.firestore.FieldValue.serverTimestamp(),
    })
  })
  .then(() => {
    logger.info("Done scan");
  })
  .catch((err: Error) => {
    logger.error(err);
  })
});
```

All this script does is calculate a hour index, which matches the schedule that you can set on the frontend, and then checks if there are any active users who have that timeslot in their schedule. Running the `processUser()` function for each one.

Some system stats are updated in the process.

# Processing a user
For each user who has that time slot, we fetch the most recent queued tweet, and post it to twitter using their stored API keys! Then we delete the tweet from their account.

```ts
async function processUser(doc: admin.firestore.DocumentSnapshot): Promise<any> {
  const uid = doc.id;
  const userKey = doc.get('accessToken');
  const userSecret = doc.get('secret');

  return doc.ref.collection('tweets').where('queued', '==', true).orderBy('added').limit(1).get()
  .then((query: admin.firestore.QuerySnapshot) => {
    if (query.size) {
      const tweetDoc = query.docs[0];
      const tweetText = tweetDoc.get('tweet');

      logger.info("Got tweet for user", {uid, tweetText});

      if (tweetText) {
        const client = new Twitter({
          consumer_key: apiKey,
          consumer_secret: apiSecret,
          access_token_key: userKey,
          access_token_secret: userSecret,
        });
        
        return client.post('statuses/update', {status: tweetText})
        .then(tweet => {
          logger.info("Tweet sent!", {tweet});
          return firestore.collection('system').doc('stats').update({
            tweetsSent: admin.firestore.FieldValue.increment(1),
          })
        })
        .then(() => {
          return tweetDoc.ref.delete();
        })

      }
      return tweetDoc.ref.delete();
    }
  
    logger.info("No more scheduled tweets for user", {uid});
    return doc.ref.update({
      isActive: false,
    });
  })
}
```

The tweet is sent using the NPM [twitter](https://www.npmjs.com/package/twitter) module, which requires several keys, the first pair of keys (consumer key/secret) is our Bot's API key, which we got when registering earlier. These are set up in Firebase Function's config space, using the CLI command:

```
firebase functions:config:set twitter.api_key="***" twitter.api_secret="***"
```
The second pair of keys (access token key/secret) is the keys provided by the user when they logged in, which allows us to post on their account.

# Deploying
Firebase takes care of scheduling this function, so we don't have to worry about the backend to achieve that. In fact, the first time deploying a schedule function, Firebase CLI goes through the process of enabling the APIs that are needed, and also prompting you to upgrade your billing as schedules cost $0.10 per month each.

![Cloud functions deployment](https://dev-to-uploads.s3.amazonaws.com/i/vxttgavo01wp4x7zipqq.png)
 
When we take a peek at Google Cloud's management console for Cloud Scheduler, we see a new entry added (we can also trigger this function from here manually, if we need to, useful for testing)

![Cloud scheduler](https://dev-to-uploads.s3.amazonaws.com/i/j02j7qcives2ty6n6u8y.png)

___
At this stage, CurateBot is feature complete! The service is now able to load tweets in bulk, allow the user to curate them, select a schedule, and allow tweets to be posted on that schedule! Everything needed for an AI Bot account on Twitter, there's one more post about doing a UI overhaul, but we're otherwise fully functional.