I am using following code to get game name from the Twitch page.
But it looks like 'chrome.scripting.executeScript' runs before page loading finished.
Sometimes it returns 'null', sometimes it returns the game name. Looks like race condition. In case of Twitch I see browser shows that it is finished loading, but probably angular/react loads more.
How to solve the problem?
chrome.tabs.onUpdated.addListener(async function (tabId, changeInfo, tab) {
if (changeInfo.status == 'complete') {
await chrome.scripting.executeScript({
target: { tabId: tabId },
function: getTwitchGameName},
(scriptResults) => { console.log('Game name: ' + scriptResults[0].result); });
}
});
function getTwitchGameName() {
return document.querySelector('a[data-a-target=\"stream-game-link\"] span')?.innerText;
}
Related
I'm working on an extension that is supposed to extract information from the DOM based specific classes/tags,etc, then allow the user to save the information as a CSV file.
I'm getting stuck on a couple of places and haven't been able to find answers to questions similar enough.
Where I am tripped up at is:
1) Making sure that the page has completely loaded so the chrome.tabs.query doesn't return null a couple of times before the promise actually succeeds and allows the blocksF to successfully inject. I have tried placing it within a settimeout function but the chrome api doesn't seem to work within such the function.
2) Saving the extracted information so when the user moves onto a new page, the information is still there. I'm not sure if I should use the chrome.storage api call or simply save the information as an array and keep passing it through. It's just text, so I don't believe that it should take up too much space.
Then main function of the background.js is below.
let mainfunc = chrome.tabs.onUpdated.addListener(
async(id, tab) => {
if (buttonOn == true) {
let actTab = await chrome.tabs.query({
active: true,
currentWindow: true,
status: "complete"
}).catch(console.log(console.error()));
if (!actTab) {
console.log("Could not get URL. Turn extension off and on again.");
} else {
console.log("Tab information recieved.")
};
console.log(actTab);
let blocksF = chrome.scripting.executeScript({
target: { tabId: actTab[0]['id'] },
func: createBlocks
})
.catch(console.error)
if (!blocksF) {
console.log("Something went wrong.")
} else {
console.log("Buttons have been created.")
};
/*
Adds listeners and should return value of the works array if the user chose to get the information
*/
let listenersF = chrome.scripting.executeScript({
target: { tabId: actTab[0]['id'] },
func: loadListeners
})
.catch(console.error)
if (!listenersF) {
console.log("Listeners failed to load.")
} else {
console.log("Listeners loaded successfully.")
};
console.log(listenersF)
};
});
Information from the DOM is extracted through an event listener on a div/button that is added. The event listener is added within the loadListeners function.
let workArr = document.getElementById("getInfo").addEventListener("click", () => {
let domAr = Array.from(
document.querySelectorAll(<class 1>, <class 2>),
el => {
return el.textContent
}
);
let newAr = []
for (let i = 0; i < domAr.length; i++) {
if (i % 2 == 0) {
newAr.push([domAr[i], domAr[i + 1]])
}
}
newAr.forEach((work, i) => {
let table = document.getElementById('extTable');
let row = document.createElement("tr");
row.appendChild(document.createElement("td")).textContent = work[0];
row.appendChild(document.createElement("td")).textContent = work[1];
table.appendChild(row);
});
return newAr
I've been stuck on this for a couple of weeks now. Any help would be appreciated. Thank you!
There are several issues.
chrome methods return a Promise in MV3 so you need to await it or chain on it via then.
tabs.onUpdated listener's parameters are different. The second one is a change info which you can check for status instead of polling the active tab, moreover the update may happen while the tab is inactive.
catch(console.log(console.error())) doesn't do anything useful because it immediately calls these two functions so it's equivalent to catch(undefined)
Using return newArr inside a DOM event listener doesn't do anything useful because the caller of this listener is the internal DOM event dispatcher which doesn't use the returned value. Instead, your injected func should return a Promise and call resolve inside the listener when done. This requires Chrome 98 which added support for resolving Promise returned by the injected function.
chrome.tabs.onUpdated.addListener(onTabUpdated);
async function onTabUpdated(tabId, info, tab) {
if (info.status === 'complete' &&
/^https?:\/\/(www\.)?example\.com\//.test(tab.url) &&
await exec(tabId, createBlocks)) {
const [{result}] = await exec(tabId, loadListeners);
console.log(result);
// here you can save it in chrome.storage if necessary
}
}
function exec(tabId, func) {
// console.error returns `undefined` so we don't need try/catch,
// because executeScript is always an array of objects on success
return chrome.scripting.executeScript({target: {tabId}, func})
.catch(console.error);
}
function loadListeners() {
return new Promise(resolve => {
document.getElementById('getInfo').addEventListener('click', () => {
const result = [];
// ...add items to result
resolve(result);
});
});
}
I have a chatbot that will eventually be deployed on multiple websites, and there are a number or variables that need to change based on the site (e.g. language, QnA Database, Dialog, etc.). I'd like to do this with a single bot, and just pass a variable so that it knows which page it is being rendered on (for a simple example, let's assume country pages: us, fr, de, etc.). I have been unsuccessful in passing this information to the bot.
Ideally this would be before the welcome message fires, but I can't even get it to send at all. I have a custom store set up:
const store = window.WebChat.createStore({}, function(dispatch) { return function(next) { return function(action) {
if (action.type === 'WEB_CHAT/SEND_MESSAGE') {
// Message sent by the user
PageTitleNotification.Off();
clearTimeout(interval);
} else if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY' && action.payload.activity.name !== "inactive") {
// Message sent by the bot
clearInterval(interval);
interval = setTimeout(function() {
// Change title to flash the page
PageTitleNotification.On('Are you still there?');
// Notify bot the user has been inactive
dispatch.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'inactive',
value: ''
}
});
}, 300000)
}
return next(action);
}}});
But for my use case I don't think what's in there actually matters, only that it is defined. The functions here just 1) clear an interval when the user sends a message and 2) set a new interval and send an inactivity message to the bot.
I also have a send message activity that is on a button click for a transcript. It looks like this:
document.querySelector('#transcriptButton').addEventListener('click', function() {
return store.dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: { text: 'Email me a transcript' }
});
/*return store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'siteContext',
value: 'eatonchatbot indexBackup.html'
}
});*/
});
This sends a "front channel" message (that I can see in the bot) to request a transcript, which kicks off a dialog. That works. The commented out section alludes to what I'm trying to do. I have a separate dispatch statement as shown below, which has the exact same SEND_EVENT code as is commented out above. The SEND_EVENT does work as expected when it keys off the button click.
Here is the additional code I added. This is the piece that is NOT working. What I want is, when the bot has been rendered (but ideally before the welcome message), send this siteContext event to the bot so that I know where the bot is being rendered. I do not get any activity in the bot with this code. I also tried replacing it with SEND_MESSAGE instead of SEND_EVENT in a sort of reverse test from above, but that didn't work either.
// Test setting site context
store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'siteContext',
value: 'eatonchatbot indexBackup.html'
}
});
/*store.dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: {
text: 'eatonchatbot indexBackup.html'
}
});*/
It just occurred to me that this statement is probably running before the bot is rendered. So I put it in an interval and this DOES work. However, it does not fire the message until after the welcome message has been sent.
setTimeout(function() {
store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'siteContext',
value: 'eatonchatbot indexBackup.html'
}
});
}, 5000);
So this kind of works, but if this siteContext value was needed to determine the language of the welcome message, this would obviously fail. So my main ask here is, is there a better way to try to pass in a siteContext value like this, or is there some way to ensure that the context is received and can be used by the bot before the welcome message fires? I do see that there is a locale setting in the renderWebChat method, but I can't figure out if and how I could access that in the bot, and besides it may not be granular enough depending on the business need. But it seems if I could send some sort of value in that renderWebChat object, that might avoid all of the other crazy stuff I'm trying to do.
With some help from #Hessel and this issue I found on GitHub, I was able to come up with a solution. Just setting the values being passed in via onEvent (which I am now using in place of onTurn to reduce an if statement) isn't good enough if you need to alter content in the welcome message (e.g. language, user name, or an altogether different message). The onMembersAdded still fires before the values can be set, at least if you're setting them in userState. The key is to set up separate welcome messages in onEvent for directline and onMembersAdded for all other channels (I didn't include webchat as in the example as I'm not sending any event for that channel).
Here is the onEvent function I used:
this.onEvent(async (context, next) => {
// Check for inactivity
if (context.activity.name && context.activity.name === 'inactive') {
await context.sendActivity({
text: 'Are you still there? Is there anything else I can help you with?',
name: 'inactive'
});
}
// Check for webchat/join event (directline conversation initiation)
if (context.activity.name && context.activity.name === 'webchat/join') {
const userData = await this.userDialogStateAccessor.get(context, {});
userData.siteContext = context.activity.value;
// Debug
console.log(`The current language is: ${userData.siteContext.language}`);
console.log(`The current page is: ${userData.siteContext.page}`);
//await context.sendActivity(`The current language is: ${userData.siteContext.language}`);
//await context.sendActivity(`The current page is: ${userData.siteContext.page}`);
if (!userData.accountNumber) {
const dc = await this.dialogs.createContext(context);
await dc.beginDialog(AUTH_DIALOG);
await this.conversationState.saveChanges(context);
await this.userState.saveChanges(context);
} else {
if (context.activity.channelId == 'msteams') {
var welcomeCard = CardHelper.GetMenuCardTeams(welcomeMessage,'Y','Y');
} else {
var welcomeCard = CardHelper.GetMenuCard(welcomeMessage,'Y','Y');
}
await context.sendActivity(welcomeCard);
this.appInsightsClient.trackEvent({name:'conversationStart', properties:{accountNumber:userData.accountNumber}});
}
await this.userState.saveChanges(context);
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
The event I used in the custom store is pretty much the same as above, except I updated it to pull in the most preferred language and current url (was hard coded above).
store = window.WebChat.createStore({}, function (dispatch) {
return function (next) {
return function (action) {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
dispatch.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/join',
value: {
language: navigator.languages[0],
page: window.location.href
}
}
});
}
return next(action);
};
};
});
If you have your renderWebChat method inside a function that you can call so that your bot doesn't automatically start (I have a floating icon that causes the bot to load onclick) this should go outside that function.
I have had recent reports of a chrome extension that I develop that stops working after an update or a fresh install. The background script seems to not start at all.
There is no response to messages sent to it from the content scripts.
There is no process for it in the task manager.
Opening background page from chrome://extensions does not show any activity in the console, or show any source files.
Profiling, memory snapshot buttons are disabled.
Once this issue appears, it persists for the chrome profile even after reloading or uninstalling/reinstalling the extension.
Restarting chrome resolves the problem.
The issue has been seen on chrome v79. But I cannot say for sure that it is exclusive to this version, as the issue is difficult to reproduce and seemingly random.
Has anyone seen such an issue, or has any ideas what to look for? I am happy to update my question with any new info I have or with any info you need.
Edit:
Here is my webNavigation listener, which is used to inject content scripts. This handler is wired up in the 'root' context of the background script (not asynchronously inside an event handler)
chrome.webNavigation.onCompleted.addListener((details) ⇒ {
if(details.frameId === 0) {
injectScript(
'js/contentScript.js',
details.tabId,
details.frameId,
details.url
).catch((e) ⇒ {});
}
}
The injectScript function is as follows
export const injectScript = ƒ (scriptPath,tab,frame,tabUrl) {
return new Promise((res,rej) ⇒ {
let options = {
file : scriptPath,
allFrames : false,
frameId : frame,
matchAboutBlank: false,
runAt : 'document_idle',
};
const cb = ƒ () {
if (chrome.runtime.lastError) {
let err = new Error('Could not inject script');
capture(err,{
...options,
tabUrl,
lastError : chrome.runtime.lastError.message,
});
rej(err);
}else{
res();
}
};
if (tabUrl.indexOf('.salesforce.com') !== -1) {
window.setTimeout(() => {
chrome.tabs.executeScript(tab,options,cb);
},500);
}else{
chrome.tabs.executeScript(tab,options,cb);
}
});
};
Note above, the capture function reports the error to a backend and I cannot see it being reported there as well. Cannot add a breakpoint in code because no source appears in the background page, as noted above.
A background service worker is loaded when it is needed, and unloaded when it goes idle.
https://developer.chrome.com/docs/extensions/mv3/service_workers/
You can use the following methods:
// Keep heartbeat
let heartTimer;
const keepAlive = () => {
heartTimer && clearTimeout(heartTimer);
heartTimer = setTimeout(() => {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
console.info('[heartbeat]')
tabs.length && chrome.tabs.sendMessage(
tabs[0].id,
{ action: "heartbeat" }
);
});
keepAlive();
}, 10000);
};
keepAlive();
I'm writing a chrome extension that consists of a background process and a popup. The background process continually makes ajax requests to get new data, and the popup displays the info.
In an extremely simplified way, the background.js process looks like this:
var Background = {
data: {},
poll: function() {
$.ajax({
url: 'some_url',
success: function(data) {
this.data = data;
chrome.extension.sendMessage('update');
}.bind(this)
});
},
startPolling: function() {
setInterval(this.poll.bind(this), 10000);
}
};
$(Background.startPolling.bind(Background));
This shoots of an ajax request every 10 seconds, gets some data back, and sets it to Background.data. Again, this is very simplified. You can see the sendMessage call in the success callback.
My popup.js looks something like this, also extremely simplified:
var Background = chrome.extension.getBackgroundPage().Background;
angular.module('App', [])
.controller('PopupCtrl', function PopupCtrl($scope) {
$scope.data = Background.data;
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
if (request == 'update') {
console.log('update');
$scope.$apply();
}
});
});
And of course, popup.html looks something like:
<html ng-app="App" ng-csp>
<body ng-controller="PopupCtrl">
<p>{{data.title}}</p>
<p>{{data.body}}</p>
<p>{{data.date}}</p>
</body>
</html>
The message is definitely received, because I can see "update" print to the console every 10 second interval. I also know that the data is being updated properly because if I close and reopen the popup, the new data is displayed correctly. So $scope.$apply() is simply not doing anything.
How can I fix this?
I have a popup, call 'welcome.html', the thing I would like to do is when the user select a text, and click my plugin, it will use some of the page information, and print back to the welcome.html. For example, the web site title, and the text which the user selected and the url. But how can I pass value to that welcome.html? Thank you.
I do a lot of this in my extension as it mines a lot of data enabling the user to easily copy it to their clipboard.
Since you're looking for a lot less data it's even simpler. When your popup is being loaded you can call the following function to retrieve the information you require;
function getData(callback) {
chrome.tabs.getSelected(null, function (tab) {
var data = {
selection: '',
title: tab.title,
url: tab.url
};
/*
* We can't call content scripts on some pages and the process will get
* stuck if we try.
*/
if (tab.url.indexOf('chrome') === 0 ||
tab.url.indexOf('https://chrome.google.com/webstore') === 0) {
callback(data);
} else {
chrome.tabs.sendRequest(tab.id, {}, function (response) {
data.selection = response.selection;
callback(data);
});
}
});
}
Ensure you pass in a callback function which will be called once all the data has been extracted;
getData(function (data) {
console.log('Title: ' + data.title);
console.log('URL: ' + data.url);
console.log('Selected Text: ' + data.selection);
// Display the data instead
});
As you may have noticed the getData function sends a request to the selected tab. A content script will need to be injected in to the page in order for this to work so be sure you've configured your manifest correctly or injected it programmatically prior to calling getData or it won't work. The script that will need to be injected should resemble the following;
(function () {
chrome.extension.onRequest.addListener(function (request, sender,
sendResponse) {
sendResponse({
selection: window.getSelection().toString()
});
});
})();
This simply returns the currently selected text. One concern is that this data look-up could potentially cause a slight pause while the popup is rendered but you should test this yourself and experiment with it but there are solutions.
This should cover all you need to know so good luck and let me know if you need any further help as I'm aware this could be slightly overwhelming if you're new to Chrome extensions.