Post Tweets on Twitter when Records are Approved

Contents

Overview

This article introduces how to post tweets on Twitter automatically after Kintone Records are approved. AWS Lambda is used to facilitate the integration.

The general flow of the operation is as follows:

  1. An employee registers a tweet as well as the datetime to post the tweet in a Kintone record.
  2. A reviewer checks the tweet contents and approves or rejects it.
  3. If approved, the tweet is posted at the set datetime.

AWS Lambda is used to host the function that checks for ready tweets and posts them to Twitter at their defined times. It also updates the Kintone record by changing the status to Complete and inserting a link to the posted tweet in the record.

Sample Image

The general flow of the customization is as follows:

  1. When the set datetime passes, the Lambda function is triggered and retrieves the tweet data from Kintone.
  2. The data is tweeted on Twitter.
  3. The process management status of the Kintone record is updated. The record is also updated with the URL of the tweet.

The following diagram shows the general flow of the customization.

tips
Note

  1. This customization uses a Node.js v12.18.0 environment.
  2. To use the Twitter API, a Twitter developer account is needed. For details, refer to the Subscription Plan (External link) page of the Twitter developer website.

Prepare the Kintone App

Create the form

Create an App (External link) with the following field and settings. Save the form when finished.

Field Type Field Name Field Code Notes
Date and time Tweet date and time tweetDate
Text area Tweet content tweetBody
Attachment Image file imageFile
Link (URL) Tweet URL tweetUrl The URL of the tweet is automatically set in this field after the tweet is posted.

Generate an API token

Create an API token with view and edit permissions. Refer to the Generating API Tokens (External link) article on the Kintone Help site for more information.

Set up the process management settings

Open the App Settings tab and navigate to Process Management under General Settings.

Check the Enable process management check box to enable the process management feature.
Set Process Management settings (External link) with the following. Do not set a default assignee for the Tweet scheduled status. If set, the status cannot be updated with the API token.

  • Status Not started, In review, Tweet scheduled, Completed

  • Process list

    Status Assignee Action Next Status
    Not started Set to anybody Start In review
    In review -
    • OK
    • NG
    • Tweet shceduled
    • Not started
    Tweet scheduled - Complete Completed

Prepare the Twitter Settings

Prepare the Twitter account

Log in to Twitter if the necessary Twitter account already exists. If not, create a new Twitter account from Twitter.com (External link) .

Apply for a developer account

A Twitter developer account is required to use the Twitter API. For those using the Twitter API for the first time, refer to the Subscription Plan (External link) page of the Twitter developer website.

Register a new application

Access the Apps page (External link) of the Twitter developer website.

Click Create an App to create a new application. Enter in an appropriate name for the app under App name, and a description of the application in Application description. The website URL can be a dummy URL such as http://sample.com.

In the Tell us how this app will be used field, fill in a brief description of the application and how it will be used.

Click Create to save the app.

Next, navigate to the Keys and tokens tab and make a note of the API key and API secret key under Consumer API keys.

Click Generate above the Access token & access token secret box.

Make note of the displayed Access token and Access token secret. They will not be displayed again and if forgotten will need to be regenerated.

Prepare the Lambda Function Executable

This step must be done in an environment where Node.js is installed.

Save the following sample code as a file named index.js.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
'use strict';
const twitter = require('twitter');
const moment = require('moment');
const https = require('https');
const querystring = require('querystring');
const path = require('path');

// Kintone subdomain and App information
const DOMAIN = process.env.KINTONE_DOMAIN;
const APP_ID = process.env.KINTONE_APP_ID;
const API_TOKEN = process.env.KINTONE_API_TOKEN;

// Twitter app information
const twitterClient = new twitter({
  consumer_key: process.env.TWITTER_API_KEY,
  consumer_secret: process.env.TWITTER_API_SECRET,
  access_token_key: process.env.TWITTER_ACCESS_TOKEN_KEY,
  access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET
});

const getOptions = (apiPath, method) => {
  return {
    hostname: DOMAIN,
    port: 443,
    path: apiPath,
    method: method,
    headers: {
      'X-Cybozu-API-Token': API_TOKEN
    }
  };
};

// Retrieve records from Kintone if the status is "Tweet scheduled" and the Tweet datetime is past the current datetime
const getRecords = () => {
  return new Promise((resolve, reject) => {
    console.log('[START] get Kintone records');

    // Retrieve the current datetime
    const m = moment();
    const currentDate = m.toISOString();

    const params = {
      app: APP_ID,
      query: `Status = "Tweet scheduled" and tweetDate <= "${currentDate}"`,
      fields: ['$id', 'tweetBody', 'imageFile']
    };
    const query = querystring.stringify(params);
    const options = getOptions('/k/v1/records.json?' + query, 'GET');

    const req = https.request(options, (res) => {
      res.setEncoding('utf-8');
      res.on('data', (chunk) => {
        if (res.statusCode === 200) {
          resolve(JSON.parse(chunk).records);
        } else {
          reject(JSON.parse(chunk).message);
        }
      });
    }).on('error', (err) => {
      console.log('error: ', err.stack);
      reject(new Error(err));
    });
    req.end();
    console.log('[END] get Kintone records');
  });
};

// Download image from Kintone
const downloadImageFile = (fileInfo) => {
  return new Promise((resolve, reject) => {
    console.log('[START] download image file');

    const params = {
      fileKey: fileInfo.fileKey
    };
    const query = querystring.stringify(params);
    const options = getOptions('/k/v1/file.json?' + query, 'GET');

    const req = https.request(options, (res) => {
      const fileData = [];
      res.on('data', (chunk) => {
        if (res.statusCode === 200) {
          fileData.push(chunk);
        } else {
          reject(JSON.parse(chunk).message);
        }
      });
      res.on('end', () => {
        if (res.statusCode === 200) {
          resolve(Buffer.concat(fileData));
        }
      });
    }).on('error', (err) => {
      console.log('error:', err.stack);
      reject(new Error(err));
    });
    req.end();
    console.log('[END] download image file');
  });
};

// Upload image to Twitter
const mediaUpload = (fileData) => {
  return new Promise((resolve, reject) => {
    console.log('[START] media upload');
    twitterClient.post('media/upload', {media: fileData})
      .then((media) => {
        resolve(media.media_id_string);
      }).catch((err) => {
        console.log('error:', err.stack);
        reject(new Error(err));
      });
  });

};

// Post to Twitter
const tweet = (tweetBody, mediaIdList) => {
  return new Promise((resolve, reject) => {
    console.log('[START] tweet');

    const status = {
      status: tweetBody,
      media_ids: mediaIdList.join(',')
    };
    twitterClient.post('statuses/update', status, (err, tweetRes, res) => {
      if (err !== null) {
        reject(JSON.stringify(err));
      } else {
        resolve(tweetRes);
      }
    });
  });
};

// Update the Kintone status and enter the Tweet URL
const updateStatus = (rid, tweetUrl) => {
  console.log('[START] Kintone status update');
  return new Promise((resolve, reject) => {
    const params = {
      requests: [
        {
          method: 'PUT',
          api: '/k/v1/record/status.json',
          payload: {
            app: APP_ID,
            id: rid,
            action: 'Complete'
          }
        },
        {
          method: 'PUT',
          api: '/k/v1/record.json',
          payload: {
            app: APP_ID,
            id: rid,
            record: {
              tweetUrl: {
                value: tweetUrl
              }
            }
          }
        }
      ]
    };
    const options = getOptions('/k/v1/bulkRequest.json', 'POST');
    options.headers['Content-Type'] = 'application/json';

    const req = https.request(options, (res) => {
      res.setEncoding('utf-8');
      res.on('data', (chunk) => {
        if (res.statusCode === 200) {
          resolve(rid);
        } else {
          reject(JSON.parse(chunk).message);
        }
      });
    }).on('error', (err) => {
      console.log('error: ', err.stack);
      reject(new Error(err));
    });
    req.write(JSON.stringify(params));
    req.end();
  });
};

const handleRecord = (record) => {
  const rid = record.$id.value;
  console.log(`[START] record id: ${rid}`);
  const tweetBody = record.tweetBody.value;

  const downloadImageHandlers = record.imageFile.value.map((imageFile) => {
    const ext = path.extname(imageFile.name).toLowerCase();
    if (ext === '.png' || ext === '.gif' || ext === '.jpg' || ext === '.jpeg') {
      return downloadImageFile(imageFile);
    }
  });
  return Promise.all(downloadImageHandlers)
    .then((fileDataList) => {
      const mediaUploadHandlers = fileDataList.map((fileData) => {
        return mediaUpload(fileData);
      });
      return Promise.all(mediaUploadHandlers);
    }).then((mediaIdList) => {
      return tweet(tweetBody, mediaIdList);
    }).then((tweetRes) => {
      const tweetUrl = `https://twitter.com/${tweetRes.user.screen_name}/status/${tweetRes.id_str}`;
      return updateStatus(rid, tweetUrl);
    }).catch((err) => {
      throw new Error(err);
    });
};

exports.handler = () => {
  return getRecords()
    .then((records) => {
      if (records.length === 0) {
        console.log('[COMPLETE] nothing to do');
      } else {
        const promises = records.map((record) => {
          return handleRecord(record);
        });
        return Promise.all(promises).then((rid) => {
          console.log('[COMPLETE] record id: ' + rid.join(', '));
        }).catch((err) => {
          throw new Error(err);
        });
      }

    }).catch((err) => {
      console.log(err);
    });
};

Run the following commands to install the module and create the ZIP file (the Lambda function executable).

1
2
$ npm install twitter moment async https querystring path
$ zip -rq kintone-to-twitter.zip index.js node_modules
tips
Note

If an error occurs with the ZIP command, check that the file index.js and the directory node_modules exist in the hierarchy.

Prepare the AWS Settings

Create an execution role

Refer to the AWS Lambda execution role (External link) documentation to create a role to execute Lambda.

In this sample, the role is created with the name AWSLambdaExecute as an example.

Create a new Lambda function

In AWS Lambda, click Create function.

Choose Author from scratch and enter in the rest of the settings as shown in the image below. The Function name can be anything and the Runtime should be set to Node.js 12.x. The Execution role should is set as the role that was created in the previous step.

After creating the function, click Add trigger.

USe the drop down to select EventBridge (CloudWatch Events) as the trigger.
Enter in any value for Rule and Rule name, and under Schedule expression, set the rate to 1 minute.

tips
Note

Note that usage of EventBridge is charged based on the number of events published. Refer to the Amazon EventBridge Pricing (External link) page for details.

Click the kintone-to-twitter box to return to the Lambda function settings and click Actions to open up the Actions menu. Click to Upload a .zip file and upload the Lambda function execution file created earlier, named kintone-to-twitter.zip.

Navigate to Environment variables and click Manage environment variables.

Click Add environment variable to create a new row of variable settings.

Referring to the following section of the index.js code, create a row of variable settings for each variable, matching up the capitalized portion of the variables with the values obtained earlier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Kintone subdomain and App information
const DOMAIN = process.env.KINTONE_DOMAIN;
const APP_ID = process.env.KINTONE_APP_ID;
const API_TOKEN = process.env.KINTONE_API_TOKEN;

// Twitter app information
const twitterClient = new twitter({
  consumer_key: process.env.TWITTER_API_KEY,
  consumer_secret: process.env.TWITTER_API_SECRET,
  access_token_key: process.env.TWITTER_ACCESS_TOKEN_KEY,
  access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET
});

The environment variable settings should look like the following:

Navigate down to the Basic settings and click Edit.

Enter the settings as follows, checking that the Handler, Timeout, and Execution role values are correct. Click save when done.

Test the Integration

Add a new record into the Kintone App, filling in the tweet content and the desired date of the tweet. Proceed the process management states to "Tweet Scheduled".

Check that the tweet is posted when the set datetime has passed. If the configuration is successful, the record status should change to Complete and the link to the posted tweet should be entered in the Link field.

Limitations

Note that images may be added to the tweet with the following conditions:

  1. Accepted file types: JPG, PNG, GIF, WEBP
  2. Maximum file size: 5MB
  3. Maximum static images in one tweet (JPG, PNG, WEBP): Up to 4
  4. Maximum GIFs in one tweet: 1
  5. GIF files must be uploaded alone and cannot be mixed with other image types.

Reference