I am using the validate-slack-request package to validate my incoming slack requests are from slack. This works fine for Slash commands and Interactive Components (buttons, etc.). However it is not working for the Events API
I noticed that the POST request body has a different format for the Events API. There's no payload. But it's not clear to me what slack is giving me to use to verify. Code is below
//This WORKS
app.post("/interactiveCommand", async (req, res) => {
const legit = validateSlackRequest(process.env.SLACK_SIGNING_SECRET, req, false);
if (!legit) {
console.log("UNAUTHORIZED ACCESS ", req.headers, req.body);
return res.status(403).send("Unauthorized");
}
await interactiveCommand(...);
return;
});
//This does NOT WORK
app.post("/slackEvents", parser, json, async (req, res) => {
const legit = validateSlackRequest(process.env.SLACK_SIGNING_SECRET, req, false);
if (!legit) {
console.log("UNAUTHORIZED ACCESS ", req.headers, req.body);
res.status(403).send("Unauthorized");
} else {
try {
switch (req.body.event.type) {
case "message":
await handleMessageEvent(...);
break;
case "app_home_opened":
res.status(200).send();
await updateUserHomePage(...);
break;
default:
res.status(200).send();
return;
}
} catch(e) {
console.log("Error with event handling! ", e);
}
}
});
const crypto = require('crypto')
const querystring = require('querystring')
// Adhering to RFC 3986
// Inspired from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
function fixedEncodeURIComponent (str) {
return str.replace(/[!'()*~]/g, function (c) {
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
})
}
/**
* Validate incoming Slack request
*
* #param {string} slackAppSigningSecret - Slack application signing secret
* #param {object} httpReq - Express request object
* #param {boolean} [logging=false] - Enable logging to console
*
* #returns {boolean} Result of vlaidation
*/
function validateSlackRequest (slackAppSigningSecret, httpReq, logging) {
logging = logging || false
if (typeof logging !== 'boolean') {
throw new Error('Invalid type for logging. Provided ' + typeof logging + ', expected boolean')
}
if (!slackAppSigningSecret || typeof slackAppSigningSecret !== 'string' || slackAppSigningSecret === '') {
throw new Error('Invalid slack app signing secret')
}
const xSlackRequestTimeStamp = httpReq.get('X-Slack-Request-Timestamp')
const SlackSignature = httpReq.get('X-Slack-Signature')
const bodyPayload = fixedEncodeURIComponent(querystring.stringify(httpReq.body).replace(/%20/g, '+')) // Fix for #1
if (!(xSlackRequestTimeStamp && SlackSignature && bodyPayload)) {
if (logging) { console.log('Missing part in Slack\'s request') }
return false
}
const baseString = 'v0:' + xSlackRequestTimeStamp + ':' + bodyPayload
const hash = 'v0=' + crypto.createHmac('sha256', slackAppSigningSecret)
.update(baseString)
.digest('hex')
if (logging) {
console.log('Slack verifcation:\n Request body: ' + bodyPayload + '\n Calculated Hash: ' + hash + '\n Slack-Signature: ' + SlackSignature)
}
return (SlackSignature === hash)
}
This is how I got it to work, it was a little bit of trial and error and I make no promises. Basically, if I was validating an Event rather than a Slash Command or an Interactive Component, I would pass type="Event" to the validation function. The only change is how I construct the payload from the incoming request
export function validateSlackRequest(
slackAppSigningSecret,
httpReq,
logging,
type = ""
) {
logging = logging || false;
if (typeof logging !== "boolean") {
throw new Error(
"Invalid type for logging. Provided " +
typeof logging +
", expected boolean"
);
}
if (
!slackAppSigningSecret ||
typeof slackAppSigningSecret !== "string" ||
slackAppSigningSecret === ""
) {
throw new Error("Invalid slack app signing secret");
}
const xSlackRequestTimeStamp = httpReq.get("X-Slack-Request-Timestamp");
const SlackSignature = httpReq.get("X-Slack-Signature");
let bodyPayload;
if (type === "Event") {
bodyPayload = (httpReq as any).rawBody;
} else {
bodyPayload = fixedEncodeURIComponent(
querystring.stringify(httpReq.body).replace(/%20/g, "+")
); // Fix for #1
}
if (!(xSlackRequestTimeStamp && SlackSignature && bodyPayload)) {
if (logging) {
console.log("Missing part in Slack's request");
}
return false;
}
const baseString = "v0:" + xSlackRequestTimeStamp + ":" + bodyPayload;
const hash =
"v0=" +
crypto
.createHmac("sha256", slackAppSigningSecret)
.update(baseString)
.digest("hex");
if (logging) {
console.log(
"Slack verification:\nTimestamp: " +
xSlackRequestTimeStamp +
"\n Request body: " +
bodyPayload +
"\n Calculated Hash: " +
hash +
"\n Slack-Signature: " +
SlackSignature
);
}
return SlackSignature === hash;
}
I convert text payloads into json with this:
IFS=$'\n'
if [ "$REQUEST_METHOD" = "POST" ]; then
if [ "$CONTENT_LENGTH" -gt 0 ]; then
cat - > /tmp/file.txt
fi
fi
cat /tmp/file.txt | sed -e "s/^payload=//g" | perl -pe 's/\%(\w\w)/chr hex $1/ge' > /tmp/file.json
Related
I am trying to activate Magento2, version 2.4.4, integration with expressjs backend.
The callback url is being hit and the data is being stored in the db. Then upon hitting the identity url, the pop up of login for app to be integrated is opened and user logs in.
Following the oauth process as defined at https://devdocs.magento.com/guides/v2.4/get-started/authentication/gs-authentication-oauth.html#pre-auth-token on making the POST request to /oauth/token/request I'm getting the following error -
oauth_problem=An+error+occurred+validating+the+nonce
I cannot figure out the source of this error, please help me fix this as I've been stuck at it since many days.
Following are one of the values calculated for the header Authorization and the post body -
Authorization: 'OAuth oauth_consumer_key=kxw5v6vwr4rm77cn2pxmqxdzdhhkor58, oauth_nonce=Fi9KRqgAmSX7sf32YpCTdPQ15FIY-LyY, oauth_signature=OTUzNWU4ZDViMzljZmM1NTM2MDNiMGQxOTUyMmRmMGRiMjdkZDZmNzY5ZTIxZTZkNGM1MzMzMmRkN2U5ZjcxNQ%3D%3D, oauth_signature_method=HMAC-SHA256, oauth_timestamp=1652694701394, oauth_version=1.0'
POST BODY -
{
oauth_consumer_key: 'kxw5v6vwr4rm77cn2pxmqxdzdhhkor58',
oauth_nonce: 'Fi9KRqgAmSX7sf32YpCTdPQ15FIY-LyY',
oauth_signature: 'OTUzNWU4ZDViMzljZmM1NTM2MDNiMGQxOTUyMmRmMGRiMjdkZDZmNzY5ZTIxZTZkNGM1MzMzMmRkN2U5ZjcxNQ%3D%3D',
oauth_signature_method: 'HMAC-SHA256',
oauth_timestamp: '1652694701394',
oauth_version: '1.0'
}
Following is callback url route code -
router.post('/magento-integration/callback', callbackHandler);
async function callbackHandler(req, res) {
const [{store_base_url, oauth_verifier, oauth_consumer_key, oauth_consumer_secret}] = [req.body];
try {
await saveOAuthCredentials({
store_base_url,
oauth_verifier,
oauth_consumer_key,
oauth_consumer_secret
});
return ApiResponse(res, 200);
} catch (err) {
// TODO: check err and set precise value of response status code and err msg
console.error(err.message)
return ApiResponse(res, 500, {message: err});
}
}
Following is the code for the controller of identity url route -
async function appLogin(req, res) {
// code to validate user
// ......
// Magento2 OAuth token exchange initiation
// Magento2 initiates the token exchange process by requesting the /login endpoint and sends
// url encoded query string params oauth_consumer_key and success_call_back which the front end sends in
// the body, against key queryParams, of the request it makes to /appLogin endpoint of sx-sellerapi.
const {oauth_consumer_key, success_call_back} = req.body.queryParams req.body.queryParams : [{}];
if(oauth_consumer_key && success_call_back){
try{
await runMagentoOAuthKeyX(sellerInfo.id, oauth_consumer_key);
res.redirect(success_call_back);
return;
} catch(err) {
return ApiResponse(res, 400, {message: err})
}
}
// rest of the code for usual login
}
Code for runMagentoOAuthKeyX
async function runMagentoOAuthKeyX(sellerId, oauthConsumerKey) {
try {
const oauthCred = await magentoModel.checkOAuthConsumerKeyExists(oauthConsumerKey, sellerId);
// isNonEmptyObject checks if arg passed is of type Object and has keys
if (isNonEmptyObject(oauthCred)) {
oauthCred.oauth_consumer_key = oauthConsumerKey;
oauthCred.url = `${oauthCred.store_base_url}${OAUTH_TOKEN_ENDPOINTS.request}`;
let requestTokenData;
try{
requestTokenData = await getToken(oauthCred, OAUTH_TOKEN_TYPE.requestToken);
} catch(err){
throw err
}
return Promise.all([
magentoModel.updateOAuthCred(oauthConsumerKey, requestTokenData, OAUTH_TOKEN_TYPE.requestToken),
getToken({...oauthCred, ...requestTokenData,
...{url: `${oauthCred.store_base_url}${OAUTH_TOKEN_ENDPOINTS.access}`}}, OAUTH_TOKEN_TYPE.accessToken)
])
.then(async ([_, accessTokenData]) =>
magentoModel.updateOAuthCred(oauthConsumerKey, accessTokenData, OAUTH_TOKEN_TYPE.accessToken)
)
.catch(err => {
throw err;
});
} else {
throw new Error(`OAuthConsumer key passed is unknown ${oauthConsumerKey}`);
}
} catch (err) {
// TODO: add logging
throw err;
}
Code for getToken()
async function getToken(tokenData, tokenType) {
const {url} = tokenData
const [authHeader, body] = await getAuthHeaderAndBody(tokenData, tokenType);
return axios.post(
url,
body,
{
headers: {
Authorization: authHeader
}
})
.catch(err => {
console.error(err.response.data);
throw err;
});
}
Code for getAuthHeaderAndBody
async function getAuthHeaderAndBody(tokenData, tokenType) {
const oauth_nonce = await genOAuthNonce();
const oauth_timestamp = Date.now();
const {
oauth_consumer_key,
oauth_consumer_secret,
oauth_signature_method,
url,
oauth_token,
oauth_token_secret,
oauth_verifier
} = tokenData;
const tokenList = ['access', 'webAPI'];
const oauthSignature = genOAuthSignature(url, {
oauth_consumer_key,
oauth_consumer_secret,
oauth_signature_method,
oauth_nonce,
oauth_timestamp,
oauth_version: OAUTH_VERSION,
oauth_token: tokenList.includes(tokenType) ? oauth_token : null,
oauth_token_secret: tokenList.includes(tokenType) ? oauth_token_secret : null,
oauth_verifier: OAUTH_TOKEN_TYPE.accessToken === tokenType ? oauth_verifier : null
});
const validParams = Object.entries({
oauth_consumer_key,
oauth_signature_method,
oauth_signature: oauthSignature,
oauth_nonce,
oauth_timestamp,
oauth_version: OAUTH_VERSION,
oauth_token: tokenList.includes(tokenType) ? oauth_token : null,
oauth_verifier: OAUTH_TOKEN_TYPE.accessToken == tokenType ? oauth_verifier : null
})
.filter(([_, val]) => val !== null)
.sort((a, b) => a[0] < b[0] ? -1 : 0);
const authHeaderValue = validParams
.map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
.join(', ');
const authHeaderStart = [OAUTH_TOKEN_TYPE.requestToken, OAUTH_TOKEN_TYPE.accessToken].includes(tokenType) ? 'OAuth' : 'Bearer';
const authHeader = `${authHeaderStart} ${authHeaderValue}`;
return [authHeader, Object.fromEntries(validParams)];
}
Code for genOAuthNonce -
async function genOAuthNonce() {
const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._~';
const buff = Buffer.alloc(32);
const result = [];
return new Promise((resolve, reject) => crypto.randomFill(buff, (err, buff) => {
if(err){
reject(err);
}
buff.forEach(c => result.push(charset[c % charset.length]));
resolve(result.join(''));
}));
}
Code for genOAuthSignature
function genOAuthSignature(baseUrl, params, method = 'POST') {
const keysNotInSignature = ['oauth_consumer_secret', 'oauth_token_secret'];
const signatureString = Object.entries(params)
.filter(([key, val]) => val
!= null && !keysNotInSignature.includes(key))
.sort((item1, item2) => item1[0] < item2[0 ] ? -1 : 0)
.map(([key, val]) => `${key}=${val}`)
.join(AUTH_HEADER_DELIMITER);
const baseString = [
encodeURIComponent(method.toUpperCase()),
encodeURIComponent(baseUrl),
encodeURIComponent(signatureString)
].join(AUTH_HEADER_DELIMITER);
const {oauth_consumer_secret, oauth_token_secret} = params;
let signKey = `${encodeURIComponent(oauth_consumer_secret)}${AUTH_HEADER_DELIMITER}`
signKey += oauth_token_secret ? `${encodeURIComponent(oauth_token_secret)}` : '';
const hmac = createHmac('sha256', signKey);
return Buffer.from(hmac.update(baseString).digest('hex')).toString('base64');
}
Found the bugs in the code for invalid Nonce. The issue was with timestamp as I was using Date.now() which returns UTC timestamp in ms whereas magento2 oauth requires it to be in seconds. Also found and fixed the bug in evaluating the signature for oauth token exchange.
In function getAuthHeaderAndBody -
async function getAuthHeaderAndBody(tokenData, tokenType) {
const oauth_nonce = await genOAuthNonce();
// changed below from Date.now() as timestamp must be in seconds.
const oauth_timestamp = parseInt(Date.now() / 1000);
// ... rest of the code
}
In genOAuthSignature
function genOAuthSignature(baseUrl, params, method = 'POST') {
// preceding code
// last line is changed by looking at Magento2 code for validating the signature
return createHmac('sha256', signKey)
.update(baseString, 'binary')
.digest()
.toString('base64');
}
I want to delete by facebook provider user in my firebase authentication using this question in my nodeJs. But somehow i getting error that getUserByProviderUid is not a function. I attached the error message below.
Here is code
exports.deleteUserData = functions.https.onRequest(async (req,
res) => {
try {
const signedRequest = req.body.signed_request;
console.log('signRewues',signedRequest)
const userObj = parseSignedRequest(signedRequest,
'Facebook secret');
console.log('User Obj',userObj, userObj.user_id);
const userRecord = await
admin.auth().getUserByProviderUid("facebook.com",
userObj.user_id);
console.log('userRecord',userRecord);
await admin.auth().deleteUser(userRecord.uid);
res.json({
url: "<URL>",
confirmation_code: "<Code>",
});
} catch (e) {
console.log(e);
res.status(400).json({
message: "Bad Request",
});
}
});
function base64decode(data) {
while (data.length % 4 !== 0) {
data += "=";
}
data = data.replace(/-/g, "+").replace(/_/g, "/");
return Buffer.from(data, "base64").toString("utf-8");
};
function parseSignedRequest(signedRequest, secret) {
var encoded_data = signedRequest.split(".", 2);
// decode the data
var sig = encoded_data[0];
var json = base64decode(encoded_data[1]);
var data = JSON.parse(json);
if (!data.algorithm || data.algorithm.toUpperCase() != "HMAC-
SHA256") {
throw Error(
"Unknown algorithm: " + data.algorithm + ". Expected HMAC-
SHA256"
);
}
var expected_sig = crypto
.createHmac("sha256", secret)
.update(encoded_data[1])
.digest("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace("=", "");
if (sig !== expected_sig) {
throw Error("Invalid signature: " + sig + ". Expected " +
expected_sig);
}
return data;
}
The getUserByProviderUid() was added in version 9.5.0 of Admin SDK. You'll have to update the Admin SDK to use it. Try upgrading to latest version.
npm i firebase-admin#latest
I am trying to implement Twitter login with my MERN application. Following twitter tutorials, i understand that all request should be signed with an OAuth headers. If i use postman, i enter my credentials (Consumer Key, Consumer Secret) in the authorization tab and the call works. The things is that postman transform the Consumer secret into and oauth_signature before sending the call. Now i want to do this workflow in Nodejs. All tutorials online use complicated passport strategies and the use of request module which is depricated. I understand that to generate oauth_signature one would have to generate an oauth_nonce and then do:
Percent encode every key and value that will be signed.
Sort the list of parameters alphabetically [1] by encoded key [2].
For each key/value pair:
Append the encoded key to the output string.
Append the ‘=’ character to the output string.
Append the encoded value to the output string.
If there are more key/value pairs remaining, append a ‘&’ character to the output string.
I am sure doing all this would be reinventing the wheel and am pretty sure there is a module that does this step specifically without all the passport and authentication (which is already done in my app) i simply need to sign my twitter requests like Postman does nothing more.
I tried the following but it seems am still doing something wrong
var axios = require('axios');
const jsSHA = require('jssha/sha1');
//Am i using the right library??
const callBackUL = 'https%3A%2F%2F127.0.0.1%3A3000%2Flogin';
var oauth_timestamp = Math.round(new Date().getTime() / 1000.0);
const nonceObj = new jsSHA('SHA-1', 'TEXT', { encoding: 'UTF8' });
nonceObj.update(Math.round(new Date().getTime() / 1000.0));
const oauth_nonce = nonceObj.getHash('HEX');
const endpoint = 'https://api.twitter.com/oauth/request_token';
const oauth_consumer_key = process.env.TWITTER_API_KEY;
const oauth_consumer_secret = process.env.TWITTER_API_SECRET;
var requiredParameters = {
oauth_consumer_key,
oauth_nonce,
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp,
oauth_version: '1.0'
};
const sortString = requiredParameters => {
var base_signature_string = 'POST&' + encodeURIComponent(endpoint) + '&';
var requiredParameterKeys = Object.keys(requiredParameters);
for (var i = 0; i < requiredParameterKeys.length; i++) {
if (i == requiredParameterKeys.length - 1) {
base_signature_string += encodeURIComponent(
requiredParameterKeys[i] +
'=' +
requiredParameters[requiredParameterKeys[i]]
);
} else {
base_signature_string += encodeURIComponent(
requiredParameterKeys[i] +
'=' +
requiredParameters[requiredParameterKeys[i]] +
'&'
);
}
}
return base_signature_string;
};
const sorted_string = sortString(requiredParameters);
console.log('Sorted string:', sorted_string);
const signing = (signature_string, consumer_secret) => {
let hmac;
if (
typeof signature_string !== 'undefined' &&
signature_string.length > 0
) {
//console.log('String OK');
if (
typeof consumer_secret !== 'undefined' &&
consumer_secret.length > 0
) {
// console.log('Secret Ok');
const secret = consumer_secret + '&';
var shaObj = new jsSHA('SHA-1', 'TEXT', {
hmacKey: { value: secret, format: 'TEXT' }
});
shaObj.update(signature_string);
hmac = encodeURIComponent(shaObj.getHash('B64'));
//var hmac_sha1 = encodeURIComponent(hmac);
}
}
return hmac;
};
const signed = signing(sorted_string, oauth_consumer_secret);
console.log(signed);
var data = {};
var config = {
method: 'post',
url: endpoint,
headers: {
Authorization: `OAuth oauth_consumer_key=${process.env.TWITTER_API_KEY},oauth_signature_method="HMAC-SHA1",oauth_timestamp=${oauth_timestamp},oauth_nonce=${oauth_nonce},oauth_version="1.0",oauth_callback=${callBackUL},oauth_consumer_secret=${signed}`,
'Content-Type': 'application/json'
},
data: data
};
try {
const response = await axios(config);
console.log(JSON.stringify(response.data));
} catch (err) {
console.log(err.response.data);
}
next();
});
SOLVED
var axios = require('axios');
const jsSHA = require('jssha/sha1');
const callBackUL = 'https%3A%2F%2F127.0.0.1%3A3000%2Flogin';
var oauth_timestamp = Math.round(new Date().getTime() / 1000.0);
const nonceObj = new jsSHA('SHA-1', 'TEXT', { encoding: 'UTF8' });
nonceObj.update(Math.round(new Date().getTime() / 1000.0));
const oauth_nonce = nonceObj.getHash('HEX');
const endpoint = 'https://api.twitter.com/oauth/request_token';
const oauth_consumer_key = process.env.TWITTER_API_KEY;
const oauth_consumer_secret = process.env.TWITTER_API_SECRET;
var requiredParameters = {
oauth_callback: callBackUL,
oauth_consumer_key,
oauth_nonce,
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp,
oauth_version: '1.0'
};
const sortString = requiredParameters => {
var base_signature_string = 'POST&' + encodeURIComponent(endpoint) + '&';
var requiredParameterKeys = Object.keys(requiredParameters);
for (var i = 0; i < requiredParameterKeys.length; i++) {
if (i == requiredParameterKeys.length - 1) {
base_signature_string += encodeURIComponent(
requiredParameterKeys[i] +
'=' +
requiredParameters[requiredParameterKeys[i]]
);
} else {
base_signature_string += encodeURIComponent(
requiredParameterKeys[i] +
'=' +
requiredParameters[requiredParameterKeys[i]] +
'&'
);
}
}
return base_signature_string;
};
const sorted_string = sortString(requiredParameters);
console.log('Sorted string:', sorted_string);
const signing = (signature_string, consumer_secret) => {
let hmac;
if (
typeof signature_string !== 'undefined' &&
signature_string.length > 0
) {
//console.log('String OK');
if (
typeof consumer_secret !== 'undefined' &&
consumer_secret.length > 0
) {
// console.log('Secret Ok');
const secret = encodeURIComponent(consumer_secret) + '&';
var shaObj = new jsSHA('SHA-1', 'TEXT', {
hmacKey: { value: secret, format: 'TEXT' }
});
shaObj.update(signature_string);
hmac = encodeURIComponent(shaObj.getHash('B64'));
}
}
return hmac;
};
const signed = signing(sorted_string, oauth_consumer_secret);
console.log(signed);
var data = {};
var config = {
method: 'post',
url: endpoint,
headers: {
Authorization: `OAuth oauth_consumer_key=${process.env.TWITTER_API_KEY},oauth_nonce=${oauth_nonce},oauth_signature=${signed},oauth_signature_method="HMAC-SHA1",oauth_timestamp=${oauth_timestamp},oauth_version="1.0",oauth_callback=${callBackUL}`,
'Content-Type': 'application/json'
},
data: data
};
try {
const response = await axios(config);
console.log(JSON.stringify(response.data));
} catch (err) {
console.log(err.response.data);
}
next();
First of all I have to say that I have and intermediate experience on Java and very very basical with JS.
I'm trying to remove expired tokens from my database, for achieve that I did:
function sendNotificationToUser(payload, userSnapshot) {
const userId = userSnapshot.id;
const user = userSnapshot.data();
let tokenMap = user.tokens;
const tokens = Object.keys(tokenMap);
const options = {priority: "high"};
admin.messaging().sendToDevice(tokens, payload, options).then(response => {
// For each message check if there was an error.
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
// Cleanup the tokens who are not registered anymore.
if (error.code === 'messaging/invalid-registration-token' || error.code === 'messaging/registration-token-not-registered') {
tokenMap.delete(tokens[index]);
}
} else{
console.log("Sent to user: ", user.name, " " ,user.surname, " notification: ", payload, " tokens: ", tokens[index]);
}
});
usersRef.doc(userId).update({
tokens: tokenMap
});
});
}
No problem to get the keys of the tokenMap, but looks like I can't remove entries with .delete(), since I got that in my Log:
TypeError: tokenMap.delete is not a function
at response.results.forEach (/user_code/index.js:127:36)
at Array.forEach (native)
at admin.messaging.sendToDevice.then.response (/user_code/index.js:122:26)
at process._tickDomainCallback (internal/process/next_tick.js:135:7)
What is the reason??
Solved:
delete tokensObj[tokensArray[index]];
Full code:
function sendNotificationToUser(payload, userSnapshot) {
const user = userSnapshot.data();
let tokensObj = user.tokens;
const tokensArray = Object.keys(tokensObj);
let toUpdate = false;
const options = {priority: "high"};
admin.messaging().sendToDevice(tokensArray, payload, options).then(response => {
// For each message check if there was an error.
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
// Cleanup the tokens who are not registered anymore.
if (error.code === 'messaging/invalid-registration-token' || error.code === 'messaging/registration-token-not-registered') {
toUpdate = true;
delete tokensObj[tokensArray[index]];
}
} else {
console.log("Sent to user: ", user.name, " ", user.surname, " notification: ", payload, " token: ", tokensArray[index]);
}
});
if (toUpdate === true) {
userSnapshot.ref.update({ tokens: tokensObj }).catch(error => console.log(error));
}
});
}
I'm using the Twit Node library to reply to tweets in a stream and while it's working perfectly the tweeted reply does not show up as a reply on the timeline, instead, it appears as a standalone tweet, not linked to a prior conversation.
Here's my code:
function tweetEvent(eventMsg) {
var replyto = eventMsg.in_reply_to_screen_name;
var text = eventMsg.text;
var from = eventMsg.user.screen_name;
console.log(replyto + ' ' + from);
if( (text.indexOf('myhandle') >= 0) || (from != 'myhandle')) {
var reply = replies[Math.floor(Math.random() * replies.length)];
var newtweet = '#' + from + ' ' + reply;
tweetIt(newtweet);
}
}
function tweetIt(txt) {
var tweet = {
status: txt
}
T.post('statuses/update', tweet, tweeted);
function tweeted(err, data, response) {
if (err) {
console.log("Something went wrong!");
} else {
console.log("It worked!");
}
}
}
In order for the reply to show up in the timeline using the Twitter API, you need the following:
// the status update or tweet ID in which we will reply
var nameID = eventMsg.id_str;
Also needed is the parameter in_reply_to_status_id in your tweet status. See the updates to your code below and it should now preserve the conversation:
function tweetEvent(eventMsg) {
var replyto = eventMsg.in_reply_to_screen_name;
var text = eventMsg.text;
var from = eventMsg.user.screen_name;
// the status update or tweet ID in which we will reply
var nameID = eventMsg.id_str;
console.log(replyto + ' ' + from);
if( (text.indexOf('myhandle') >= 0) || (from != 'myhandle')) {
var reply = replies[Math.floor(Math.random() * replies.length)];
var newtweet = '#' + from + ' ' + reply;
tweetIt(newtweet);
}
function tweetIt(txt) {
var tweet = {
status: txt,
in_reply_to_status_id: nameID
}
}
T.post('statuses/update', tweet, tweeted);
function tweeted(err, data, response) {
if (err) {
console.log("Something went wrong!");
} else {
console.log("It worked!");
}
}
}