metastream
This commit is contained in:
parent
8c201a0b49
commit
4b28e8a841
5
browser.js
Normal file
5
browser.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = require("puppeteer").launch({headless:false, args: [
|
||||
`--load-extension=` + require("path").join(process.cwd(), `resources/metastream-extension-0.4.0_0`)
|
||||
], ignoreDefaultArgs: [
|
||||
"--disable-extensions"
|
||||
]})
|
@ -4,7 +4,7 @@ let { Command } = require("discord-akairo")
|
||||
module.exports = class extends Command {
|
||||
constructor() {
|
||||
super("help", {
|
||||
aliases: ["help", "?"],
|
||||
aliases: ["help", "commands"],
|
||||
description: "Help"
|
||||
})
|
||||
}
|
||||
|
36
commands/metastream.js
Normal file
36
commands/metastream.js
Normal file
@ -0,0 +1,36 @@
|
||||
let metastreamUrl;
|
||||
module.exports = class extends Akairo.Command {
|
||||
constructor() {
|
||||
super("metastream", {
|
||||
aliases: ["metastream"],
|
||||
description: "Creates a new Metastream session",
|
||||
typing: true
|
||||
})
|
||||
}
|
||||
async exec(message, args) {
|
||||
if (metastreamUrl) return await message.channel.send(metastreamUrl)
|
||||
await message.channel.send("Creating a Metastream session, just a moment...")
|
||||
let browser = await require.main.require("./browser")
|
||||
let page = await browser.newPage()
|
||||
await page.goto("https://app.getmetastream.com")
|
||||
let usernameInput = await page.waitForSelector("#profile_username")
|
||||
await usernameInput.type("Melony")
|
||||
await page.keyboard.press("Enter")
|
||||
let startSession = await page.waitForSelector(`a[href^="/join/"]`)
|
||||
await startSession.click()
|
||||
/*setInterval(async () => {
|
||||
let allowButton = await page.$('[title="Allow"]')
|
||||
if (allowButton) await allowButton.click()
|
||||
}, 2000)*/
|
||||
try {
|
||||
await (await page.waitForSelector(`[title="Settings"]`)).click()
|
||||
await (await page.$$(`[class^="UserAvatar__image"]`)).pop().click()
|
||||
await (await page.$x("//button[contains(text(), 'Session')]"))[0].click()
|
||||
await (await page.$x(`//span[contains(text(), 'Public')]`))[0].click()
|
||||
await (await page.$x(`//button[contains(text(), 'Advanced')]`))[0].click()
|
||||
await (await page.$(`label[for="safebrowse"]`)).click()
|
||||
await (await page.$(`button[class^="Modal__close"]`)).click()
|
||||
} catch(e) { console.error(e.stack) }
|
||||
await message.channel.send(`Here you go:\n${metastreamUrl = page.url()}`)
|
||||
}
|
||||
}
|
@ -1,7 +1,4 @@
|
||||
let { Command } = require('discord-akairo');
|
||||
let puppeteer = require("puppeteer");
|
||||
|
||||
let browser;
|
||||
|
||||
module.exports = class extends Command {
|
||||
constructor() {
|
||||
@ -11,20 +8,20 @@ module.exports = class extends Command {
|
||||
id: "url",
|
||||
match: "content"
|
||||
}],
|
||||
description: "Screenshots of the provided website."
|
||||
description: "Screenshots of the provided website.",
|
||||
typing: true
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message, args) {
|
||||
message.react('🆗');
|
||||
if (!browser) {
|
||||
browser = await puppeteer.launch();
|
||||
}
|
||||
await message.react('🆗');
|
||||
let browser = await require.main.require("./browser")
|
||||
let page = await browser.newPage();
|
||||
let url = args.url;
|
||||
if (!(url.startsWith("http://") || url.startsWith("https://"))) url = "http://" + url;
|
||||
await page.goto(url);
|
||||
let screenshot = await page.screenshot({type: 'png'});
|
||||
await page.close();
|
||||
await message.channel.send({files:[{ attachment: screenshot, name: "screenshot.png" }]});
|
||||
}
|
||||
}
|
49
resources/metastream-extension-0.4.0_0/app.js
Normal file
49
resources/metastream-extension-0.4.0_0/app.js
Normal file
@ -0,0 +1,49 @@
|
||||
'use strict'
|
||||
|
||||
//
|
||||
// The app script handles bidirectional communication with the background
|
||||
// script from the Metastream application.
|
||||
//
|
||||
;(function app() {
|
||||
const isInstalled = typeof document.documentElement.dataset.extensionInstalled !== 'undefined'
|
||||
if (isInstalled) {
|
||||
console.warn(`Metastream already initialized, is the extension installed twice?`)
|
||||
return
|
||||
}
|
||||
|
||||
if (window.self !== window.top) {
|
||||
console.warn('Metastream is unsupported within subframes.')
|
||||
return
|
||||
}
|
||||
|
||||
// Notify background script of initialization request
|
||||
chrome.runtime.sendMessage({ type: 'metastream-init' }, initialized => {
|
||||
document.documentElement.dataset.extensionInstalled = ''
|
||||
console.debug(`[Metastream Remote] Initialized`, initialized)
|
||||
})
|
||||
|
||||
// Listen for subframe events
|
||||
chrome.runtime.onMessage.addListener(message => {
|
||||
if (typeof message !== 'object' || typeof message.type !== 'string') return
|
||||
|
||||
if (message.type.startsWith('metastream-')) {
|
||||
console.debug('[Metastream Remote] Received message', message)
|
||||
|
||||
// Send to main world
|
||||
message.__internal = true
|
||||
window.postMessage(message, location.origin)
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for events to forward to background script
|
||||
window.addEventListener('message', event => {
|
||||
if (event.origin !== location.origin) return
|
||||
const { data: action } = event
|
||||
if (typeof action !== 'object' || typeof action.type !== 'string' || action.__internal) return
|
||||
|
||||
if (action.type.startsWith('metastream-')) {
|
||||
console.debug('[Metastream Remote] Forwarding message to background', action)
|
||||
chrome.runtime.sendMessage(action)
|
||||
}
|
||||
})
|
||||
})()
|
690
resources/metastream-extension-0.4.0_0/background.js
Normal file
690
resources/metastream-extension-0.4.0_0/background.js
Normal file
@ -0,0 +1,690 @@
|
||||
'use strict'
|
||||
|
||||
//
|
||||
// The background script provides monitoring of tabs for an active Metastream
|
||||
// webapp. When activated, web requests originating from the app are modified
|
||||
// to allow bypassing browser security limitations. This script also provides
|
||||
// message brokering between embedded websites and the app itself.
|
||||
//
|
||||
|
||||
//=============================================================================
|
||||
// Helpers
|
||||
//=============================================================================
|
||||
|
||||
const TOP_FRAME = 0
|
||||
const HEADER_PREFIX = 'x-metastream'
|
||||
const METASTREAM_APP_URL = 'https://app.getmetastream.com'
|
||||
const isMetastreamUrl = url =>
|
||||
url.startsWith(METASTREAM_APP_URL) ||
|
||||
url.startsWith('http://local.getmetastream.com') ||
|
||||
url.startsWith('http://localhost:8080') ||
|
||||
url.startsWith('https://localhost:8080')
|
||||
const isTopFrame = details => details.frameId === TOP_FRAME
|
||||
const isDirectChild = details => details.parentFrameId === TOP_FRAME
|
||||
const isValidAction = action => typeof action === 'object' && typeof action.type === 'string'
|
||||
const isFirefox = () => navigator.userAgent.toLowerCase().includes('firefox')
|
||||
|
||||
const asyncTimeout = (promise, timeout = 5000) => {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout))
|
||||
])
|
||||
}
|
||||
|
||||
const escapePattern = pattern => pattern.replace(/[\\^$+?.()|[\]{}]/g, '\\$&')
|
||||
|
||||
// Check whether pattern matches.
|
||||
// https://developer.chrome.com/extensions/match_patterns
|
||||
const matchesPattern = function(url, pattern) {
|
||||
if (pattern === '<all_urls>') return true
|
||||
const regexp = new RegExp(
|
||||
`^${pattern
|
||||
.split('*')
|
||||
.map(escapePattern)
|
||||
.join('.*')}$`
|
||||
)
|
||||
return url.match(regexp)
|
||||
}
|
||||
|
||||
// Memoized frame paths
|
||||
const framePaths = {}
|
||||
|
||||
// Get path from top level frame to subframe.
|
||||
const getFramePath = async (tabId, frameId) => {
|
||||
if (framePaths[frameId]) return framePaths[frameId]
|
||||
let path = [frameId]
|
||||
let currentFrameId = frameId
|
||||
while (currentFrameId > 0) {
|
||||
const result = await new Promise(resolve => {
|
||||
const details = { tabId, frameId: currentFrameId }
|
||||
chrome.webNavigation.getFrame(details, details => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error(`Error in getFramePath: ${chrome.runtime.lastError.message}`)
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
resolve(details)
|
||||
})
|
||||
})
|
||||
if (!result) return []
|
||||
const { parentFrameId } = result
|
||||
path.push(parentFrameId)
|
||||
currentFrameId = parentFrameId
|
||||
}
|
||||
path = path.reverse()
|
||||
framePaths[frameId] = path
|
||||
return path
|
||||
}
|
||||
|
||||
const sendToFrame = (tabId, frameId, message) =>
|
||||
chrome.tabs.sendMessage(tabId, message, { frameId })
|
||||
|
||||
const sendToHost = (tabId, message) => sendToFrame(tabId, TOP_FRAME, message)
|
||||
|
||||
const sendWebviewEventToHost = async (tabId, frameId, message) => {
|
||||
const framePath = await getFramePath(tabId, frameId)
|
||||
sendToHost(
|
||||
tabId,
|
||||
{ type: 'metastream-webview-event', payload: message, framePath },
|
||||
{ frameId: TOP_FRAME }
|
||||
)
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Locals
|
||||
//=============================================================================
|
||||
|
||||
// Observed tabs on Metastream URL
|
||||
const watchedTabs = new Set()
|
||||
|
||||
// Store for active tabs state
|
||||
const tabStore = {}
|
||||
|
||||
// Used to know which metastream instance to sent browser badge requests to
|
||||
let lastActiveTabId
|
||||
|
||||
// Map from popup webview ID to parent tab ID
|
||||
// Used for popups pending initialization
|
||||
const popupParents = {}
|
||||
|
||||
// Map from popup tab ID to parent tab ID
|
||||
// Used for routing messages between popup and parent app
|
||||
const popupParentTabs = {}
|
||||
|
||||
// List of popup tab IDs
|
||||
const popupTabs = new Set()
|
||||
|
||||
//=============================================================================
|
||||
// Content scripts
|
||||
//=============================================================================
|
||||
|
||||
const CONTENT_SCRIPTS = [
|
||||
{
|
||||
matches: ['https://*.netflix.com/*'],
|
||||
file: '/scripts/netflix.js'
|
||||
},
|
||||
{
|
||||
matches: ['https://*.hulu.com/*'],
|
||||
file: '/scripts/hulu.js'
|
||||
},
|
||||
{
|
||||
matches: ['https://www.dcuniverse.com/*'],
|
||||
file: '/scripts/dcuniverse.js'
|
||||
},
|
||||
{
|
||||
matches: ['https://docs.google.com/*', 'https://drive.google.com/*'],
|
||||
file: '/scripts/googledrive.js'
|
||||
},
|
||||
{
|
||||
matches: ['https://www.disneyplus.com/*'],
|
||||
file: '/scripts/disneyplus.js'
|
||||
}
|
||||
]
|
||||
|
||||
//=============================================================================
|
||||
// Event listeners
|
||||
//=============================================================================
|
||||
|
||||
// Add Metastream header overwrites
|
||||
const onBeforeSendHeaders = details => {
|
||||
const { tabId, requestHeaders: headers } = details
|
||||
const shouldModify = (watchedTabs.has(tabId) && isTopFrame(details)) || tabId === -1
|
||||
if (shouldModify) {
|
||||
for (let i = headers.length - 1; i >= 0; --i) {
|
||||
const header = headers[i].name.toLowerCase()
|
||||
if (header.startsWith(HEADER_PREFIX)) {
|
||||
const name = header.substr(HEADER_PREFIX.length + 1)
|
||||
const value = headers[i].value
|
||||
headers.push({ name, value })
|
||||
headers.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return { requestHeaders: headers }
|
||||
}
|
||||
|
||||
// Allow embedding any website in Metastream iframe
|
||||
const onHeadersReceived = details => {
|
||||
const { tabId, frameId, responseHeaders: headers } = details
|
||||
let permitted = false
|
||||
|
||||
const isMetastreamTab = watchedTabs.has(tabId) && isDirectChild(details)
|
||||
const isServiceWorkerRequest = watchedTabs.size > 0 && tabId === -1 && frameId === -1
|
||||
const shouldModify = isMetastreamTab || isServiceWorkerRequest
|
||||
|
||||
// TODO: HTTP 301 redirects don't get captured. Try https://reddit.com/
|
||||
|
||||
if (shouldModify) {
|
||||
for (let i = headers.length - 1; i >= 0; --i) {
|
||||
const header = headers[i].name.toLowerCase()
|
||||
const value = headers[i].value
|
||||
if (header === 'x-frame-options' || header === 'frame-options') {
|
||||
headers.splice(i, 1)
|
||||
permitted = true
|
||||
} else if (header === 'content-security-policy' && value.includes('frame-ancestors')) {
|
||||
const policies = value.split(';').filter(value => !value.includes('frame-ancestors'))
|
||||
headers[i].value = policies.join(';')
|
||||
permitted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (permitted) {
|
||||
console.log(`Permitting iframe embedded in tabId=${tabId}, url=${details.url}`)
|
||||
}
|
||||
|
||||
return { responseHeaders: headers }
|
||||
}
|
||||
|
||||
const onTabRemove = (tabId, removeInfo) => {
|
||||
if (watchedTabs.has(tabId)) {
|
||||
stopWatchingTab(tabId)
|
||||
}
|
||||
}
|
||||
|
||||
const onBeforeNavigate = details => {
|
||||
const { tabId, frameId, url } = details
|
||||
if (!watchedTabs.has(tabId)) return
|
||||
if (isTopFrame(frameId)) return
|
||||
;(async () => {
|
||||
const framePath = await getFramePath(tabId, frameId)
|
||||
const isWebviewFrame = framePath[1] === frameId
|
||||
if (isWebviewFrame) {
|
||||
sendWebviewEventToHost(tabId, frameId, { type: 'will-navigate', payload: { url } })
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
// Programmatically inject content scripts into Metastream subframes
|
||||
const initScripts = details => {
|
||||
const { tabId, frameId, url } = details
|
||||
|
||||
if (url.startsWith('about:blank?webview')) {
|
||||
initializeWebview(details)
|
||||
return
|
||||
}
|
||||
|
||||
if (!watchedTabs.has(tabId)) return
|
||||
|
||||
if (isTopFrame(details) && !popupTabs.has(tabId)) {
|
||||
// Listen for top frame navigating away from Metastream
|
||||
if (!isMetastreamUrl(details.url)) {
|
||||
stopWatchingTab(tabId)
|
||||
}
|
||||
} else {
|
||||
injectContentScripts(details)
|
||||
}
|
||||
}
|
||||
|
||||
const onCompleted = details => {
|
||||
const { tabId, frameId, url } = details
|
||||
if (!watchedTabs.has(tabId)) return
|
||||
if (isTopFrame(frameId)) return
|
||||
;(async () => {
|
||||
const framePath = await getFramePath(tabId, frameId)
|
||||
const isWebviewFrame = framePath[1] === frameId
|
||||
if (isWebviewFrame) {
|
||||
sendWebviewEventToHost(tabId, frameId, { type: 'did-navigate', payload: { url } })
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
const onHistoryStateUpdated = details => {
|
||||
const { tabId, frameId, url } = details
|
||||
if (!watchedTabs.has(tabId)) return
|
||||
if (isTopFrame(frameId)) return
|
||||
;(async () => {
|
||||
const framePath = await getFramePath(tabId, frameId)
|
||||
const isWebviewFrame = framePath[1] === frameId
|
||||
if (isWebviewFrame) {
|
||||
sendWebviewEventToHost(tabId, frameId, { type: 'did-navigate-in-page', payload: { url } })
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
const initializeWebview = details => {
|
||||
console.log('Initialize webview', details)
|
||||
const { tabId, frameId, url } = details
|
||||
|
||||
const { searchParams } = new URL(url)
|
||||
|
||||
const isPopup = searchParams.get('popup') === 'true'
|
||||
const webviewId = searchParams.get('webview')
|
||||
|
||||
let hostId
|
||||
|
||||
if (isPopup) {
|
||||
const parentTabId = popupParents[webviewId]
|
||||
if (!parentTabId) {
|
||||
console.error(`No parent tab ID found for popup webview #${webviewId}`)
|
||||
return
|
||||
}
|
||||
hostId = parentTabId
|
||||
popupTabs.add(tabId)
|
||||
watchedTabs.add(tabId)
|
||||
popupParentTabs[tabId] = hostId
|
||||
} else if (watchedTabs.has(tabId)) {
|
||||
hostId = tabId
|
||||
} else {
|
||||
console.warn(`Ignoring webview with tabId=${tabId}, frameId=${frameId}`)
|
||||
return
|
||||
}
|
||||
|
||||
sendToHost(hostId, { type: `metastream-webview-init${webviewId}`, payload: { tabId, frameId } })
|
||||
|
||||
const tabState = tabStore[hostId]
|
||||
const allowScripts = searchParams.get('allowScripts') === 'true'
|
||||
if (allowScripts && tabState && !isPopup) {
|
||||
tabState.scriptableFrames.add(frameId)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: error injecting scripts into popup windows
|
||||
const executeScript = (opts, attempt = 0) => {
|
||||
chrome.tabs.executeScript(
|
||||
opts.tabId,
|
||||
{
|
||||
file: opts.file,
|
||||
runAt: opts.runAt || 'document_start',
|
||||
frameId: opts.frameId
|
||||
},
|
||||
result => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.log(`executeScript error [${opts.file}]: ${chrome.runtime.lastError.message}`)
|
||||
if (opts.retry !== false) {
|
||||
if (attempt < 20) {
|
||||
setTimeout(() => executeScript(opts, attempt + 1), 5)
|
||||
} else {
|
||||
console.error('Reached max attempts while injecting content script.', opts)
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to inject content script', chrome.runtime.lastError, opts)
|
||||
}
|
||||
} else {
|
||||
console.log(`executeScript ${opts.file}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const injectContentScripts = async details => {
|
||||
const { tabId, frameId, url } = details
|
||||
if (url === 'about:blank') return
|
||||
|
||||
// Inject common webview script
|
||||
executeScript({ tabId, frameId, file: '/webview.js' })
|
||||
|
||||
const framePath = await getFramePath(tabId, frameId)
|
||||
const topIFrameId = framePath[1]
|
||||
const tabState = tabStore[tabId]
|
||||
const scriptable =
|
||||
(tabState && tabState.scriptableFrames.has(topIFrameId)) || popupTabs.has(tabId)
|
||||
if (scriptable) {
|
||||
console.log(`Injecting player script tabId=${tabId}, frameId=${frameId}, url=${url}`)
|
||||
executeScript({ tabId, frameId, file: '/player.js' })
|
||||
|
||||
CONTENT_SCRIPTS.forEach(script => {
|
||||
if (!script.matches.some(matchesPattern.bind(null, url))) return
|
||||
executeScript({ tabId, frameId, file: script.file })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Metastream tab management
|
||||
//=============================================================================
|
||||
|
||||
const startWatchingTab = tab => {
|
||||
const { id: tabId } = tab
|
||||
console.log(`Metastream watching tabId=${tabId}`)
|
||||
watchedTabs.add(tabId)
|
||||
|
||||
const state = {
|
||||
// Webview frames which allow scripts to be injected
|
||||
scriptableFrames: new Set(),
|
||||
|
||||
// Event handlers
|
||||
onHeadersReceived: onHeadersReceived.bind(null)
|
||||
}
|
||||
|
||||
tabStore[tabId] = state
|
||||
|
||||
chrome.webRequest.onHeadersReceived.addListener(
|
||||
state.onHeadersReceived,
|
||||
{
|
||||
tabId,
|
||||
urls: ['<all_urls>'],
|
||||
types: ['sub_frame', 'xmlhttprequest', 'script']
|
||||
},
|
||||
[
|
||||
chrome.webRequest.OnHeadersReceivedOptions.BLOCKING,
|
||||
chrome.webRequest.OnHeadersReceivedOptions.RESPONSEHEADERS, // firefox
|
||||
chrome.webRequest.OnHeadersReceivedOptions.RESPONSE_HEADERS // chromium
|
||||
].filter(Boolean)
|
||||
)
|
||||
|
||||
const shouldAddGlobalListeners = watchedTabs.size === 1
|
||||
if (shouldAddGlobalListeners) {
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigate)
|
||||
if (isFirefox()) {
|
||||
chrome.webNavigation.onDOMContentLoaded.addListener(initScripts)
|
||||
} else {
|
||||
chrome.webNavigation.onCommitted.addListener(initScripts)
|
||||
}
|
||||
chrome.webNavigation.onCompleted.addListener(onCompleted)
|
||||
chrome.webNavigation.onHistoryStateUpdated.addListener(onHistoryStateUpdated)
|
||||
chrome.tabs.onRemoved.addListener(onTabRemove)
|
||||
|
||||
// Listen for requests from background script
|
||||
chrome.webRequest.onBeforeSendHeaders.addListener(
|
||||
onBeforeSendHeaders,
|
||||
{ tabId: -1, urls: ['<all_urls>'] },
|
||||
[
|
||||
chrome.webRequest.OnBeforeSendHeadersOptions.BLOCKING,
|
||||
chrome.webRequest.OnBeforeSendHeadersOptions.REQUESTHEADERS, // firefox
|
||||
chrome.webRequest.OnBeforeSendHeadersOptions.REQUEST_HEADERS, // chromium
|
||||
chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS // chromium
|
||||
].filter(Boolean)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const stopWatchingTab = tabId => {
|
||||
watchedTabs.delete(tabId)
|
||||
|
||||
const state = tabStore[tabId]
|
||||
if (state) {
|
||||
chrome.webRequest.onHeadersReceived.removeListener(state.onHeadersReceived)
|
||||
delete tabStore[tabId]
|
||||
}
|
||||
|
||||
const shouldRemoveGlobalListeners = watchedTabs.size === 0
|
||||
if (shouldRemoveGlobalListeners) {
|
||||
chrome.webNavigation.onBeforeNavigate.removeListener(onBeforeNavigate)
|
||||
if (isFirefox()) {
|
||||
chrome.webNavigation.onDOMContentLoaded.removeListener(initScripts)
|
||||
} else {
|
||||
chrome.webNavigation.onCommitted.removeListener(initScripts)
|
||||
}
|
||||
chrome.webNavigation.onCompleted.removeListener(onCompleted)
|
||||
chrome.webNavigation.onHistoryStateUpdated.removeListener(onHistoryStateUpdated)
|
||||
chrome.tabs.onRemoved.removeListener(onTabRemove)
|
||||
chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders)
|
||||
}
|
||||
|
||||
console.log(`Metastream stopped watching tabId=${tabId}`)
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Background fetch proxy
|
||||
//=============================================================================
|
||||
|
||||
const serializeResponse = async response => {
|
||||
let body
|
||||
let headers = {}
|
||||
|
||||
const contentType = (response.headers.get('content-type') || '').toLowerCase()
|
||||
if (contentType && contentType.indexOf('application/json') !== -1) {
|
||||
try {
|
||||
body = await response.json()
|
||||
} catch (e) {}
|
||||
} else {
|
||||
body = await response.text()
|
||||
}
|
||||
|
||||
for (let pair of response.headers.entries()) {
|
||||
headers[pair[0]] = pair[1]
|
||||
}
|
||||
|
||||
return {
|
||||
...response,
|
||||
headers,
|
||||
body
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch on behalf of Metastream app, skips cross-domain security restrictions
|
||||
const request = async (tabId, requestId, url, options) => {
|
||||
const { timeout } = options || {}
|
||||
const controller = new AbortController()
|
||||
const { signal } = controller
|
||||
|
||||
let response, err
|
||||
|
||||
try {
|
||||
console.debug(`Requesting ${url}`)
|
||||
response = await asyncTimeout(fetch(url, { ...options, signal }), timeout)
|
||||
} catch (e) {
|
||||
controller.abort()
|
||||
err = e.message
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: `metastream-fetch-response${requestId}`,
|
||||
payload: {
|
||||
err,
|
||||
resp: response ? await serializeResponse(response) : null
|
||||
}
|
||||
}
|
||||
sendToHost(tabId, action)
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Message passing interface
|
||||
//=============================================================================
|
||||
|
||||
const handleWebviewEvent = async (sender, action) => {
|
||||
const { frameId } = sender
|
||||
const { id: tabId } = sender.tab
|
||||
|
||||
// popups always send webview events back to host app
|
||||
if (popupTabs.has(tabId)) {
|
||||
const parentId = popupParentTabs[tabId]
|
||||
sendWebviewEventToHost(parentId, TOP_FRAME, action.payload)
|
||||
return
|
||||
}
|
||||
|
||||
if (isTopFrame(sender)) {
|
||||
// sent from app
|
||||
sendToFrame(action.tabId || tabId, action.frameId, action.payload)
|
||||
} else {
|
||||
// sent from embedded frame
|
||||
sendWebviewEventToHost(tabId, frameId, action.payload)
|
||||
}
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((action, sender, sendResponse) => {
|
||||
const { id: tabId } = sender.tab
|
||||
if (!isValidAction(action)) return
|
||||
|
||||
// Listen for Metastream app initialization signal
|
||||
if (action.type === 'metastream-init' && isMetastreamUrl(sender.tab.url)) {
|
||||
startWatchingTab(sender.tab)
|
||||
sendResponse(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Filter out messages from non-Metastream app tabs
|
||||
if (!watchedTabs.has(tabId)) return
|
||||
|
||||
switch (action.type) {
|
||||
case 'metastream-webview-event':
|
||||
handleWebviewEvent(sender, action)
|
||||
break
|
||||
case 'metastream-host-event': {
|
||||
// DEPRECATED: used by app v0.5.0
|
||||
handleWebviewEvent(sender, {
|
||||
type: 'metastream-webview-event',
|
||||
payload: action
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'metastream-fetch': {
|
||||
const { requestId, url, options } = action.payload
|
||||
request(tabId, requestId, url, options)
|
||||
break
|
||||
}
|
||||
case 'metastream-remove-data': {
|
||||
const { options, dataToRemove } = action.payload
|
||||
chrome.browsingData.remove(options, dataToRemove)
|
||||
break
|
||||
}
|
||||
case 'metastream-popup-init': {
|
||||
const { id: webviewId } = action.payload
|
||||
popupParents[webviewId] = tabId
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
lastActiveTabId = tabId
|
||||
})
|
||||
|
||||
//=============================================================================
|
||||
// Inject content scripts into existing tabs on startup
|
||||
//=============================================================================
|
||||
|
||||
const { content_scripts: contentScripts = [] } = chrome.runtime.getManifest()
|
||||
const appContentScript = contentScripts.find(
|
||||
script => script.js && script.js.some(file => file.endsWith('app.js'))
|
||||
)
|
||||
|
||||
if (appContentScript) {
|
||||
chrome.tabs.query({ url: appContentScript.matches }, tabs => {
|
||||
tabs.forEach(tab => {
|
||||
chrome.tabs.executeScript(tab.id, { file: appContentScript.js[0] })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Add URL to session on badge click
|
||||
//=============================================================================
|
||||
|
||||
const getMediaTimeInTab = tabId =>
|
||||
new Promise(resolve => {
|
||||
chrome.tabs.executeScript(
|
||||
tabId,
|
||||
{
|
||||
file: '/get-media-time.js',
|
||||
allFrames: true
|
||||
},
|
||||
results => {
|
||||
let time = (results.length > 0 && !isNaN(results[0]) && results[0]) || undefined
|
||||
resolve(time)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// pause media in all non-metastream tabs
|
||||
const pauseMediaInOtherTabs = () => {
|
||||
chrome.tabs.query({ audible: true }, tabs => {
|
||||
tabs.forEach(({ id: tabId }) => {
|
||||
if (watchedTabs.has(tabId)) return
|
||||
chrome.tabs.executeScript(tabId, {
|
||||
file: '/pause-media.js',
|
||||
allFrames: true
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const openLinkInMetastream = details => {
|
||||
const { url: requestUrl, currentTime, source } = details
|
||||
|
||||
const { protocol } = new URL(requestUrl)
|
||||
if (protocol !== 'http:' && protocol !== 'https:') return
|
||||
|
||||
console.log(`Opening URL in Metastream: ${requestUrl}${currentTime ? ` @ ${currentTime}` : ''}`)
|
||||
|
||||
const isMetastreamOpen = watchedTabs.size > 0
|
||||
if (isMetastreamOpen) {
|
||||
const targetTabId = watchedTabs.has(lastActiveTabId)
|
||||
? lastActiveTabId
|
||||
: Array.from(watchedTabs)[0]
|
||||
sendToHost(targetTabId, {
|
||||
type: 'metastream-extension-request',
|
||||
payload: { url: requestUrl, time: currentTime, source }
|
||||
})
|
||||
chrome.tabs.update(targetTabId, { active: true }) // focus tab
|
||||
} else {
|
||||
const params = new URLSearchParams()
|
||||
params.append('url', requestUrl)
|
||||
if (currentTime) params.append('t', currentTime)
|
||||
if (source) params.append('source', source)
|
||||
const url = `${METASTREAM_APP_URL}/?${params.toString()}`
|
||||
// const url = `http://localhost:8080/#?${params.toString()}` // dev
|
||||
chrome.tabs.create({ url })
|
||||
}
|
||||
|
||||
pauseMediaInOtherTabs()
|
||||
}
|
||||
|
||||
const openTabInMetastream = async ({ tab, source }) => {
|
||||
const { id: tabId, url } = tab
|
||||
if (tabId < 0) return
|
||||
|
||||
// ignore badge presses from Metastream tabs
|
||||
if (watchedTabs.has(tabId)) return
|
||||
|
||||
const currentTime = await getMediaTimeInTab(tabId)
|
||||
openLinkInMetastream({ url, currentTime, source })
|
||||
}
|
||||
|
||||
chrome.browserAction.onClicked.addListener(tab =>
|
||||
openTabInMetastream({ tab, source: 'browser-action' })
|
||||
)
|
||||
|
||||
//=============================================================================
|
||||
// Create context menu items to add links to metastream session
|
||||
//=============================================================================
|
||||
|
||||
const TARGET_URL_PATTERNS = ['https://*/*']
|
||||
|
||||
chrome.contextMenus.create({
|
||||
title: 'Open link in Metastream session',
|
||||
contexts: ['link'],
|
||||
targetUrlPatterns: TARGET_URL_PATTERNS,
|
||||
onclick(info, tab) {
|
||||
const { linkUrl: url } = info
|
||||
if (url) openLinkInMetastream({ url, source: 'context-menu-link' })
|
||||
}
|
||||
})
|
||||
|
||||
chrome.contextMenus.create({
|
||||
title: 'Open link in Metastream session',
|
||||
contexts: ['browser_action'],
|
||||
documentUrlPatterns: TARGET_URL_PATTERNS,
|
||||
onclick(info, tab) {
|
||||
openTabInMetastream({ tab, source: 'context-menu-browser-action' })
|
||||
}
|
||||
})
|
||||
|
||||
chrome.contextMenus.create({
|
||||
title: 'Open video in Metastream session',
|
||||
contexts: ['video'],
|
||||
targetUrlPatterns: TARGET_URL_PATTERNS,
|
||||
onclick(info, tab) {
|
||||
const { srcUrl: url } = info
|
||||
if (url) openLinkInMetastream({ url, source: 'context-menu-video' })
|
||||
}
|
||||
})
|
150
resources/metastream-extension-0.4.0_0/first.js
Normal file
150
resources/metastream-extension-0.4.0_0/first.js
Normal file
@ -0,0 +1,150 @@
|
||||
//
|
||||
// The first script to be run on every page as to capture media elements
|
||||
// when they're created.
|
||||
//
|
||||
// Has to be run before anything else is executed in the document.
|
||||
// Declaring the script in the manifest, as opposed to using
|
||||
// chrome.tabs.executeScript, seems to be the only way to achieve this from
|
||||
// my own testing.
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=471801
|
||||
//
|
||||
|
||||
;(function() {
|
||||
// Only run in iframes, the same as Metastream webviews
|
||||
if (window.self === window.top) return
|
||||
|
||||
const mainWorldScript = function() {
|
||||
document.getElementById('metastreaminitscript').remove()
|
||||
|
||||
const INIT_TIMEOUT = 5e3
|
||||
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox')
|
||||
|
||||
//=========================================================================
|
||||
// document.createElement proxy
|
||||
//=========================================================================
|
||||
|
||||
window.__metastreamMediaElements = new Set()
|
||||
|
||||
// Proxy document.createElement to trap media elements created in-memory
|
||||
const origCreateElement = document.createElement
|
||||
const proxyCreateElement = function() {
|
||||
const element = origCreateElement.apply(document, arguments)
|
||||
if (window.__metastreamMediaElements && element instanceof HTMLMediaElement) {
|
||||
window.__metastreamMediaElements.add(element)
|
||||
}
|
||||
return element
|
||||
}
|
||||
proxyCreateElement.toString = origCreateElement.toString.bind(origCreateElement)
|
||||
document.createElement = proxyCreateElement
|
||||
|
||||
setTimeout(() => {
|
||||
if (window.__metastreamMediaElements) {
|
||||
window.__metastreamMediaElements.clear()
|
||||
window.__metastreamMediaElements = undefined
|
||||
}
|
||||
}, INIT_TIMEOUT)
|
||||
|
||||
//=========================================================================
|
||||
// navigator.mediaSession proxy (Firefox)
|
||||
//=========================================================================
|
||||
|
||||
if (isFirefox) {
|
||||
// stub out MediaSession API until Firefox supports this natively
|
||||
if (!navigator.mediaSession) {
|
||||
const noop = () => {}
|
||||
const mediaSessionStub = {
|
||||
__installedByMetastreamRemote__: true,
|
||||
setActionHandler: noop
|
||||
}
|
||||
Object.defineProperty(window.navigator, 'mediaSession', {
|
||||
value: mediaSessionStub,
|
||||
enumerable: false,
|
||||
writable: true
|
||||
})
|
||||
|
||||
function MediaMetadata(metadata) {
|
||||
Object.assign(this, metadata)
|
||||
}
|
||||
window.MediaMetadata = MediaMetadata
|
||||
}
|
||||
|
||||
const { mediaSession } = navigator
|
||||
|
||||
// Capture action handlers for player.js proxy
|
||||
mediaSession._handlers = {}
|
||||
|
||||
const _setActionHandler = mediaSession.setActionHandler
|
||||
mediaSession.setActionHandler = function(name, handler) {
|
||||
mediaSession._handlers[name] = handler
|
||||
_setActionHandler.apply(mediaSession, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// document.domain fix (Firefox)
|
||||
//=========================================================================
|
||||
|
||||
if (isFirefox) {
|
||||
const domains = ['twitch.tv', 'crunchyroll.com']
|
||||
|
||||
// Fix for setting document.domain in sandboxed iframe
|
||||
try {
|
||||
const { domain } = document
|
||||
if (domain && domains.some(d => domain.includes(d))) {
|
||||
Object.defineProperty(document, 'domain', {
|
||||
value: domain,
|
||||
writable: true
|
||||
})
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// Inline script embed prevention fix
|
||||
//=========================================================================
|
||||
|
||||
const observeScripts = () => {
|
||||
const scriptSnippets = [
|
||||
{ code: 'window.top !== window.self', replacement: 'false' },
|
||||
{ code: 'self == top', replacement: 'true' },
|
||||
{ code: 'top.location != window.location', replacement: 'false' }
|
||||
]
|
||||
|
||||
const getAddedScripts = mutationList =>
|
||||
mutationList.reduce((scripts, mutation) => {
|
||||
if (mutation.type !== 'childList') return scripts
|
||||
const inlineScripts = Array.from(mutation.addedNodes).filter(
|
||||
node => node instanceof HTMLScriptElement && node.innerHTML.length > 0
|
||||
)
|
||||
return inlineScripts.length > 0 ? [...scripts, ...inlineScripts] : scripts
|
||||
}, [])
|
||||
|
||||
// Modifies inline scripts to allow embedding content in iframe
|
||||
const inlineScriptModifier = mutationsList => {
|
||||
const scripts = getAddedScripts(mutationsList)
|
||||
for (let script of scripts) {
|
||||
for (let snippet of scriptSnippets) {
|
||||
if (script.innerHTML.includes(snippet.code)) {
|
||||
script.innerHTML = script.innerHTML.split(snippet.code).join(snippet.replacement)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(inlineScriptModifier)
|
||||
observer.observe(document.documentElement, { childList: true, subtree: true })
|
||||
|
||||
// Stop watching for changes after we finish loading
|
||||
window.addEventListener('load', () => observer.disconnect())
|
||||
}
|
||||
|
||||
observeScripts()
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.id = 'metastreaminitscript'
|
||||
script.textContent = `(${mainWorldScript}());`
|
||||
if (document.documentElement) {
|
||||
document.documentElement.appendChild(script)
|
||||
}
|
||||
})()
|
4
resources/metastream-extension-0.4.0_0/get-media-time.js
Normal file
4
resources/metastream-extension-0.4.0_0/get-media-time.js
Normal file
@ -0,0 +1,4 @@
|
||||
Array.from(document.querySelectorAll('video, audio'))
|
||||
.filter(media => media.currentTime > 0)
|
||||
.map(media => Math.floor(media.currentTime))
|
||||
.shift()
|
BIN
resources/metastream-extension-0.4.0_0/icon128.png
Normal file
BIN
resources/metastream-extension-0.4.0_0/icon128.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 11 KiB |
BIN
resources/metastream-extension-0.4.0_0/icon16.png
Normal file
BIN
resources/metastream-extension-0.4.0_0/icon16.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 790 B |
BIN
resources/metastream-extension-0.4.0_0/icon48.png
Normal file
BIN
resources/metastream-extension-0.4.0_0/icon48.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 3.1 KiB |
32
resources/metastream-extension-0.4.0_0/manifest.json
Normal file
32
resources/metastream-extension-0.4.0_0/manifest.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"background": {
|
||||
"persistent": true,
|
||||
"scripts": [ "background.js" ]
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": "icon48.png"
|
||||
},
|
||||
"content_scripts": [ {
|
||||
"js": [ "app.js" ],
|
||||
"matches": [ "https://app.getmetastream.com/*", "http://local.getmetastream.com/*", "http://localhost:8080/*" ],
|
||||
"run_at": "document_start"
|
||||
}, {
|
||||
"all_frames": true,
|
||||
"js": [ "first.js" ],
|
||||
"matches": [ "*://*/*" ],
|
||||
"run_at": "document_start"
|
||||
} ],
|
||||
"description": "Watch streaming media with friends.",
|
||||
"icons": {
|
||||
"128": "icon128.png",
|
||||
"16": "icon16.png",
|
||||
"48": "icon48.png"
|
||||
},
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwATjrynWXJ48EwhcylI6Qe9D8RKhUtWJ6e+fa0nM1KUZpW0gTXNRfY8k8ZfByuwzSquLQUv5l9d+enkWz+txQEnuJrAeFMdEZKyZJwEhEw68Zz+TSDl5BZSRbUJlLdVz+ZeBBEJnbtHpI4ZIaZbNx3QN7S0VTV/GDqX6C4IJi/6f6V230UyKD9mptFAFjtsPmrUZT3jmNJ7C7XlfsyVFtziePvuZaRuYQeB+xg5ZgXguWpN+++sjU2IvTko+nuOuMSLyGJYaaMjtSIreyjo5GnBvn/j12Ub0uyrnAnh/+195Wwl/mjP28PJFVr6ms1ij/oh/909OgBTbDt4/8201swIDAQAB",
|
||||
"manifest_version": 2,
|
||||
"name": "Metastream Remote",
|
||||
"permissions": [ "tabs", "webNavigation", "webRequest", "webRequestBlocking", "browsingData", "contextMenus", "\u003Call_urls>" ],
|
||||
"short_name": "Metastream",
|
||||
"update_url": "https://clients2.google.com/service/update2/crx",
|
||||
"version": "0.4.0"
|
||||
}
|
5
resources/metastream-extension-0.4.0_0/pause-media.js
Normal file
5
resources/metastream-extension-0.4.0_0/pause-media.js
Normal file
@ -0,0 +1,5 @@
|
||||
;(function() {
|
||||
Array.from(document.querySelectorAll('video, audio')).forEach(media => {
|
||||
if (!media.paused) media.pause()
|
||||
})
|
||||
})()
|
1093
resources/metastream-extension-0.4.0_0/player.js
Normal file
1093
resources/metastream-extension-0.4.0_0/player.js
Normal file
File diff suppressed because it is too large
Load Diff
17
resources/metastream-extension-0.4.0_0/scripts/dcuniverse.js
Normal file
17
resources/metastream-extension-0.4.0_0/scripts/dcuniverse.js
Normal file
@ -0,0 +1,17 @@
|
||||
(function () {
|
||||
let timeoutId
|
||||
const seek = e => {
|
||||
e.preventDefault()
|
||||
const time = e.detail / 1000
|
||||
const media = document.querySelector('video')
|
||||
if (!media || media.paused) return
|
||||
|
||||
// Fix for initial seek throwing exception
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => {
|
||||
media.currentTime = time
|
||||
document.removeEventListener('metastreamseek', seek, false)
|
||||
}, 1000)
|
||||
}
|
||||
document.addEventListener('metastreamseek', seek, false)
|
||||
}())
|
16
resources/metastream-extension-0.4.0_0/scripts/disneyplus.js
Normal file
16
resources/metastream-extension-0.4.0_0/scripts/disneyplus.js
Normal file
@ -0,0 +1,16 @@
|
||||
;(function() {
|
||||
let pauseTimeoutId = -1
|
||||
|
||||
document.addEventListener('metastreamplay', e => {
|
||||
clearTimeout(pauseTimeoutId)
|
||||
})
|
||||
|
||||
// Pausing soon after seek doesn't work well on Disney+ so we'll wait and try again
|
||||
document.addEventListener('metastreampause', e => {
|
||||
clearTimeout(pauseTimeoutId)
|
||||
pauseTimeoutId = setTimeout(() => {
|
||||
const vid = document.querySelector('video')
|
||||
if (vid) vid.pause()
|
||||
}, 300)
|
||||
})
|
||||
})()
|
@ -0,0 +1,6 @@
|
||||
window.addEventListener('load', () => {
|
||||
const previewImg = document.querySelector('img:not([alt=""])')
|
||||
if (previewImg) {
|
||||
previewImg.click()
|
||||
}
|
||||
})
|
60
resources/metastream-extension-0.4.0_0/scripts/hulu.js
Normal file
60
resources/metastream-extension-0.4.0_0/scripts/hulu.js
Normal file
@ -0,0 +1,60 @@
|
||||
const clickAtProgress = (target, progress) => {
|
||||
const { width, height, left, top } = target.getBoundingClientRect()
|
||||
const x = left + width * progress
|
||||
const y = top + height / 2
|
||||
|
||||
var clickEvent = document.createEvent('MouseEvents')
|
||||
clickEvent.initMouseEvent(
|
||||
'click',
|
||||
true,
|
||||
true,
|
||||
window,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
x,
|
||||
y,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
null
|
||||
)
|
||||
|
||||
target.dispatchEvent(clickEvent)
|
||||
}
|
||||
|
||||
document.addEventListener('metastreamplay', e => {
|
||||
const btn = document.querySelector('.controls__playback-button--paused')
|
||||
if (btn) {
|
||||
e.preventDefault()
|
||||
btn.click()
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('metastreampause', e => {
|
||||
const btn = document.querySelector('.controls__playback-button--playing')
|
||||
if (btn) {
|
||||
e.preventDefault()
|
||||
btn.click()
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('metastreamseek', e => {
|
||||
e.preventDefault()
|
||||
const time = e.detail / 1000
|
||||
const media = document.querySelector('video')
|
||||
if (media.paused) return
|
||||
|
||||
const progress = Math.max(0, Math.min(time / media.duration, 1))
|
||||
|
||||
const controlsContainer = document.querySelector('.controls-bar-container')
|
||||
const controlsDisplay = controlsContainer.style.display
|
||||
controlsContainer.style.display = 'block'
|
||||
|
||||
const progressBar = document.querySelector('.controls__progress-bar-total')
|
||||
clickAtProgress(progressBar, progress)
|
||||
|
||||
controlsContainer.style.display = controlsDisplay
|
||||
})
|
44
resources/metastream-extension-0.4.0_0/scripts/netflix.js
Normal file
44
resources/metastream-extension-0.4.0_0/scripts/netflix.js
Normal file
@ -0,0 +1,44 @@
|
||||
'use strict'
|
||||
;(function() {
|
||||
const mainWorldScript = function() {
|
||||
const netflixPlayer = () => {
|
||||
let player
|
||||
try {
|
||||
const { videoPlayer } = netflix.appContext.state.playerApp.getAPI()
|
||||
const playerSessionId = videoPlayer.getAllPlayerSessionIds().find(Boolean)
|
||||
player = videoPlayer.getVideoPlayerBySessionId(playerSessionId)
|
||||
} catch (e) {}
|
||||
return player || null
|
||||
}
|
||||
|
||||
document.addEventListener('metastreamplay', e => {
|
||||
e.preventDefault()
|
||||
const player = netflixPlayer()
|
||||
if (player && player.getPaused()) {
|
||||
player.play()
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('metastreampause', e => {
|
||||
e.preventDefault()
|
||||
const player = netflixPlayer()
|
||||
if (player && !player.getPaused()) {
|
||||
player.pause()
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('metastreamseek', e => {
|
||||
e.preventDefault()
|
||||
const player = netflixPlayer()
|
||||
if (player) {
|
||||
const time = e.detail
|
||||
player.seek(time)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Inject inline script at top of DOM to execute as soon as possible
|
||||
const script = document.createElement('script')
|
||||
script.textContent = `(${mainWorldScript}());`
|
||||
document.documentElement.appendChild(script)
|
||||
})()
|
80
resources/metastream-extension-0.4.0_0/webview.js
Normal file
80
resources/metastream-extension-0.4.0_0/webview.js
Normal file
@ -0,0 +1,80 @@
|
||||
//
|
||||
// Listens for webview events
|
||||
//
|
||||
|
||||
;(function() {
|
||||
const throttle = (func, limit) => {
|
||||
let inThrottle
|
||||
return function() {
|
||||
const args = arguments
|
||||
const context = this
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args)
|
||||
inThrottle = true
|
||||
setTimeout(() => (inThrottle = false), limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(action => {
|
||||
if (typeof action !== 'object' || typeof action.type !== 'string') return
|
||||
switch (action.type) {
|
||||
case 'navigate':
|
||||
history.go(Number(action.payload) || 0)
|
||||
break
|
||||
case 'reload':
|
||||
location.reload(Boolean(action.payload))
|
||||
break
|
||||
case 'stop':
|
||||
stop()
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'metastream-webview-event',
|
||||
payload: { type: 'load-script' }
|
||||
})
|
||||
|
||||
// Forward activity signal to top frame
|
||||
// Used for determining inactivity in interactive mode and for verifying
|
||||
// whether user triggered media state changes.
|
||||
const onWebviewActivity = event => {
|
||||
if (!event.isTrusted) return
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'metastream-webview-event',
|
||||
payload: { type: 'activity', payload: event.type }
|
||||
})
|
||||
}
|
||||
const onFrequentWebviewActivity = throttle(onWebviewActivity, 500)
|
||||
const onImportantWebviewActivity = throttle(onWebviewActivity, 80)
|
||||
document.addEventListener('mousemove', onFrequentWebviewActivity, true)
|
||||
document.addEventListener('mousedown', onImportantWebviewActivity, true)
|
||||
document.addEventListener('mouseup', onImportantWebviewActivity, true)
|
||||
document.addEventListener('mousewheel', onFrequentWebviewActivity, true)
|
||||
document.addEventListener('keydown', onImportantWebviewActivity, true)
|
||||
|
||||
const mainWorldScript = function() {
|
||||
document.getElementById('metastreamwebviewinit').remove()
|
||||
|
||||
// Fix for setting document.domain in sandboxed iframe
|
||||
try {
|
||||
Object.defineProperty(document, 'domain', {
|
||||
value: document.domain,
|
||||
writable: true
|
||||
})
|
||||
} catch (e) {}
|
||||
|
||||
// Fix for sandboxed iframe detection
|
||||
Object.defineProperty(HTMLObjectElement.prototype, 'onerror', {
|
||||
set: () => {}
|
||||
})
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.id = 'metastreamwebviewinit'
|
||||
script.textContent = `(${mainWorldScript}());`
|
||||
if (document.documentElement) {
|
||||
document.documentElement.appendChild(script)
|
||||
}
|
||||
})()
|
28
test.js
Normal file
28
test.js
Normal file
@ -0,0 +1,28 @@
|
||||
(async function() {
|
||||
let browser = await require("puppeteer").launch({headless:false, args: [
|
||||
`--load-extension=` + require("path").join(process.cwd(), `resources/metastream-extension-0.4.0_0`)
|
||||
], ignoreDefaultArgs: [
|
||||
"--disable-extensions"
|
||||
]})
|
||||
let page = await browser.newPage()
|
||||
await page.goto("https://app.getmetastream.com")
|
||||
let usernameInput = await page.waitForSelector("#profile_username")
|
||||
await usernameInput.type("Melony")
|
||||
await page.keyboard.press("Enter")
|
||||
let startSession = await page.waitForSelector(`a[href^="/join/"]`)
|
||||
await startSession.click()
|
||||
/*setInterval(async () => {
|
||||
let allowButton = await page.$('[title="Allow"]')
|
||||
if (allowButton) await allowButton.click()
|
||||
}, 2000)*/
|
||||
try {
|
||||
await (await page.waitForSelector(`[title="Settings"]`)).click()
|
||||
await (await page.$$(`[class^="UserAvatar__image"]`)).pop().click()
|
||||
await (await page.$x("//button[contains(text(), 'Session')]"))[0].click()
|
||||
await (await page.$x(`//span[contains(text(), 'Public')]`))[0].click()
|
||||
await (await page.$x(`//button[contains(text(), 'Advanced')]`))[0].click()
|
||||
await (await page.$(`label[for="safebrowse"]`)).click()
|
||||
await (await page.$(`button[class^="Modal__close"]`)).click()
|
||||
} catch(e) { console.error(e.stack) }
|
||||
console.log(page.url())
|
||||
})()
|
Reference in New Issue
Block a user