Puppeteer: How to bypass FUNCAPTCHA without submit button - web-scraping

I'm going to get right to the point. I'm trying to bypass Twitch's login funcaptcha using 2captcha's API. Everything works well: My bot communicates with 2captcha fast enough where the captcha won't timeout and my bot receives the token needed to authenticate the captcha and login, except there is no submit button to actually go through with the login. To the best of my knowledge, I've looked through every Github post, every stackoverflow post regarding this issue, and the best solution I could find was to look through the Google Network tab in the devtools to look for a function to call manually which would simulate a button and get you through the captcha. With my limited coding ability, I was able to find a function which references ArkoseEnforcement & the word "callback" which I believe is what I'm trying to find. After attempting to scavenge the sources tab and network tab the best I could find was this:
I've been trying to implement this function for an hour with no success and I also tried looking at "callback" and "ArkoseEnforment.funcaptcha_events" etc. etc.
I seriously have no idea how to implement them into my code and I would appreciate any help on where to start.
//require packages
require('dotenv').config(); const fs = require('fs'); const puppeteer = require('puppeteer'); const https = require('https');
//define varibles
var usernames = []; var pkValue; var surl = process.env.SURL ; var pk = process.env.PUBLICKEY ; var api = process.env.APIKEY;
const readFile = fs.readFileSync("accounts.txt").toString().split("\n");
//read accounts
console.log(`Reading ${readFile.length} lines.`);
for(i in readFile) {
(readFile[i].toLowerCase().startsWith("u") ? usernames.push(readFile[i].slice(10).replace("\r", "")) : null);
}
//get captcha
//retreive stream keys
async function getKey(username) {
const browser = await puppeteer.launch({headless:false}); const page = await browser.newPage(); const navigationPromise = page.waitForNavigation()
//get captcha
function getCaptcha(captchaID) {
https.get(`https://2captcha.com/res.php?key=${api}&action=get&id=${captchaID.slice(3)}`, (response) => {
var dataRes = '';
response.on('data', (chunk) => {
dataRes += chunk;
})
response.on('end', () => {
if (dataRes.toLowerCase()==="capcha_not_ready") {
setTimeout(() => {getCaptcha(captchaID)}, 5000)
}
else {
pkValue = dataRes.slice(3)
console.log(pkValue);
page.evaluate((pkValue) => {
//add token to val
document.querySelector('#FunCaptcha-Token').value = pkValue;
//submit form
//somehow need to submit the form here.
}, pkValue)
}
})
})
}
await page.goto(`https://dashboard.twitch.tv/u/${username}/settings/stream`, { waitUntil: ['networkidle2'] })
await page.setViewport({ width: 1920, height: 880 })
await page.waitForSelector('.sc-AxjAm #login-username')
await page.click('.sc-AxjAm #login-username')
await page.type('.sc-AxjAm #login-username', username)
await page.waitForSelector('.sc-AxjAm #password-input')
await page.click('.sc-AxjAm #password-input')
await page.type('.sc-AxjAm #password-input', process.env.PASSWORD)
await page.waitForSelector('.sc-AxjAm > .sc-AxjAm:nth-child(3) > .ScCoreButton-sc-1qn4ixc-0 > .ScCoreButtonLabel-lh1yxp-0 > .sc-AxjAm', {visible:true})
await page.click('.sc-AxjAm > .sc-AxjAm:nth-child(3) > .ScCoreButton-sc-1qn4ixc-0 > .ScCoreButtonLabel-lh1yxp-0 > .sc-AxjAm')
await page.waitForSelector('#FunCaptcha-Token')
//request 2captcha funcaptcha
await https.get(`https://2captcha.com/in.php?key=${api}&method=funcaptcha&publickey=${pk}&surl=${surl}&pageurl=https://dashboard.twitch.tv/u/${username}/settings/stream`, (response) => {
var data = '';
response.on('data', (chunk) => {
data += chunk;
})
response.on('end', () => {
setTimeout(() => {getCaptcha(data)}, 20000)
})
})
.on('error', (e) => {
console.log(e);
})
await page.waitForSelector('.sc-AxjAm:nth-child(2) > .sc-AxjAm > .ScCoreButton-sc-1qn4ixc-0 > .ScCoreButtonLabel-lh1yxp-0 > .sc-AxjAm', {visible:true,timeout:0})
var streamkey = await page.evaluate(() => document.querySelector('.sc-AxjAm:nth-child(2) > .sc-AxjAm > .ScCoreButton-sc-1qn4ixc-0 > .ScCoreButtonLabel-lh1yxp-0 > .sc-AxjAm').innerHTML);
await navigationPromise
await fs.appendFile('userkeys.txt', `${username}:${streamkey}`, e => console.log(`error: ${e}`))
}
console.log(`Getting keys.`);
(async () => {
for(i in usernames) {
await getKey(usernames[i]);
}
})();
//https://dashboard.twitch.tv/u/stokeshester/settings/stream trying to login as soon as I enter this link
Here is an example of my code logging the "tc-token" value for the funcaptcha:
Here is what the captcha interface looks like (no button):

Related

Telegram bot won't respond even doe webhook is set

The code for the bot is currently hosted on a Cloudflare worker, and there are no errors being reported from that end. Additionally, upon investigation of the Botfather side, everything seems to be functioning normally as well. However, despite attempting various solutions such as changing bots, tokens, and chat groups, the issue remains.
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const { pathname } = new URL(request.url)
if (pathname === '/') {
return new Response('Hello! This is a Telegram bot. Send me a message.')
}
const { text } = await request.json()
if (text.startsWith('/start')) {
return new Response('Welcome to the bot! Use the /help command to see available options.')
} else if (text.startsWith('/help')) {
return new Response('Available commands:\n/scrape - scrapes videos from the specified website and sends them to the Telegram chat.\n/<example> - scrapes posts from the specified website and sends them to the Telegram chat.')
} else if (text.startsWith('/scrape')) {
const videoUrl = await scrapeVideoUrl('<example url>')
const message = `Here's the latest video: ${videoUrl}`
await sendMessageToChatId(message)
return new Response('OK')
} else if (text.startsWith('/<example>')) {
const post = await scrapePost('<example url>')
const message = `Here's the latest post: ${post.title}\n${post.url}`
await sendMessageToChatId(message)
return new Response('OK')
} else {
return new Response('Invalid command. Use the /help command to see available options.')
}
}
async function scrapeVideoUrl(url) {
const response = await fetch(url)
const html = await response.text()
const cheerio = require('cheerio')
const $ = cheerio.load(html)
const videoUrl = $('div.media > a').attr('href')
return videoUrl
}
async function scrapePost(url) {
const response = await fetch(url)
const html = await response.text()
const cheerio = require('cheerio')
const $ = cheerio.load(html)
const post = {
title: $('div.content h1').text(),
url: url
}
return post
}
async function sendMessageToChatId(message) {
const telegramApiUrl = "https://api.telegram.org/bot<token>/sendMessage";
const chatId = "<id>";
const response = await fetch(telegramApiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
chat_id: chatId,
text: message
})
})
}
There is zero response on the telegram side, even doe on web side everything seems to be fine.

Can i dispatch many actions in getServersideprops?

In my social media app in Home page i want to dispatch 3 actions from my api:
posts , users , userDetails
But this may show an error(500) on vercel because the request takes a lot of time to get all these data.
vercel Error
this error will not appear again after refreshing the page !!!
i think that's because the request takes a lot of time to get all the data.
-> getServersideProps Code
export const getServerSideProps = wrapper.getServerSideProps(
store => async (context) =>
{
const {req} = context
const session = await getSession({ req });
await store.dispatch(fetchPostsAction());
await store.dispatch(fetchUsersAction(4));
await store.dispatch(LoggedInUserAction({email:session.user.email}));
})
-> fetchPostsAction Code
"post/list",
async (_, { rejectWithValue, getState, dispatch }) => {
try
{
let link = `${URL}/api/posts`;
const { data } = await axios.get(link,{
headers: { "Accept-Encoding": "gzip,deflate,compress" }
});
console.log("#2 got the data",data)
return data;
} catch (error) {
if (!error?.response) throw error;
return rejectWithValue(error?.response?.data);
}
}
);
-> extraReducer Code
builder.addCase(createpostAction.pending, (state, action) => {
state.createPostLoading = true;
});
builder.addCase(createpostAction.fulfilled, (state, action) => {
state.postLists = [...state.postLists, action.payload.post].sort((a, b) => b.createdAt > a.createdAt ? 1 : -1)
state.createPostLoading = false;
state.isCreated = true;
state.appErr = null;
state.serverErr = null;
});
builder.addCase(createpostAction.rejected, (state, action) => {
state.createPostLoading = false;
state.appErr =
action?.payload?.message || action?.payload?.error?.message;
state.serverErr = action?.error?.message;
});
-> get posts from api Code
handler.get(async (req, res) =>
{
await db.connect();
try {
const posts = await Post.find().populate({
path: 'user',
model: 'User',
}).populate({
path:'comments',
options: {sort: {'createdAt' : -1} }
}).sort('-createdAt')
res.status(200).json({
success:true,
posts
});
} catch (err) {
res.status(500).json(err.message)
}
await db.disconnect();
})
so what is the best way to fetch all these data in next js ?
I hope there is a way to solve this problem

waitForResponse does not work in the cluster.tasks callback

I need to open 20 pages parallelly and click on a button then wait for a response after that get the data from a tag. and my code is:
async function getPageData(links) {
return new Promise(async (resolve, reject) => {
try {
const cluster = await Cluster.launch({
concurrency: Cluster.CONCURRENCY_PAGE,
maxConcurrency: 200,
monitor: true,
});
let allData = [];
await cluster.task(async function getData({ page, data: url }) {
await page.goto(url, {
waitUntil: 'networkidle2',
});
const buttonQuery = 'button[role=tab]:first-child';
const buttonElement = await page.waitForSelector(buttonQuery);
await buttonElement.click(buttonElement);
await page.waitForResponse('https://XXX'); // the problem is here
const data = await page.evaluate(getList);
const [oscillators, summary, movingAverage] = data;
allData.push({ oscillators, summary, movingAverage });
});
links.map(async function addQueue(link) {
cluster.queue(link);
});
await cluster.idle();
await cluster.close();
resolve(allData);
} catch (e) {
reject(e);
}
});
but it just work for the first time and ignore the rest of the tasks. but when I remove page.waitForResponse() the all tasks will be run as expected.
How can I make all tasks wait for their response then extract the data?

FCM very slow and unreliable when sending to a group of recipients through Cloud Function

I have the following Function that:
Listens for document (text message) creation
Grab IDs of members of a group chat
Get the FCM Tokens for each member
With a for-loop, send messages to group members
exports.sendChatMessage = functions.firestore
.document("chats/{mealID}/messages/{messageID}")
.onCreate((snap, context) => {
const data = snap.data();
const mealID = context.params.mealID;
const senderID = data.senderID;
const senderName = data.senderName;
const messageContent = data.content;
var docRef = db.collection("chats").doc(mealID);
docRef
.get()
.then((doc) => {
if (doc.exists) {
const docData = doc.data();
const mealName = docData.name;
const userStatus = docData.userStatus;
var users = docData.to;
var eligibleUsers = users.filter(
(user) => userStatus[user] == "accepted"
);
eligibleUsers.push(docData.from);
// get fcmTokens from eligibleUsers and send the messagme
db.collection("users")
.where("uid", "in", eligibleUsers)
.get()
.then((snapshot) => {
var fcmTokens = [];
var thumbnailPicURL = "";
// get thumbnailpic of the sender and collect fcmTokens
snapshot.forEach((doc) => {
if (doc.data().uid == senderID) {
thumbnailPicURL =
doc.data().thumbnailPicURL == null
? "https://i.imgur.com/8wSudUk.png"
: doc.data().thumbnailPicURL;
} else {
fcmTokens.push(doc.data().fcmToken);
}
});
// send the message fcmTokens
fcmTokens.forEach((token) => {
if (token != "") {
const fcmMessage = {
message: {
token: token,
notification: {
title: mealName,
body: senderName + ": " + messageContent,
image: thumbnailPicURL,
},
apns: {
payload: {
aps: {
category: "MESSAGE_RECEIVED",
},
MEAL_ID: mealID,
},
},
},
};
tokenManger.sendFcmMessage(fcmMessage);
}
});
return true;
});
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
return false;
}
})
.catch((error) => {
console.log("Error getting document:", error);
return false;
});
return true;
});
My send function comes from a helper file that uses the HTTP V1 protocol to build the send-request:
const { google } = require("googleapis");
const https = require("https");
const MESSAGING_SCOPE = "https://www.googleapis.com/auth/firebase.messaging";
const SCOPES = [MESSAGING_SCOPE];
const PROJECT_ID = MY_PROJECT_ID;
const HOST = "fcm.googleapis.com";
const PATH = "/v1/projects/" + PROJECT_ID + "/messages:send";
exports.getAccessToken = () => {
return new Promise(function (resolve, reject) {
const key = require("./service-account.json");
var jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
SCOPES,
null
);
jwtClient.authorize(function (err, tokens) {
if (err) {
reject(err);
return;
}
resolve(tokens.access_token);
});
});
};
//send message
exports.sendFcmMessage = (fcmMessage) => {
this.getAccessToken().then(function (accessToken) {
var options = {
hostname: HOST,
path: PATH,
method: "POST",
headers: {
Authorization: "Bearer " + accessToken,
},
// … plus the body of your notification or data message
};
var request = https.request(options, function (resp) {
resp.setEncoding("utf8");
resp.on("data", function (data) {
console.log("Message sent to Firebase for delivery, response:");
console.log(data);
});
});
request.on("error", function (err) {
console.log("Unable to send message to Firebase");
console.log(err);
});
request.write(JSON.stringify(fcmMessage));
request.end();
});
};
It worked all fine in the emulator but once deployed, there're significant delays (~3 mins):
I also noticed that the console says the cloud function finishes execution BEFORE sendFcmMessage logs success messages.
I did some research online, it appears that it might have something to do with the usage of Promise but I wasn't sure if that's the sole reason or it has something to do with my for-loop.
The Problem
To summarize the issue, you are creating "floating promises" or starting other asynchronous tasks (like in sendFcmMessage) where you aren't returning a promise because they use callbacks instead.
In a deployed function, as soon as the function returns its result or the Promise chain resolves, all further actions should be treated as if they will never be executed as documented here. An "inactive" function might be terminated at any time, is severely throttled and any network calls you make (like setting data in database or calling out to FCM) may never be executed.
An indicator that you haven't properly chained the promises is when you see the function completion log message ("Function execution took...") before other messages you are logging. When you see this, you need to look at the code you are running and confirm whether you have any "floating promises" or are using callback-based APIs. Once you have changed the callback-based APIs to use promises and then made sure they are all chained together properly, you should see a significant boost in performance.
The fixes
Sending the message data to FCM
In your tokenManger file, getAccessToken() could be reworked slightly and sendFcmMessage should be converted to return a Promise:
exports.getAccessToken = () => {
return new Promise(function (resolve, reject) {
const key = require("./service-account.json");
const jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
SCOPES,
null
);
jwtClient.authorize(
(err, tokens) => err ? reject(err) : resolve(tokens.access_token)
);
});
};
//send message
exports.sendFcmMessage = (fcmMessage) => {
// CHANGED: return the Promise
return this.getAccessToken().then(function (accessToken) {
const options = {
hostname: HOST,
path: PATH,
method: "POST",
headers: {
Authorization: "Bearer " + accessToken,
},
// … plus the body of your notification or data message
};
// CHANGED: convert to Promise:
return new Promise((resolve, reject) => {
const request = https.request(options, (resp) => {
resp.setEncoding("utf8");
resp.on("data", resolve);
resp.on("error", reject);
});
request.on("error", reject);
request.write(JSON.stringify(fcmMessage));
request.end();
});
});
};
However, the above code was built for googleapis ^52.1.0 and google-auth-library ^6.0.3. The modern versions of these modules are v92.0.0 and v7.11.0 respectively. This means you should really update the code to use these later versions like so:
// Import JWT module directly
const { JWT } = require('google-auth-library');
// FIREBASE_CONFIG is a JSON string available in Cloud Functions
const PROJECT_ID = JSON.parse(process.env.FIREBASE_CONFIG).projectId;
const FCM_ENDPOINT = `https://fcm.googleapis.com/v1/projects/${PROJECT_ID}/messages:send`;
const FCM_SCOPES = ["https://www.googleapis.com/auth/firebase.messaging"];
exports.sendFcmMessage = (fcmMessage) => {
const key = require("./service-account.json"); // consider moving outside of function (so it throws an error during deployment if its missing)
const client = new JWT({
email: key.client_email,
key: key.private_key,
scopes: FCM_SCOPES
});
return client.request({ // <-- this uses `gaxios`, Google's fork of `axios` built for Promise-based APIs
url: FCM_ENDPOINT,
method: "POST",
data: fcmMessage
});
}
Better yet, just use the messaging APIs provided by the Firebase Admin SDKs that handle the details for you. Just feed it the message and tokens as needed.
import { initializeApp } from "firebase-admin/app";
import { getMessaging } from "firebase-admin/messaging";
initializeApp(); // initializes using default credentials provided by Cloud Functions
const fcm = getMessaging();
fcm.send(message) // send to one (uses the given token)
fcm.sendAll(messagesArr) // send to many at once (each message uses the given token)
fcm.sendMulticast(message) // send to many at once (uses a `tokens` array instead of `token`)
The Cloud Function
Updating the main Cloud Function, you'd get:
exports.sendChatMessage = functions.firestore
.document("chats/{mealID}/messages/{messageID}")
.onCreate((snap, context) => {
const mealID = context.params.mealID;
const { senderID, senderName, content: messageContent } = snap.data();
const docRef = db.collection("chats").doc(mealID);
/* --> */ return docRef
.get()
.then((doc) => {
if (!doc.exists) { // CHANGED: Fail fast and avoid else statements
console.log(`Could not find "chat:${mealID}"!`);
return false;
}
const { userStatus, to: users, name: mealName, from: fromUser } = doc.data();
const eligibleUsers = users.filter(
(user) => userStatus[user] == "accepted"
);
eligibleUsers.push(fromUser);
// get fcmTokens from eligibleUsers and send the message
/* --> */ return db.collection("users")
.where("uid", "in", eligibleUsers) // WARNING: This will only work for up to 10 users! You'll need to break it up into chunks of 10 if there are more.
.get()
.then(async (snapshot) => {
const fcmTokens = [];
let thumbnailPicURL = "";
// get thumbnailpic of the sender and collect fcmTokens
snapshot.forEach((doc) => {
if (doc.get("uid") == senderID) {
thumbnailPicURL = doc.get("thumbnailPicURL"); // update with given thumbnail pic
} else {
fcmTokens.push(doc.get("fcmToken"));
}
});
const baseMessage = {
notification: {
title: mealName,
body: senderName + ": " + messageContent,
image: thumbnailPicURL || "https://i.imgur.com/8wSudUk.png", // CHANGED: specified fallback image here
},
apns: {
payload: {
aps: {
category: "MESSAGE_RECEIVED",
},
MEAL_ID: mealID,
},
}
}
// log error if fcmTokens empty?
// ----- OPTION 1 -----
// send the message to each fcmToken
const messagePromises = fcmTokens.map((token) => {
if (!token) // handle "" and undefined
return; // skip
/* --> */ return tokenManger
.sendFcmMessage({
message: { ...baseMessage, token }
})
.catch((err) => { // catch the error here, so as many notifications are sent out as possible
console.error(`Failed to send message to "fcm:${token}"`, err);
})
});
await Promise.all(messagePromises); // wait for all messages to be sent out
// --------------------
// ----- OPTION 2 -----
// send the message to each fcmToken
await getMessaging().sendAll(
fcmTokens.map((token) => ({ ...baseMessage, token }))
);
// --------------------
return true;
})
.catch((error) => {
console.log("Error sending messages:", error);
return false;
});
})
.catch((error) => {
console.log("Error getting document:", error);
return false;
});
});
I found out that the culprit is my queries to db. Like #samthecodingman commented, I was creating floating Promises.
Originally, I have codes like:
db.collection("users")
.where("uid", "in", eligibleUsers)
.get()
.then((snapshot) => {...}
All I needed to do is to return that call:
return db.collection("users")
.where("uid", "in", eligibleUsers)
.get()
.then((snapshot) => {...}
Although it's still not instant delivery, it's much faster now.

Firebase: Flutter: Optimizing for stability: many Transactions vs. Batch writes vs. Set calls

A few months ago, a glorious soul here taught me about transactions. I may have gone a little overboard thinking they were they best thing since sliced bread. The problem they solved was obvious, guaranteed concurrent writes on a single doc. However, I've noticed already with as little as three closely timed function triggers that I produced the dreaded: ------------------------"10 ABORTED: Too much contention on these documents."...-------------------------
Optimizing for stability, my question is: Would it be best practice to use a mixed bag of these write calls for different situations? For example: if a cloud function is writing to a location where I do not expect contention, should it just be a set call? Instead of 4 transactions to various locations, should I use a batch?
Reading the Firebase limitations I assumed I was in the clear with max 60w/doc/sec. However, I've learned now that Transactions can timeout AND only try to write 5 times.
Some background on the app and the contention error:
- It's a basic social media app.
- The contention error came from making three posts in close succession from a single user.
- Each post triggers a cloud function that does several transactions to link the post to appropriate places. i.e. followers, feed, groups, sends notifications, and sets activity feed docs for each follower.
Side question: Am I wrongly understanding that firebase can handle an app with this level of activity?
EDIT: I was aware of these firebase limitations early on and did my best work to keep documents and collections spread apart appropriately.
CODE EDIT: index.js: adminPostReview is the specific function to throw the error (did the best I could to simplify).
The specific transaction to throw the error, I believe, is the call to transactionDayIndexAdd().
function transactionDelete(docRef) {
return db.runTransaction(async t => {
var doc = await t.get(docRef);
if (doc.exists)
t.delete(docRef);
})
}
// THIS FUNCTION. Is it bad to read and set two documents?
function transactionDayIndexAdd(docRef, dayPosted, postId, userId) {
return db.runTransaction(async (t) => {
var postMap = {};
const doc = await t.get(docRef.doc(dayPosted));
if (doc.exists) {
postMap = doc.data().pids;
} else {
const indexDoc = await t.get(docRef.doc('index'));
var newIndex = indexDoc.exists ? indexDoc.data().index : {};
newIndex[dayPosted] = true;
t.set(docRef.doc('index'), { 'index': newIndex });
}
postMap[postId] = userId;
t.set(doc.ref, { 'pids': postMap });
})
}
exports.adminPostReview = functions.firestore
.document('/adminPostReview/{postId}')
.onUpdate(async (change, context) => {
const postId = context.params.postId;
const userId = change.before.data().ownerId;
const approvedMaks = change.after.data().approvedMaks;
const approvedRita = change.after.data().approvedRita;
var promises = [];
if (approvedMaks == false || approvedRita == false) {
promises.push(transactionDelete(db.collection('posts').doc(userId).collection('userPosts').doc(postId)));
}
else if (approvedMaks == true || approvedRita == true) {
var newPost = change.after.data();
promises.push(postLive(newPost));
}
if (approvedMaks != null || approvedRita != null) {
promises.push(transactionDelete(db.collection('activityFeed').doc(MAKS_ID).collection('feedItems').doc(`${postId}_review`)));
promises.push(transactionDelete(db.collection('activityFeed').doc(RITA_ID).collection('feedItems').doc(`${postId}_review`)));
}
});
async function postLive(newPost) {
const userId = newPost.ownerId;
const postId = newPost.postId;
const dayPosted = newPost.dayPosted;
var postToFeed = newPost.postToFeed;
var postToGroups = newPost.postToGroups;
newPost.approved = true;
delete newPost.postToFeed;
delete newPost.postToGroups;
var batch = db.batch();
var promises = [];
if (postToFeed == true) {
batch.set(
db.collection('posts').doc(userId).collection('userPosts').doc(postId),
newPost
);
batch.update(
db.collection('userActivity').doc(userId),
'numPosts',
admin.firestore.FieldValue.increment(1),
)
promises.push(batch.commit());
promises.push(transactionDayIndexAdd(db.collection("feedRandom"), dayPosted, postId, userId));
var querySnap = await db.collection('followers')
.doc(userId)
.collection('userFollowers')
.get();
querySnap.docs.forEach(async follower => {
promises.push(transactionDayIndexAdd(
db.collection('feedFollowing').doc(follower.id).collection('feedItems'),
dayPosted, postId, userId));
promises.push(transactionSet(db.collection('activityFeed').doc(follower.id)
.collection('feedItems').doc(postId),
{
media1Url: newPost.media1Url,
media2Url: newPost.media2Url,
postId: newPost.postId,
timestamp: newPost.timestamp,
type: 'newFollowingPost',
userId: userId,
userProfileImg: newPost.ownerProfileImg,
username: newPost.username,
displayName: newPost.displayName,
}
));
if (follower.data().notificationToken != null) {
const payload = {
notification: {
title: 'Someone you follow made a new post!',
body: `${newPost.username} has a new post.`
},
data: {
click_action: "FLUTTER_NOTIFICATION_CLICK",
vestiq_type: 'newFollowingPost',
vestiq_uid: follower.id,
vestiq_fid: userId,
vestiq_pid: postId,
vestiq_displayName: newPost.displayName,
vestiq_photoUrl: newPost.ownerProfileImg,
vestiq_username: newPost.username,
}
};
var user = await db.collection('users').doc(follower.id).get();
if (user.data().notificationOp3 == true)
promises.push(pushNotification(follower.data().notificationToken, payload));
}
});
if (postToGroups != null && postToGroups.length > 0) {
promises.push(pushGroupPosts(postToGroups, userId, postId, newPost));
return Promise.all(promises);
} else return Promise.all(promises);
}
else if (postToGroups != null && postToGroups.length > 0) {
promises.push(pushGroupPosts(postToGroups, userId, postId, newPost));
return Promise.all(promises);
}
}
async function pushGroupPosts(postToGroups, userId, postId, newPost) {
var groupBatch = db.batch();
postToGroups.forEach((gid) => {
groupBatch.set(
db.collection('groups').doc(gid).collection('posts').doc(postId),
newPost,
);
groupBatch.set(
db.collection('usersGroupPosts').doc(userId).collection(gid).doc(postId),
{ 'gid': gid, 'postId': postId },
);
});
return push(groupBatch.commit());
}
I was able to fix the contention problem by splitting transactionDayIndexAdd() into two separate transactions. I flip a bool to determine if the second should run instead.
This leads me to believe that the nested t.get/t.set transaction significantly increases chances for contention issues. Since the split, I have not been able to reproduce the error. Here is the new transactionDayIndexAdd() for those who are curious.
HOWEVER, my original question still stands regarding optimising for stability.
async function transactionDayIndexAdd(docRef, dayPosted, postId, userId) {
var dayAdd = 0;
var promises = [];
await db.runTransaction(async (t) => {
var postMap = {};
const doc = await t.get(docRef.doc(dayPosted));
if (doc.exists)
postMap = doc.data().pids;
else {
dayAdd = 1;
}
postMap[postId] = userId;
t.set(doc.ref, { 'pids': postMap });
});
if (dayAdd == 1) {
promises.push(db.runTransaction(async (t) => {
const indexDoc = await t.get(docRef.doc('index'));
var newIndex = indexDoc.exists ? indexDoc.data().index : {};
newIndex[dayPosted] = true;
t.set(indexDoc.ref, { 'index': newIndex });
}));
}
return await Promise.all(promises);
}

Resources