Last active
December 12, 2025 09:28
-
-
Save dvygolov/4449da3e5aa420d4ecf0b4b37f5e7b82 to your computer and use it in GitHub Desktop.
Script to manage column presets of Ads Manager: import/export columns
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ============================================ | |
| // Configuration | |
| // ============================================ | |
| const Config = { | |
| VERSION: "2025.12.12", | |
| API_VERSION: "v23.0", | |
| API_URL: "https://adsmanager-graph.facebook.com/v23.0/" | |
| }; | |
| // ============================================ | |
| // Logger Class | |
| // ============================================ | |
| class Logger { | |
| constructor(uiInstance = null) { | |
| this.ui = uiInstance; | |
| } | |
| setUI(uiInstance) { | |
| this.ui = uiInstance; | |
| } | |
| log(message, type = "info") { | |
| if (this.ui && this.ui.log) { | |
| this.ui.log(message, type); | |
| } | |
| if (type === "error") { | |
| console.error(message); | |
| } else { | |
| console.log(message); | |
| } | |
| } | |
| info(message) { | |
| this.log(message, "info"); | |
| } | |
| error(message) { | |
| this.log(message, "error"); | |
| } | |
| success(message) { | |
| this.log(message, "success"); | |
| } | |
| warning(message) { | |
| this.log(message, "warning"); | |
| } | |
| } | |
| // Global logger instance | |
| const logger = new Logger(); | |
| // ============================================ | |
| // FileHelper Class | |
| // ============================================ | |
| class FileHelper { | |
| async readFileAsJsonAsync(file) { | |
| try { | |
| const fileContent = await this.readFileAsync(file); | |
| return JSON.parse(fileContent); | |
| } catch (error) { | |
| console.error("Error:", error); | |
| throw error; | |
| } | |
| } | |
| readFileAsync(file) { | |
| return new Promise((resolve, reject) => { | |
| let reader = new FileReader(); | |
| reader.onload = () => resolve(reader.result); | |
| reader.onerror = () => reject("Error reading file"); | |
| reader.readAsText(file); | |
| }); | |
| } | |
| } | |
| // ============================================ | |
| // FileSelector Class | |
| // ============================================ | |
| class FileSelector { | |
| constructor(fileProcessor) { | |
| this.fileProcessor = fileProcessor; | |
| } | |
| createDiv() { | |
| this.div = document.createElement("div"); | |
| this.div.style.position = "fixed"; | |
| this.div.style.top = "50%"; | |
| this.div.style.left = "50%"; | |
| this.div.style.transform = "translate(-50%, -50%)"; | |
| this.div.style.width = "200px"; | |
| this.div.style.height = "120px"; | |
| this.div.style.backgroundColor = "yellow"; | |
| this.div.style.zIndex = "1001"; | |
| this.div.style.display = "flex"; | |
| this.div.style.flexDirection = "column"; | |
| this.div.style.alignItems = "center"; | |
| this.div.style.justifyContent = "center"; | |
| this.div.style.padding = "10px"; | |
| this.div.style.boxSizing = "border-box"; | |
| this.div.style.borderRadius = "10px"; | |
| var title = document.createElement("div"); | |
| title.innerHTML = "Select file to import preset"; | |
| title.style.textAlign = "center"; | |
| title.style.fontWeight = "bold"; | |
| var closeButton = document.createElement("button"); | |
| closeButton.innerHTML = "X"; | |
| closeButton.style.position = "absolute"; | |
| closeButton.style.top = "5px"; | |
| closeButton.style.right = "5px"; | |
| closeButton.style.border = "none"; | |
| closeButton.style.background = "none"; | |
| closeButton.style.cursor = "pointer"; | |
| closeButton.onclick = () => { | |
| document.body.removeChild(this.div); | |
| }; | |
| this.div.appendChild(title); | |
| this.div.appendChild(closeButton); | |
| } | |
| createFileInput() { | |
| this.fileInput = document.createElement("input"); | |
| this.fileInput.type = "file"; | |
| this.fileInput.accept = ".json"; | |
| this.fileInput.style.display = "none"; | |
| } | |
| createButton() { | |
| this.button = document.createElement("button"); | |
| this.button.textContent = "Select File"; | |
| this.button.onclick = () => { | |
| this.fileInput.click(); | |
| }; | |
| } | |
| show() { | |
| return new Promise((resolve, reject) => { | |
| this.createDiv(); | |
| this.createFileInput(); | |
| this.createButton(); | |
| this.div.appendChild(this.button); | |
| this.div.appendChild(this.fileInput); | |
| document.body.appendChild(this.div); | |
| this.fileInput.onchange = async () => { | |
| if (!this.fileInput.files || this.fileInput.files.length === 0) { | |
| document.body.removeChild(this.div); | |
| alert("Operation canceled"); | |
| reject("File selection cancelled by user"); | |
| return; | |
| } | |
| try { | |
| const result = await this.fileProcessor(this.fileInput.files[0]); | |
| document.body.removeChild(this.div); | |
| resolve(result); | |
| } catch (error) { | |
| document.body.removeChild(this.div); | |
| reject(error); | |
| } | |
| }; | |
| }); | |
| } | |
| } | |
| // ============================================ | |
| // Facebook API Class | |
| // ============================================ | |
| class FbApi { | |
| apiUrl = Config.API_URL; | |
| async getRequest(path, qs = null, token = null) { | |
| token = token ?? __accessToken; | |
| let finalUrl = path.startsWith('http') ? path : this.apiUrl + path; | |
| const hasAccessToken = finalUrl.includes('access_token='); | |
| if (!hasAccessToken) { | |
| qs = qs != null ? `${qs}&access_token=${token}` : `access_token=${token}`; | |
| const separator = finalUrl.includes('?') ? '&' : '?'; | |
| finalUrl = `${finalUrl}${separator}${qs}`; | |
| } else if (qs) { | |
| finalUrl = `${finalUrl}&${qs}`; | |
| } | |
| let f = await fetch(finalUrl, { | |
| headers: { | |
| accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", | |
| "accept-language": "ca-ES,ca;q=0.9,en-US;q=0.8,en;q=0.7", | |
| "cache-control": "max-age=0", | |
| "sec-ch-ua": '"Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"', | |
| "sec-ch-ua-mobile": "?0", | |
| "sec-ch-ua-platform": '"Windows"', | |
| "sec-fetch-dest": "empty", | |
| "sec-fetch-mode": "cors", | |
| "sec-fetch-site": "same-site", | |
| }, | |
| referrerPolicy: "strict-origin-when-cross-origin", | |
| body: null, | |
| method: "GET", | |
| mode: "cors", | |
| credentials: "include", | |
| referrer: "https://business.facebook.com/", | |
| }); | |
| let json = await f.json(); | |
| return json; | |
| } | |
| async getAllPages(path, qs, token = null) { | |
| let items = []; | |
| let page = await this.getRequest(path, qs, token); | |
| items = items.concat(page.data); | |
| while (page.paging && page.paging.next) { | |
| page = await this.getRequest(page.paging.next, null, token); | |
| items = items.concat(page.data); | |
| } | |
| return items; | |
| } | |
| async postRequest(path, body, token = null) { | |
| token = token ?? __accessToken; | |
| body["access_token"] = token; | |
| let headers = { | |
| accept: "*/*", | |
| "accept-language": "en-US,en;q=0.9", | |
| "content-type": "application/x-www-form-urlencoded", | |
| "sec-ch-ua": '"Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"', | |
| "sec-ch-ua-mobile": "?0", | |
| "sec-ch-ua-platform": '"Windows"', | |
| "sec-fetch-dest": "empty", | |
| "sec-fetch-mode": "cors", | |
| "sec-fetch-site": "same-site", | |
| }; | |
| let finalUrl = path.startsWith('http') ? path : this.apiUrl + path; | |
| let f = await fetch(finalUrl, { | |
| headers: headers, | |
| referrer: "https://business.facebook.com/", | |
| referrerPolicy: "origin-when-cross-origin", | |
| body: new URLSearchParams(body).toString(), | |
| method: "POST", | |
| mode: "cors", | |
| credentials: "include", | |
| }); | |
| let json = await f.json(); | |
| return json; | |
| } | |
| } | |
| // Global API instance | |
| const API = new FbApi(); | |
| // ============================================ | |
| // Account Manager Class | |
| // ============================================ | |
| class AccountManager { | |
| constructor() { | |
| this.accounts = []; | |
| } | |
| async loadAll() { | |
| try { | |
| logger.info("Loading all accounts..."); | |
| const accounts = await API.getAllPages("me/adaccounts", "fields=id,name,account_status"); | |
| this.accounts = accounts.map(account => { | |
| const accountId = account.id.replace("act_", ""); | |
| return { | |
| id: accountId, | |
| name: account.name || accountId, | |
| status: account.account_status | |
| }; | |
| }); | |
| logger.success(`Loaded ${this.accounts.length} accounts.`); | |
| return this.accounts; | |
| } catch (error) { | |
| logger.error("Error loading accounts: " + error); | |
| throw error; | |
| } | |
| } | |
| getAll() { | |
| return this.accounts; | |
| } | |
| findById(accountId) { | |
| return this.accounts.find(acc => acc.id === accountId); | |
| } | |
| } | |
| // Global account manager instance | |
| const accountManager = new AccountManager(); | |
| // Legacy global variable accessor | |
| let allAccountsData = new Proxy({}, { | |
| get(target, prop) { | |
| if (typeof prop === 'symbol') return undefined; | |
| const accounts = accountManager.getAll(); | |
| if (prop === 'length') return accounts.length; | |
| if (prop === 'find') return accounts.find.bind(accounts); | |
| if (prop === 'map') return accounts.map.bind(accounts); | |
| if (prop === 'filter') return accounts.filter.bind(accounts); | |
| if (prop === 'forEach') return accounts.forEach.bind(accounts); | |
| return accounts[prop]; | |
| } | |
| }); | |
| // ============================================ | |
| // Custom Derived Metrics Functions | |
| // ============================================ | |
| const CUSTOM_METRIC_PREFIX = "custom_derived_metrics:"; | |
| async function fetchCustomMetrics(accountId) { | |
| const accId = accountId ?? require("BusinessUnifiedNavigationContext").adAccountID; | |
| logger.info(`Loading custom metrics for account ${accId}...`); | |
| const metrics = await API.getAllPages( | |
| `act_${accId}/ad_custom_derived_metrics`, | |
| `fields=name,formula,format_type,description` | |
| ); | |
| logger.success(`Loaded ${metrics.length} custom metrics.`); | |
| return metrics; | |
| } | |
| async function createCustomMetric(accountId, metricData) { | |
| const accId = accountId ?? require("BusinessUnifiedNavigationContext").adAccountID; | |
| logger.info(`Creating custom metric "${metricData.name}" on account ${accId}...`); | |
| const data = { | |
| name: metricData.name, | |
| formula: metricData.formula, | |
| format_type: metricData.format_type || "FLOAT", | |
| permission: "shared" | |
| }; | |
| if (metricData.description) { | |
| data.description = metricData.description; | |
| } | |
| const result = await API.postRequest(`act_${accId}/ad_custom_derived_metrics`, data); | |
| if (result.id) { | |
| logger.success(`Created custom metric "${metricData.name}" with ID ${result.id}`); | |
| return result.id; | |
| } else { | |
| logger.error(`Failed to create custom metric "${metricData.name}": ${JSON.stringify(result)}`); | |
| return null; | |
| } | |
| } | |
| function extractCustomMetricIds(preset) { | |
| const customMetricIds = []; | |
| if (preset.columns && Array.isArray(preset.columns)) { | |
| for (const col of preset.columns) { | |
| if (col.column_id && col.column_id.startsWith(CUSTOM_METRIC_PREFIX)) { | |
| const metricId = col.column_id.replace(CUSTOM_METRIC_PREFIX, ""); | |
| customMetricIds.push(metricId); | |
| } | |
| } | |
| } | |
| return customMetricIds; | |
| } | |
| function replaceCustomMetricIds(preset, idMapping) { | |
| if (!preset.columns || !Array.isArray(preset.columns)) return preset; | |
| const newColumns = preset.columns.map(col => { | |
| if (col.column_id && col.column_id.startsWith(CUSTOM_METRIC_PREFIX)) { | |
| const oldId = col.column_id.replace(CUSTOM_METRIC_PREFIX, ""); | |
| const newId = idMapping[oldId]; | |
| if (newId) { | |
| return { ...col, column_id: `${CUSTOM_METRIC_PREFIX}${newId}` }; | |
| } | |
| } | |
| return col; | |
| }); | |
| return { ...preset, columns: newColumns }; | |
| } | |
| // ============================================ | |
| // Column Preset Functions | |
| // ============================================ | |
| async function fetchAccountPresets(accountId) { | |
| const accId = accountId ?? require("BusinessUnifiedNavigationContext").adAccountID; | |
| logger.info(`Loading presets for account ${accId}...`); | |
| let js = await API.getRequest( | |
| `act_${accId}`, | |
| `fields=["user_settings{id,column_presets{attribution_windows,columns,id,name,time_created,time_updated}},ad_column_sizes{page,tab,report,view,columns}"]` | |
| ); | |
| const presets = js.user_settings?.column_presets?.data || []; | |
| const sizes = js.ad_column_sizes?.data || []; | |
| logger.success(`Loaded ${presets.length} presets.`); | |
| return { presets, sizes }; | |
| } | |
| async function exportColumnPreset(selectedPreset, sizes = [], accountId = null) { | |
| if (!selectedPreset) { | |
| logger.warning("No preset selected"); | |
| return null; | |
| } | |
| const jsFile = { | |
| preset: selectedPreset, | |
| sizes: sizes, | |
| customMetrics: [] | |
| }; | |
| // Check for custom metrics in preset | |
| const customMetricIds = extractCustomMetricIds(selectedPreset); | |
| if (customMetricIds.length > 0) { | |
| logger.info(`Found ${customMetricIds.length} custom metric(s) in preset, fetching details...`); | |
| const allMetrics = await fetchCustomMetrics(accountId); | |
| const usedMetrics = allMetrics.filter(m => customMetricIds.includes(m.id)); | |
| jsFile.customMetrics = usedMetrics.map(m => ({ | |
| id: m.id, | |
| name: m.name, | |
| formula: m.formula, | |
| format_type: m.format_type, | |
| description: m.description || "" | |
| })); | |
| logger.success(`Included ${jsFile.customMetrics.length} custom metric(s) in export.`); | |
| } | |
| const blob = new Blob([JSON.stringify(jsFile)], { type: "application/json" }); | |
| const a = document.createElement("a"); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = `${selectedPreset.name}.json`; | |
| a.click(); | |
| logger.success(`Exported preset: ${selectedPreset.name}`); | |
| return selectedPreset; | |
| } | |
| async function fetchUserSettingsId(adAccountId) { | |
| let accId = adAccountId ?? require("BusinessUnifiedNavigationContext").adAccountID; | |
| logger.info(`Getting user settings for acc ${accId}...`); | |
| let js = await API.getRequest(`act_${accId}`, `fields=[]`); | |
| let usId = js?.user_settings?.id; | |
| if (usId == null) { | |
| logger.info(`No default user settings found! Creating them...`); | |
| js = await API.getRequest(`act_${accId}/user_settings`, `method=post`); | |
| usId = js.id; | |
| } | |
| return usId; | |
| } | |
| async function uploadPreset(userSettingsId, presetData) { | |
| let data = { | |
| name: presetData.name, | |
| attribution_windows: JSON.stringify(presetData.attribution_windows), | |
| columns: JSON.stringify(presetData.columns), | |
| }; | |
| logger.info(`Uploading preset ${presetData.name} to user settings ${userSettingsId}...`); | |
| let js = await API.postRequest(`${userSettingsId}/column_presets`, data); | |
| return js.id; | |
| } | |
| async function setDefaultColumnPreset(adAccountId, presetId) { | |
| let accId = adAccountId ?? require("BusinessUnifiedNavigationContext").adAccountID; | |
| logger.info(`Setting default column preset for acc ${accId}, preset id ${presetId}...`); | |
| let data = { | |
| default_column_preset: `{ "id": "${presetId}" }`, | |
| default_column_preset_id: presetId, | |
| }; | |
| let js = await API.postRequest(`act_${accId}/user_settings`, data); | |
| return js; | |
| } | |
| async function uploadSize(adAccountId, size) { | |
| let accId = adAccountId ?? require("BusinessUnifiedNavigationContext").adAccountID; | |
| const columns = size.columns.reduce((acc, { key, value }) => { | |
| acc[key] = parseInt(value, 10); | |
| return acc; | |
| }, {}); | |
| let data = { | |
| page: size.page, | |
| tab: size.tab, | |
| columns: JSON.stringify(columns), | |
| }; | |
| logger.info(`Uploading sizes to ad account ${accId}...`); | |
| let js = await API.postRequest(`act_${accId}/ad_column_sizes`, data); | |
| const sizeId = js.id; | |
| js = await API.postRequest(sizeId, data); | |
| return js.success; | |
| } | |
| async function importPresetToAccount(accountId, presetContent) { | |
| try { | |
| let presetToUpload = { ...presetContent.preset }; | |
| // Handle custom metrics if present | |
| if (presetContent.customMetrics && presetContent.customMetrics.length > 0) { | |
| logger.info(`Creating ${presetContent.customMetrics.length} custom metric(s) on account ${accountId}...`); | |
| const idMapping = {}; | |
| for (const metric of presetContent.customMetrics) { | |
| const newId = await createCustomMetric(accountId, metric); | |
| if (newId) { | |
| idMapping[metric.id] = newId; | |
| } else { | |
| logger.warning(`Skipping metric "${metric.name}" - creation failed`); | |
| } | |
| } | |
| // Replace old IDs with new IDs in preset | |
| presetToUpload = replaceCustomMetricIds(presetToUpload, idMapping); | |
| } | |
| const userSettingsId = await fetchUserSettingsId(accountId); | |
| let presetId = await uploadPreset(userSettingsId, presetToUpload); | |
| await setDefaultColumnPreset(accountId, presetId); | |
| logger.success(`Imported preset to account ${accountId}`); | |
| return { success: true, presetId }; | |
| } catch (error) { | |
| logger.error(`Error importing to account ${accountId}: ${error}`); | |
| return { success: false, error }; | |
| } | |
| } | |
| async function importPresetToSelectedAccounts(accountIds, presetContent, uiInstance) { | |
| logger.info(`Importing preset to ${accountIds.length} accounts...`); | |
| let successCount = 0; | |
| let failedCount = 0; | |
| for (let i = 0; i < accountIds.length; i++) { | |
| const accountId = accountIds[i]; | |
| logger.info(`Processing account ${accountId} (${i+1}/${accountIds.length})...`); | |
| const result = await importPresetToAccount(accountId, presetContent); | |
| if (result.success) { | |
| successCount++; | |
| } else { | |
| failedCount++; | |
| } | |
| // Add delay between accounts to avoid rate limiting | |
| if (i < accountIds.length - 1) { | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| } | |
| } | |
| const summaryMessage = `Processed ${accountIds.length} accounts: ${successCount} successful, ${failedCount} failed.`; | |
| logger.success(summaryMessage); | |
| } | |
| async function importSizesToAccount(accountId, sizes) { | |
| try { | |
| for (let i = 0; i < sizes.length; i++) { | |
| logger.info(`Uploading size ${i+1}/${sizes.length} to account ${accountId}...`); | |
| await uploadSize(accountId, sizes[i]); | |
| } | |
| logger.success(`Imported ${sizes.length} sizes to account ${accountId}`); | |
| return { success: true }; | |
| } catch (error) { | |
| logger.error(`Error importing sizes to account ${accountId}: ${error}`); | |
| return { success: false, error }; | |
| } | |
| } | |
| function reloadPageWithPreset(presetId) { | |
| const urlObj = new URL(window.location.href); | |
| urlObj.searchParams.set("column_preset", presetId); | |
| window.location.href = urlObj.toString(); | |
| } | |
| // ============================================ | |
| // Column Presets Manager UI Class | |
| // ============================================ | |
| class ColumnPresetsManagerUI { | |
| constructor() { | |
| this.div = null; | |
| this.buttons = {}; | |
| this.selectedExportAccountId = null; | |
| this.selectedImportAccountIds = []; | |
| this.logArea = null; | |
| this.accountPresets = []; // Presets loaded for selected account | |
| this.selectedPreset = null; // Currently selected preset | |
| this.accountSizes = []; // Column sizes for selected account | |
| } | |
| createDiv() { | |
| this.div = document.createElement("div"); | |
| this.div.style.position = "fixed"; | |
| this.div.style.top = "50%"; | |
| this.div.style.left = "50%"; | |
| this.div.style.transform = "translate(-50%, -50%)"; | |
| this.div.style.width = "400px"; | |
| this.div.style.maxHeight = "90vh"; | |
| this.div.style.overflowY = "auto"; | |
| this.div.style.backgroundColor = "yellow"; | |
| this.div.style.zIndex = "1000"; | |
| this.div.style.display = "flex"; | |
| this.div.style.flexDirection = "column"; | |
| this.div.style.alignItems = "center"; | |
| this.div.style.justifyContent = "flex-start"; | |
| this.div.style.padding = "20px"; | |
| this.div.style.boxSizing = "border-box"; | |
| this.div.style.borderRadius = "10px"; | |
| this.div.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.2)"; | |
| // Create and style the title | |
| const title = document.createElement("div"); | |
| title.innerHTML = `<h2>FB Column Preset Manager ${Config.VERSION}</h2><p><a href='https://yellowweb.top' target='_blank'>by Yellow Web</a></p>`; | |
| title.style.textAlign = "center"; | |
| title.style.marginBottom = "20px"; | |
| // Create and style the close button | |
| const closeButton = document.createElement("button"); | |
| closeButton.innerHTML = "X"; | |
| closeButton.style.position = "absolute"; | |
| closeButton.style.top = "10px"; | |
| closeButton.style.right = "10px"; | |
| closeButton.style.border = "none"; | |
| closeButton.style.background = "none"; | |
| closeButton.style.fontSize = "18px"; | |
| closeButton.style.cursor = "pointer"; | |
| closeButton.onclick = () => { | |
| document.body.removeChild(this.div); | |
| }; | |
| this.div.appendChild(title); | |
| this.div.appendChild(closeButton); | |
| return this.div; | |
| } | |
| createButton(id, text, onClick) { | |
| const button = document.createElement("button"); | |
| button.id = id; | |
| button.textContent = text; | |
| button.style.margin = "10px 0"; | |
| button.style.padding = "10px 15px"; | |
| button.style.width = "100%"; | |
| button.style.backgroundColor = "#4CAF50"; | |
| button.style.color = "white"; | |
| button.style.border = "none"; | |
| button.style.borderRadius = "5px"; | |
| button.style.cursor = "pointer"; | |
| button.style.fontSize = "16px"; | |
| button.setAttribute("data-original-text", text); | |
| this.buttons[id] = button; | |
| button.onclick = async () => { | |
| this.setButtonLoading(id, true); | |
| try { | |
| await onClick(); | |
| } finally { | |
| this.setButtonLoading(id, false); | |
| } | |
| }; | |
| return button; | |
| } | |
| setButtonLoading(id, isLoading) { | |
| const button = this.buttons[id]; | |
| if (!button) return; | |
| if (isLoading) { | |
| button.disabled = true; | |
| button.style.opacity = "0.7"; | |
| button.style.cursor = "not-allowed"; | |
| button.textContent = "Working on it..."; | |
| } else { | |
| button.disabled = false; | |
| button.style.opacity = "1"; | |
| button.style.cursor = "pointer"; | |
| button.textContent = button.getAttribute("data-original-text"); | |
| } | |
| } | |
| createExportAccountDropdown() { | |
| const container = document.createElement("div"); | |
| container.style.width = "100%"; | |
| container.style.margin = "10px 0"; | |
| const label = document.createElement("label"); | |
| label.textContent = "Select account to export from:"; | |
| label.style.display = "block"; | |
| label.style.marginBottom = "5px"; | |
| label.style.fontSize = "14px"; | |
| label.style.fontWeight = "bold"; | |
| const select = document.createElement("select"); | |
| select.id = "ywbExportAccountSelect"; | |
| select.style.width = "100%"; | |
| select.style.padding = "8px"; | |
| select.style.borderRadius = "5px"; | |
| select.style.border = "1px solid #ccc"; | |
| select.style.fontSize = "14px"; | |
| const defaultOption = document.createElement("option"); | |
| defaultOption.value = ""; | |
| defaultOption.textContent = "-- Choose an account --"; | |
| defaultOption.disabled = true; | |
| defaultOption.selected = true; | |
| select.appendChild(defaultOption); | |
| allAccountsData.forEach(account => { | |
| const option = document.createElement("option"); | |
| option.value = account.id; | |
| option.textContent = `${account.id} - ${account.name}`; | |
| select.appendChild(option); | |
| }); | |
| select.onchange = async () => { | |
| this.selectedExportAccountId = select.value; | |
| this.selectedPreset = null; | |
| this.accountPresets = []; | |
| this.accountSizes = []; | |
| // Load presets for selected account | |
| if (select.value) { | |
| try { | |
| const { presets, sizes } = await fetchAccountPresets(select.value); | |
| this.accountPresets = presets; | |
| this.accountSizes = sizes; | |
| this.refreshPresetDropdown(); | |
| } catch (error) { | |
| logger.error(`Error loading presets: ${error}`); | |
| } | |
| } else { | |
| this.refreshPresetDropdown(); | |
| } | |
| }; | |
| container.appendChild(label); | |
| container.appendChild(select); | |
| return container; | |
| } | |
| createPresetDropdown() { | |
| const container = document.createElement("div"); | |
| container.style.width = "100%"; | |
| container.style.margin = "10px 0"; | |
| const label = document.createElement("label"); | |
| label.textContent = "Select preset to export:"; | |
| label.style.display = "block"; | |
| label.style.marginBottom = "5px"; | |
| label.style.fontSize = "14px"; | |
| label.style.fontWeight = "bold"; | |
| const select = document.createElement("select"); | |
| select.id = "ywbPresetSelect"; | |
| select.style.width = "100%"; | |
| select.style.padding = "8px"; | |
| select.style.borderRadius = "5px"; | |
| select.style.border = "1px solid #ccc"; | |
| select.style.fontSize = "14px"; | |
| const defaultOption = document.createElement("option"); | |
| defaultOption.value = ""; | |
| defaultOption.textContent = "-- Select account first --"; | |
| defaultOption.disabled = true; | |
| defaultOption.selected = true; | |
| select.appendChild(defaultOption); | |
| select.onchange = () => { | |
| const selectedIndex = parseInt(select.value, 10); | |
| if (!isNaN(selectedIndex) && this.accountPresets[selectedIndex]) { | |
| this.selectedPreset = this.accountPresets[selectedIndex]; | |
| } else { | |
| this.selectedPreset = null; | |
| } | |
| }; | |
| container.appendChild(label); | |
| container.appendChild(select); | |
| return container; | |
| } | |
| refreshPresetDropdown() { | |
| const select = document.getElementById("ywbPresetSelect"); | |
| if (!select) return; | |
| select.innerHTML = ""; | |
| const defaultOption = document.createElement("option"); | |
| defaultOption.value = ""; | |
| defaultOption.disabled = true; | |
| defaultOption.selected = true; | |
| if (this.accountPresets.length === 0) { | |
| defaultOption.textContent = this.selectedExportAccountId | |
| ? "-- No presets available --" | |
| : "-- Select account first --"; | |
| select.appendChild(defaultOption); | |
| return; | |
| } | |
| defaultOption.textContent = "-- Choose a preset --"; | |
| select.appendChild(defaultOption); | |
| this.accountPresets.forEach((preset, index) => { | |
| const option = document.createElement("option"); | |
| option.value = index; | |
| option.textContent = preset.name; | |
| select.appendChild(option); | |
| }); | |
| } | |
| createImportAccountDropdown() { | |
| const container = document.createElement("div"); | |
| container.style.width = "100%"; | |
| container.style.margin = "10px 0"; | |
| const label = document.createElement("label"); | |
| label.textContent = "Select accounts to import to:"; | |
| label.style.display = "block"; | |
| label.style.marginBottom = "5px"; | |
| label.style.fontSize = "14px"; | |
| label.style.fontWeight = "bold"; | |
| const selectAllContainer = document.createElement("div"); | |
| selectAllContainer.style.marginBottom = "5px"; | |
| const selectAllCheckbox = document.createElement("input"); | |
| selectAllCheckbox.type = "checkbox"; | |
| selectAllCheckbox.id = "ywbSelectAllAccounts"; | |
| selectAllCheckbox.style.marginRight = "5px"; | |
| const selectAllLabel = document.createElement("label"); | |
| selectAllLabel.htmlFor = "ywbSelectAllAccounts"; | |
| selectAllLabel.textContent = "Select All Accounts"; | |
| selectAllLabel.style.fontSize = "14px"; | |
| selectAllContainer.appendChild(selectAllCheckbox); | |
| selectAllContainer.appendChild(selectAllLabel); | |
| const select = document.createElement("select"); | |
| select.id = "ywbImportAccountSelect"; | |
| select.multiple = true; | |
| select.size = Math.min(allAccountsData.length, 8); | |
| select.style.width = "100%"; | |
| select.style.padding = "5px"; | |
| select.style.borderRadius = "5px"; | |
| select.style.border = "1px solid #ccc"; | |
| select.style.fontSize = "12px"; | |
| allAccountsData.forEach(account => { | |
| const option = document.createElement("option"); | |
| option.value = account.id; | |
| option.textContent = `${account.id} - ${account.name}`; | |
| select.appendChild(option); | |
| }); | |
| const updateSelection = () => { | |
| this.selectedImportAccountIds = Array.from(select.selectedOptions).map(opt => opt.value); | |
| }; | |
| select.onchange = updateSelection; | |
| selectAllCheckbox.onchange = () => { | |
| if (selectAllCheckbox.checked) { | |
| Array.from(select.options).forEach(opt => opt.selected = true); | |
| } else { | |
| Array.from(select.options).forEach(opt => opt.selected = false); | |
| } | |
| updateSelection(); | |
| }; | |
| container.appendChild(label); | |
| container.appendChild(selectAllContainer); | |
| container.appendChild(select); | |
| return container; | |
| } | |
| refreshDropdowns() { | |
| const exportSelect = document.getElementById("ywbExportAccountSelect"); | |
| const importSelect = document.getElementById("ywbImportAccountSelect"); | |
| if (exportSelect) { | |
| const currentValue = exportSelect.value; | |
| exportSelect.innerHTML = ""; | |
| const defaultOption = document.createElement("option"); | |
| defaultOption.value = ""; | |
| defaultOption.textContent = "-- Choose an account --"; | |
| defaultOption.disabled = true; | |
| defaultOption.selected = !currentValue; | |
| exportSelect.appendChild(defaultOption); | |
| allAccountsData.forEach(account => { | |
| const option = document.createElement("option"); | |
| option.value = account.id; | |
| option.textContent = `${account.id} - ${account.name}`; | |
| if (account.id === currentValue) { | |
| option.selected = true; | |
| } | |
| exportSelect.appendChild(option); | |
| }); | |
| } | |
| if (importSelect) { | |
| const currentValues = Array.from(importSelect.selectedOptions).map(opt => opt.value); | |
| importSelect.innerHTML = ""; | |
| allAccountsData.forEach(account => { | |
| const option = document.createElement("option"); | |
| option.value = account.id; | |
| option.textContent = `${account.id} - ${account.name}`; | |
| if (currentValues.includes(account.id)) { | |
| option.selected = true; | |
| } | |
| importSelect.appendChild(option); | |
| }); | |
| } | |
| } | |
| createTabs() { | |
| const tabContainer = document.createElement("div"); | |
| tabContainer.style.display = "flex"; | |
| tabContainer.style.width = "100%"; | |
| tabContainer.style.marginBottom = "15px"; | |
| tabContainer.style.borderBottom = "2px solid #333"; | |
| const exportTab = document.createElement("button"); | |
| exportTab.id = "ywbExportTab"; | |
| exportTab.textContent = "Export"; | |
| exportTab.style.flex = "1"; | |
| exportTab.style.padding = "10px"; | |
| exportTab.style.border = "none"; | |
| exportTab.style.background = "none"; | |
| exportTab.style.cursor = "pointer"; | |
| exportTab.style.fontSize = "14px"; | |
| exportTab.style.fontWeight = "bold"; | |
| exportTab.style.borderBottom = "3px solid #333"; | |
| const importTab = document.createElement("button"); | |
| importTab.id = "ywbImportTab"; | |
| importTab.textContent = "Import"; | |
| importTab.style.flex = "1"; | |
| importTab.style.padding = "10px"; | |
| importTab.style.border = "none"; | |
| importTab.style.background = "none"; | |
| importTab.style.cursor = "pointer"; | |
| importTab.style.fontSize = "14px"; | |
| importTab.style.fontWeight = "bold"; | |
| exportTab.onclick = () => { | |
| exportTab.style.borderBottom = "3px solid #333"; | |
| importTab.style.borderBottom = "none"; | |
| document.getElementById("ywbExportTabContent").style.display = "block"; | |
| document.getElementById("ywbImportTabContent").style.display = "none"; | |
| }; | |
| importTab.onclick = () => { | |
| importTab.style.borderBottom = "3px solid #333"; | |
| exportTab.style.borderBottom = "none"; | |
| document.getElementById("ywbExportTabContent").style.display = "none"; | |
| document.getElementById("ywbImportTabContent").style.display = "block"; | |
| }; | |
| tabContainer.appendChild(exportTab); | |
| tabContainer.appendChild(importTab); | |
| return tabContainer; | |
| } | |
| createLogArea() { | |
| const logContainer = document.createElement("div"); | |
| logContainer.style.width = "100%"; | |
| logContainer.style.marginTop = "15px"; | |
| logContainer.style.borderTop = "2px solid #333"; | |
| logContainer.style.paddingTop = "10px"; | |
| const logLabel = document.createElement("div"); | |
| logLabel.textContent = "Log:"; | |
| logLabel.style.fontSize = "12px"; | |
| logLabel.style.fontWeight = "bold"; | |
| logLabel.style.marginBottom = "5px"; | |
| this.logArea = document.createElement("div"); | |
| this.logArea.id = "ywbLogArea"; | |
| this.logArea.style.width = "100%"; | |
| this.logArea.style.height = "120px"; | |
| this.logArea.style.overflowY = "auto"; | |
| this.logArea.style.backgroundColor = "#f5f5f5"; | |
| this.logArea.style.border = "1px solid #ccc"; | |
| this.logArea.style.borderRadius = "5px"; | |
| this.logArea.style.padding = "8px"; | |
| this.logArea.style.fontSize = "11px"; | |
| this.logArea.style.fontFamily = "monospace"; | |
| this.logArea.style.lineHeight = "1.4"; | |
| logContainer.appendChild(logLabel); | |
| logContainer.appendChild(this.logArea); | |
| return logContainer; | |
| } | |
| log(message, type = "info") { | |
| if (!this.logArea) return; | |
| const logEntry = document.createElement("div"); | |
| logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; | |
| if (type === "error") { | |
| logEntry.style.color = "red"; | |
| } else if (type === "success") { | |
| logEntry.style.color = "green"; | |
| } else if (type === "warning") { | |
| logEntry.style.color = "orange"; | |
| } | |
| this.logArea.appendChild(logEntry); | |
| this.logArea.scrollTop = this.logArea.scrollHeight; | |
| } | |
| clearLog() { | |
| if (this.logArea) { | |
| this.logArea.innerHTML = ""; | |
| } | |
| } | |
| show() { | |
| const div = this.createDiv(); | |
| // Create tabs | |
| const tabs = this.createTabs(); | |
| div.appendChild(tabs); | |
| // Export Tab Content | |
| const exportTabContent = document.createElement("div"); | |
| exportTabContent.id = "ywbExportTabContent"; | |
| exportTabContent.style.width = "100%"; | |
| exportTabContent.style.display = "block"; | |
| const exportDropdown = this.createExportAccountDropdown(); | |
| const presetDropdown = this.createPresetDropdown(); | |
| const exportButton = this.createButton("export-btn", "Export Column Preset to JSON", async () => { | |
| if (!this.selectedExportAccountId) { | |
| alert("Please select an account to export from."); | |
| return; | |
| } | |
| if (!this.selectedPreset) { | |
| alert("Please select a preset to export."); | |
| return; | |
| } | |
| await exportColumnPreset(this.selectedPreset, this.accountSizes, this.selectedExportAccountId); | |
| }); | |
| exportTabContent.appendChild(exportDropdown); | |
| exportTabContent.appendChild(presetDropdown); | |
| exportTabContent.appendChild(exportButton); | |
| // Import Tab Content | |
| const importTabContent = document.createElement("div"); | |
| importTabContent.id = "ywbImportTabContent"; | |
| importTabContent.style.width = "100%"; | |
| importTabContent.style.display = "none"; | |
| const importDropdown = this.createImportAccountDropdown(); | |
| const importSizesCheckbox = document.createElement("div"); | |
| importSizesCheckbox.style.display = "flex"; | |
| importSizesCheckbox.style.alignItems = "center"; | |
| importSizesCheckbox.style.margin = "10px 0"; | |
| importSizesCheckbox.style.width = "100%"; | |
| const sizesCheckbox = document.createElement("input"); | |
| sizesCheckbox.type = "checkbox"; | |
| sizesCheckbox.id = "ywbImportSizes"; | |
| sizesCheckbox.style.marginRight = "10px"; | |
| const sizesLabel = document.createElement("label"); | |
| sizesLabel.htmlFor = "ywbImportSizes"; | |
| sizesLabel.textContent = "Also import column sizes"; | |
| sizesLabel.style.fontSize = "14px"; | |
| importSizesCheckbox.appendChild(sizesCheckbox); | |
| importSizesCheckbox.appendChild(sizesLabel); | |
| const importButton = this.createButton("import-btn", "Import Column Preset to Selected Accounts", async () => { | |
| if (!this.selectedImportAccountIds || this.selectedImportAccountIds.length === 0) { | |
| alert("Please select at least one account to import to."); | |
| return; | |
| } | |
| const fileHelper = new FileHelper(); | |
| const fileSelector = new FileSelector(file => fileHelper.readFileAsJsonAsync(file)); | |
| try { | |
| logger.info("Opening file selector..."); | |
| const presetContent = await fileSelector.show(); | |
| if (!presetContent || !presetContent.preset) { | |
| logger.error("Invalid file format. Expected a JSON file with 'preset' object."); | |
| return; | |
| } | |
| await importPresetToSelectedAccounts(this.selectedImportAccountIds, presetContent, this); | |
| // Import sizes if checkbox is checked | |
| const importSizes = document.getElementById("ywbImportSizes").checked; | |
| if (importSizes && presetContent.sizes && presetContent.sizes.length > 0) { | |
| for (const accountId of this.selectedImportAccountIds) { | |
| await importSizesToAccount(accountId, presetContent.sizes); | |
| } | |
| } | |
| // Only ask for reload if current account was in the import list | |
| const currentAccountId = require("BusinessUnifiedNavigationContext").adAccountID; | |
| if (this.selectedImportAccountIds.includes(currentAccountId)) { | |
| if (confirm("Column presets in current account changed, reload?")) { | |
| location.reload(); | |
| } | |
| } | |
| logger.success("Import complete!"); | |
| } catch (error) { | |
| logger.error(`Error: ${error}`); | |
| } | |
| }); | |
| importTabContent.appendChild(importDropdown); | |
| importTabContent.appendChild(importSizesCheckbox); | |
| importTabContent.appendChild(importButton); | |
| // Add tab contents to div | |
| div.appendChild(exportTabContent); | |
| div.appendChild(importTabContent); | |
| // Add log area | |
| const logArea = this.createLogArea(); | |
| div.appendChild(logArea); | |
| // Create a small link for copying as bookmark | |
| const copyBookmarkLink = document.createElement("a"); | |
| copyBookmarkLink.href = "#"; | |
| copyBookmarkLink.textContent = "Copy as bookmark"; | |
| copyBookmarkLink.style.fontSize = "12px"; | |
| copyBookmarkLink.style.color = "blue"; | |
| copyBookmarkLink.style.textDecoration = "underline"; | |
| copyBookmarkLink.style.cursor = "pointer"; | |
| copyBookmarkLink.style.marginTop = "10px"; | |
| copyBookmarkLink.style.display = "block"; | |
| copyBookmarkLink.style.textAlign = "center"; | |
| copyBookmarkLink.onclick = (e) => { | |
| e.preventDefault(); | |
| copyScriptAsBase64Bookmarklet(); | |
| }; | |
| div.appendChild(copyBookmarkLink); | |
| // Add div to body | |
| document.body.appendChild(div); | |
| // Initial log message | |
| this.log("UI initialized. Ready to work.", "success"); | |
| } | |
| } | |
| // ============================================ | |
| // Main function to show the column presets manager UI | |
| // ============================================ | |
| async function showColumnPresetsManager() { | |
| try { | |
| // Show loading message | |
| const loadingDiv = document.createElement("div"); | |
| loadingDiv.style.position = "fixed"; | |
| loadingDiv.style.top = "50%"; | |
| loadingDiv.style.left = "50%"; | |
| loadingDiv.style.transform = "translate(-50%, -50%)"; | |
| loadingDiv.style.padding = "20px"; | |
| loadingDiv.style.backgroundColor = "yellow"; | |
| loadingDiv.style.borderRadius = "10px"; | |
| loadingDiv.style.zIndex = "1000"; | |
| loadingDiv.style.fontSize = "16px"; | |
| loadingDiv.style.fontWeight = "bold"; | |
| loadingDiv.textContent = "Loading accounts..."; | |
| document.body.appendChild(loadingDiv); | |
| // Load all accounts | |
| await accountManager.loadAll(); | |
| // Remove loading message | |
| document.body.removeChild(loadingDiv); | |
| // Show UI | |
| const ui = new ColumnPresetsManagerUI(); | |
| logger.setUI(ui); | |
| ui.show(); | |
| } catch (error) { | |
| console.error("Error loading accounts:", error); | |
| alert(`Error loading accounts: ${error.message || error}`); | |
| } | |
| } | |
| // Function to copy the script as base64 bookmarklet | |
| function copyScriptAsBase64Bookmarklet() { | |
| try { | |
| const configStr = `const Config = ${JSON.stringify(Config)};`; | |
| const scriptContent = `// FB Column Preset Manager ${Config.VERSION} | |
| ${configStr} | |
| ${Logger.toString()} | |
| const logger = new Logger(); | |
| ${FileHelper.toString()} | |
| ${FileSelector.toString()} | |
| ${FbApi.toString()} | |
| const API = new FbApi(); | |
| ${AccountManager.toString()} | |
| const accountManager = new AccountManager(); | |
| let allAccountsData = new Proxy({}, { | |
| get(target, prop) { | |
| if (typeof prop === 'symbol') return undefined; | |
| const accounts = accountManager.getAll(); | |
| if (prop === 'length') return accounts.length; | |
| if (prop === 'find') return accounts.find.bind(accounts); | |
| if (prop === 'map') return accounts.map.bind(accounts); | |
| if (prop === 'filter') return accounts.filter.bind(accounts); | |
| if (prop === 'forEach') return accounts.forEach.bind(accounts); | |
| return accounts[prop]; | |
| } | |
| }); | |
| const CUSTOM_METRIC_PREFIX = "custom_derived_metrics:"; | |
| ${fetchCustomMetrics.toString()} | |
| ${createCustomMetric.toString()} | |
| ${extractCustomMetricIds.toString()} | |
| ${replaceCustomMetricIds.toString()} | |
| ${fetchAccountPresets.toString()} | |
| ${exportColumnPreset.toString()} | |
| ${fetchUserSettingsId.toString()} | |
| ${uploadPreset.toString()} | |
| ${setDefaultColumnPreset.toString()} | |
| ${uploadSize.toString()} | |
| ${importPresetToAccount.toString()} | |
| ${importPresetToSelectedAccounts.toString()} | |
| ${importSizesToAccount.toString()} | |
| ${reloadPageWithPreset.toString()} | |
| ${ColumnPresetsManagerUI.toString()} | |
| ${showColumnPresetsManager.toString()} | |
| ${copyScriptAsBase64Bookmarklet.toString()} | |
| window.showColumnPresetsManager = showColumnPresetsManager; | |
| window.copyScriptAsBase64Bookmarklet = copyScriptAsBase64Bookmarklet; | |
| showColumnPresetsManager();`; | |
| const base64Content = btoa(unescape(encodeURIComponent(scriptContent))); | |
| const bookmarkletCode = `javascript:eval(decodeURIComponent(escape(atob("${base64Content}"))));`; | |
| navigator.clipboard.writeText(bookmarkletCode) | |
| .then(() => { | |
| alert("Bookmarklet copied to clipboard!"); | |
| }) | |
| .catch(err => { | |
| console.error('Failed to copy: ', err); | |
| const textArea = document.createElement("textarea"); | |
| textArea.value = bookmarkletCode; | |
| document.body.appendChild(textArea); | |
| textArea.select(); | |
| document.execCommand("copy"); | |
| document.body.removeChild(textArea); | |
| alert("Bookmarklet copied to clipboard!"); | |
| }); | |
| } catch (error) { | |
| console.error('Error creating bookmarklet:', error); | |
| alert(`Error creating bookmarklet: ${error.message}`); | |
| } | |
| } | |
| // Make the functions available globally | |
| window.showColumnPresetsManager = showColumnPresetsManager; | |
| window.copyScriptAsBase64Bookmarklet = copyScriptAsBase64Bookmarklet; | |
| // Auto-run when script is loaded | |
| showColumnPresetsManager(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
javascript:eval("(async () => {" + atob("Y2xhc3MgRmlsZVNlbGVjdG9yIHsKICBjb25zdHJ1Y3RvcihmaWxlUHJvY2Vzc29yKSB7CiAgICB0aGlzLmZpbGVQcm9jZXNzb3IgPSBmaWxlUHJvY2Vzc29yOwogIH0KCiAgY3JlYXRlRGl2KCkgewogICAgdGhpcy5kaXYgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJkaXYiKTsKICAgIHRoaXMuZGl2LnN0eWxlLnBvc2l0aW9uID0gImZpeGVkIjsKICAgIHRoaXMuZGl2LnN0eWxlLnRvcCA9ICI1MCUiOwogICAgdGhpcy5kaXYuc3R5bGUubGVmdCA9ICI1MCUiOwogICAgdGhpcy5kaXYuc3R5bGUudHJhbnNmb3JtID0gInRyYW5zbGF0ZSgtNTAlLCAtNTAlKSI7CiAgICB0aGlzLmRpdi5zdHlsZS53aWR0aCA9ICIyMDBweCI7IC8vIFVwZGF0ZWQgd2lkdGgKICAgIHRoaXMuZGl2LnN0eWxlLmhlaWdodCA9ICIxMjBweCI7CiAgICB0aGlzLmRpdi5zdHlsZS5iYWNrZ3JvdW5kQ29sb3IgPSAieWVsbG93IjsKICAgIHRoaXMuZGl2LnN0eWxlLnpJbmRleCA9ICIxMDAwIjsKICAgIHRoaXMuZGl2LnN0eWxlLmRpc3BsYXkgPSAiZmxleCI7CiAgICB0aGlzLmRpdi5zdHlsZS5mbGV4RGlyZWN0aW9uID0gImNvbHVtbiI7IC8vIFRvIGFsaWduIGl0ZW1zIHZlcnRpY2FsbHkKICAgIHRoaXMuZGl2LnN0eWxlLmFsaWduSXRlbXMgPSAiY2VudGVyIjsKICAgIHRoaXMuZGl2LnN0eWxlLmp1c3RpZnlDb250ZW50ID0gImNlbnRlciI7CiAgICB0aGlzLmRpdi5zdHlsZS5wYWRkaW5nID0gIjEwcHgiOyAvLyBBZGRlZCBwYWRkaW5nCiAgICB0aGlzLmRpdi5zdHlsZS5ib3hTaXppbmcgPSAiYm9yZGVyLWJveCI7IC8vIFRvIGluY2x1ZGUgcGFkZGluZyBpbiB3aWR0aC9oZWlnaHQKICAgIHRoaXMuZGl2LnN0eWxlLmJvcmRlclJhZGl1cyA9ICIxMHB4IjsKCiAgICAvLyBDcmVhdGUgYW5kIHN0eWxlIHRoZSB0aXRsZQogICAgdmFyIHRpdGxlID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiZGl2Iik7CiAgICB0aXRsZS5pbm5lckhUTUwgPQogICAgICAiPGJyPkZCIENvbHVtbiBQcmVzZXQgTWFuYWdlciB2Mi4xPGJyPlRFQU0gRWRpdGlvbjxicj5ieSBZZWxsb3cgV2ViPGJyPjxicj4iOwogICAgdGl0bGUuc3R5bGUudGV4dEFsaWduID0gImNlbnRlciI7CiAgICB0aXRsZS5zdHlsZS5mb250V2VpZ2h0ID0gImJvbGQiOwoKICAgIC8vIENyZWF0ZSBhbmQgc3R5bGUgdGhlIGNsb3NlIGJ1dHRvbgogICAgdmFyIGNsb3NlQnV0dG9uID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiYnV0dG9uIik7CiAgICBjbG9zZUJ1dHRvbi5pbm5lckhUTUwgPSAiWCI7CiAgICBjbG9zZUJ1dHRvbi5zdHlsZS5wb3NpdGlvbiA9ICJhYnNvbHV0ZSI7CiAgICBjbG9zZUJ1dHRvbi5zdHlsZS50b3AgPSAiNXB4IjsKICAgIGNsb3NlQnV0dG9uLnN0eWxlLnJpZ2h0ID0gIjVweCI7CiAgICBjbG9zZUJ1dHRvbi5zdHlsZS5ib3JkZXIgPSAibm9uZSI7CiAgICBjbG9zZUJ1dHRvbi5zdHlsZS5iYWNrZ3JvdW5kID0gIm5vbmUiOwogICAgY2xvc2VCdXR0b24uc3R5bGUuY3Vyc29yID0gInBvaW50ZXIiOwogICAgY2xvc2VCdXR0b24ub25jbGljayA9ICgpID0+IHsKICAgICAgZG9jdW1lbnQuYm9keS5yZW1vdmVDaGlsZCh0aGlzLmRpdik7CiAgICB9OwoKICAgIHRoaXMuZGl2LmFwcGVuZENoaWxkKHRpdGxlKTsKICAgIHRoaXMuZGl2LmFwcGVuZENoaWxkKGNsb3NlQnV0dG9uKTsKICB9CgogIGNyZWF0ZUZpbGVJbnB1dCgpIHsKICAgIC8vIENyZWF0ZSB0aGUgZmlsZSBpbnB1dCBhbmQgaGFuZGxlIGZpbGUgc2VsZWN0aW9uCiAgICB0aGlzLmZpbGVJbnB1dCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImlucHV0Iik7CiAgICB0aGlzLmZpbGVJbnB1dC50eXBlID0gImZpbGUiOwogICAgdGhpcy5maWxlSW5wdXQuYWNjZXB0ID0gIi5qc29uIjsgLy8gQWNjZXB0IG9ubHkgSlNPTiBmaWxlcwogICAgdGhpcy5maWxlSW5wdXQuc3R5bGUuZGlzcGxheSA9ICJub25lIjsKICB9CgogIGNyZWF0ZUJ1dHRvbigpIHsKICAgIC8vIENyZWF0ZSB0aGUgYnV0dG9uCiAgICB0aGlzLmJ1dHRvbiA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImJ1dHRvbiIpOwogICAgdGhpcy5idXR0b24udGV4dENvbnRlbnQgPSAiT3BlbiBQcmVzZXQiOwogICAgdGhpcy5idXR0b24ub25jbGljayA9ICgpID0+IHsKICAgICAgdGhpcy5maWxlSW5wdXQuY2xpY2soKTsKICAgIH07CiAgfQoKICBzaG93KCkgewogICAgcmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHsKICAgICAgdGhpcy5jcmVhdGVEaXYoKTsKICAgICAgdGhpcy5jcmVhdGVGaWxlSW5wdXQoKTsKICAgICAgdGhpcy5jcmVhdGVCdXR0b24oKTsKCiAgICAgIC8vIEFwcGVuZCBlbGVtZW50cyB0byB0aGUgZGl2IGFuZCB0aGUgZGl2IHRvIHRoZSBib2R5CiAgICAgIHRoaXMuZGl2LmFwcGVuZENoaWxkKHRoaXMuYnV0dG9uKTsKICAgICAgdGhpcy5kaXYuYXBwZW5kQ2hpbGQodGhpcy5maWxlSW5wdXQpOwogICAgICBkb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKHRoaXMuZGl2KTsKCiAgICAgIHRoaXMuZmlsZUlucHV0Lm9uY2hhbmdlID0gYXN5bmMgKCkgPT4gewogICAgICAgIC8vIElmIG5vIGZpbGUgaXMgc2VsZWN0ZWQgKHVzZXIgY2FuY2VsbGVkKQogICAgICAgIGlmICghdGhpcy5maWxlSW5wdXQuZmlsZXMgfHwgdGhpcy5maWxlSW5wdXQuZmlsZXMubGVuZ3RoID09PSAwKSB7CiAgICAgICAgICBkb2N1bWVudC5ib2R5LnJlbW92ZUNoaWxkKHRoaXMuZGl2KTsKICAgICAgICAgIGFsZXJ0KCJPcGVyYXRpb24gY2FuY2VsZWQiKTsKICAgICAgICAgIHJlamVjdCgiRmlsZSBzZWxlY3Rpb24gY2FuY2VsbGVkIGJ5IHVzZXIiKTsKICAgICAgICAgIHJldHVybjsKICAgICAgICB9CgogICAgICAgIHRyeSB7CiAgICAgICAgICAvLyBQcm9jZXNzIHRoZSBmaWxlIGFuZCByZXNvbHZlIHRoZSBwcm9taXNlCiAgICAgICAgICBjb25zdCByZXN1bHQgPSBhd2FpdCB0aGlzLmZpbGVQcm9jZXNzb3IodGhpcy5maWxlSW5wdXQuZmlsZXNbMF0pOwogICAgICAgICAgZG9jdW1lbnQuYm9keS5yZW1vdmVDaGlsZCh0aGlzLmRpdik7CiAgICAgICAgICByZXNvbHZlKHJlc3VsdCk7CiAgICAgICAgfSBjYXRjaCAoZXJyb3IpIHsKICAgICAgICAgIC8vIEhhbmRsZSBhbnkgZXJyb3JzIGluIHByb2Nlc3NpbmcKICAgICAgICAgIGRvY3VtZW50LmJvZHkucmVtb3ZlQ2hpbGQodGhpcy5kaXYpOwogICAgICAgICAgcmVqZWN0KGVycm9yKTsKICAgICAgICB9CiAgICAgIH07CiAgICB9KTsKICB9Cn0KCmNsYXNzIEZpbGVIZWxwZXIgewogIGFzeW5jIHJlYWRGaWxlQXNKc29uQXN5bmMoZmlsZSkgewogICAgdHJ5IHsKICAgICAgY29uc3QgZmlsZUNvbnRlbnQgPSBhd2FpdCB0aGlzLnJlYWRGaWxlQXN5bmMoZmlsZSk7CiAgICAgIHJldHVybiBKU09OLnBhcnNlKGZpbGVDb250ZW50KTsKICAgIH0gY2F0Y2ggKGVycm9yKSB7CiAgICAgIGNvbnNvbGUuZXJyb3IoIkVycm9yOiIsIGVycm9yKTsKICAgICAgdGhyb3cgZXJyb3I7CiAgICB9CiAgfQoKICByZWFkRmlsZUFzeW5jKGZpbGUpIHsKICAgIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7CiAgICAgIGxldCByZWFkZXIgPSBuZXcgRmlsZVJlYWRlcigpOwoKICAgICAgcmVhZGVyLm9ubG9hZCA9ICgpID0+IHsKICAgICAgICByZXNvbHZlKHJlYWRlci5yZXN1bHQpOwogICAgICB9OwoKICAgICAgcmVhZGVyLm9uZXJyb3IgPSAoKSA9PiB7CiAgICAgICAgcmVqZWN0KCJFcnJvciByZWFkaW5nIGZpbGUiKTsKICAgICAgfTsKCiAgICAgIHJlYWRlci5yZWFkQXNUZXh0KGZpbGUpOyAvLyBSZWFkIHRoZSBmaWxlIGFzIHRleHQKICAgIH0pOwogIH0KfQoKY2xhc3MgRmJBcGkgewogIGFwaVVybCA9ICJodHRwczovL2Fkc21hbmFnZXItZ3JhcGguZmFjZWJvb2suY29tL3YxOC4wLyI7CiAgZGVidWcgPSBmYWxzZTsKCiAgY29uc3RydWN0b3IoZGVidWcpIHsKICAgIHRoaXMuZGVidWcgPSBkZWJ1ZzsKICB9CgogIGFzeW5jIGdldFJlcXVlc3QocGF0aCwgcXMsIHRva2VuID0gbnVsbCkgewogICAgcGF0aCA9IHBhdGguc3RhcnRzV2l0aCh0aGlzLmFwaVVybCkgPyBwYXRoIDogYCR7dGhpcy5hcGlVcmx9JHtwYXRofWA7CiAgICB0b2tlbiA9IHRva2VuID8/IF9fYWNjZXNzVG9rZW47CiAgICBsZXQgdXJsID0gcGF0aDsKICAgIGlmICghcGF0aC5pbmNsdWRlcyh0b2tlbikpIHsKICAgICAgbGV0IHFzUGFydCA9IHFzID09PSBudWxsIHx8IHFzID09PSAiIiA/ICI/IiA6IGA/JHtxc30mYDsKICAgICAgdXJsID0gYCR7cGF0aH0ke3FzUGFydH1hY2Nlc3NfdG9rZW49JHt0b2tlbn1gOwogICAgfQogICAgbGV0IGYgPSBhd2FpdCBmZXRjaCh1cmwsIHsKICAgICAgaGVhZGVyczogewogICAgICAgIGFjY2VwdDoKICAgICAgICAgICJ0ZXh0L2h0bWwsYXBwbGljYXRpb24veGh0bWwreG1sLGFwcGxpY2F0aW9uL3htbDtxPTAuOSxpbWFnZS9hdmlmLGltYWdlL3dlYnAsaW1hZ2UvYXBuZywqLyo7cT0wLjgsYXBwbGljYXRpb24vc2lnbmVkLWV4Y2hhbmdlO3Y9YjM7cT0wLjkiLAogICAgICAgICJhY2NlcHQtbGFuZ3VhZ2UiOiAiY2EtRVMsY2E7cT0wLjksZW4tVVM7cT0wLjgsZW47cT0wLjciLAogICAgICAgICJjYWNoZS1jb250cm9sIjogIm1heC1hZ2U9MCIsCiAgICAgICAgInNlYy1jaC11YSI6CiAgICAgICAgICAnIk5vdD9BX0JyYW5kIjt2PSI4IiwgIkNocm9taXVtIjt2PSIxMDgiLCAiR29vZ2xlIENocm9tZSI7dj0iMTA4IicsCiAgICAgICAgInNlYy1jaC11YS1tb2JpbGUiOiAiPzAiLAogICAgICAgICJzZWMtY2gtdWEtcGxhdGZvcm0iOiAnIldpbmRvd3MiJywKICAgICAgICAic2VjLWZldGNoLWRlc3QiOiAiZW1wdHkiLAogICAgICAgICJzZWMtZmV0Y2gtbW9kZSI6ICJjb3JzIiwKICAgICAgICAic2VjLWZldGNoLXNpdGUiOiAic2FtZS1zaXRlIiwKICAgICAgfSwKICAgICAgcmVmZXJyZXJQb2xpY3k6ICJzdHJpY3Qtb3JpZ2luLXdoZW4tY3Jvc3Mtb3JpZ2luIiwKICAgICAgYm9keTogbnVsbCwKICAgICAgbWV0aG9kOiAiR0VUIiwKICAgICAgbW9kZTogImNvcnMiLAogICAgICBjcmVkZW50aWFsczogImluY2x1ZGUiLAogICAgICByZWZlcnJlcjogImh0dHBzOi8vYnVzaW5lc3MuZmFjZWJvb2suY29tLyIsCiAgICAgIHJlZmVycmVyUG9saWN5OiAib3JpZ2luLXdoZW4tY3Jvc3Mtb3JpZ2luIiwKICAgIH0pOwogICAgbGV0IGpzb24gPSBhd2FpdCBmLmpzb24oKTsKICAgIGlmICh0aGlzLmRlYnVnKSBjb25zb2xlLmxvZyhKU09OLnN0cmluZ2lmeShqc29uKSk7CiAgICByZXR1cm4ganNvbjsKICB9CgogIGFzeW5jIGdldEFsbFBhZ2VzKHBhdGgsIHFzLCB0b2tlbiA9IG51bGwpIHsKICAgIGxldCBpdGVtcyA9IFtdOwogICAgbGV0IHBhZ2UgPSBhd2FpdCB0aGlzLmdldFJlcXVlc3QocGF0aCwgcXMsIHRva2VuKTsKICAgIGl0ZW1zID0gaXRlbXMuY29uY2F0KHBhZ2UuZGF0YSk7CgogICAgbGV0IGkgPSAyOwogICAgd2hpbGUgKHBhZ2UucGFnaW5nICYmIHBhZ2UucGFnaW5nLm5leHQpIHsKICAgICAgaWYgKHRoaXMuZGVidWcpIGNvbnNvbGUubG9nKGBHZXR0aW5nIHBhZ2UgIyR7aX0uLi5gKTsKICAgICAgcGFnZSA9IGF3YWl0IHRoaXMuZ2V0UmVxdWVzdChwYWdlLnBhZ2luZy5uZXh0LCAiIiwgdG9rZW4pOwogICAgICBpdGVtcyA9IGl0ZW1zLmNvbmNhdChwYWdlLmRhdGEpOwogICAgICBpKys7CiAgICB9CgogICAgcmV0dXJuIGl0ZW1zOwogIH0KCiAgYXN5bmMgcG9zdFJlcXVlc3QocGF0aCwgYm9keSwgdG9rZW4gPSBudWxsKSB7CiAgICB0b2tlbiA9IHRva2VuID8/IF9fYWNjZXNzVG9rZW47CiAgICBib2R5WyJhY2Nlc3NfdG9rZW4iXSA9IHRva2VuOwogICAgbGV0IGhlYWRlcnMgPSB7CiAgICAgIGFjY2VwdDogIiovKiIsCiAgICAgICJhY2NlcHQtbGFuZ3VhZ2UiOiAiZW4tVVMsZW47cT0wLjkiLAogICAgICAiY29udGVudC10eXBlIjogImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCIsCiAgICAgICJzZWMtY2gtdWEiOgogICAgICAgICciR29vZ2xlIENocm9tZSI7dj0iMTA3IiwgIkNocm9taXVtIjt2PSIxMDciLCAiTm90PUE/QnJhbmQiO3Y9IjI0IicsCiAgICAgICJzZWMtY2gtdWEtbW9iaWxlIjogIj8wIiwKICAgICAgInNlYy1jaC11YS1wbGF0Zm9ybSI6ICciV2luZG93cyInLAogICAgICAic2VjLWZldGNoLWRlc3QiOiAiZW1wdHkiLAogICAgICAic2VjLWZldGNoLW1vZGUiOiAiY29ycyIsCiAgICAgICJzZWMtZmV0Y2gtc2l0ZSI6ICJzYW1lLXNpdGUiLAogICAgfTsKICAgIGxldCBmID0gYXdhaXQgZmV0Y2goYCR7dGhpcy5hcGlVcmx9JHtwYXRofWAsIHsKICAgICAgaGVhZGVyczogaGVhZGVycywKICAgICAgcmVmZXJyZXI6ICJodHRwczovL2J1c2luZXNzLmZhY2Vib29rLmNvbS8iLAogICAgICByZWZlcnJlclBvbGljeTogIm9yaWdpbi13aGVuLWNyb3NzLW9yaWdpbiIsCiAgICAgIGJvZHk6IG5ldyBVUkxTZWFyY2hQYXJhbXMoYm9keSkudG9TdHJpbmcoKSwKICAgICAgbWV0aG9kOiAiUE9TVCIsCiAgICAgIG1vZGU6ICJjb3JzIiwKICAgICAgY3JlZGVudGlhbHM6ICJpbmNsdWRlIiwKICAgIH0pOwogICAgbGV0IGpzb24gPSBhd2FpdCBmLmpzb24oKTsKICAgIGlmICh0aGlzLmRlYnVnKSBjb25zb2xlLmxvZyhKU09OLnN0cmluZ2lmeShqc29uKSk7CiAgICByZXR1cm4ganNvbjsKICB9Cn0KCmNvbnN0IERFQlVHID0gdHJ1ZTsKY29uc3QgQVBJID0gbmV3IEZiQXBpKERFQlVHKTsKCmFzeW5jIGZ1bmN0aW9uIG1haW4oKSB7CiAgY29uc3QgY2hvaWNlID0gcHJvbXB0KAogICAgYFNlbGVjdCBhbiBvcHRpb246CgoxLiBFeHBvcnQgY29sdW1uIHByZXNldAoyLiBJbXBvcnQgY29sdW1uIHByZXNldCB0byB0aGlzIGFjY291bnQKMy4gSW1wb3J0IGNvbHVtbiBwcmVzZXQgdG8gQUxMIGFjY291bnRzYCwKICApOwoKICB2YXIgZmggPSBuZXcgRmlsZUhlbHBlcigpOwogIHRyeSB7CiAgICBzd2l0Y2ggKGNob2ljZSkgewogICAgICBjYXNlICIxIjoKICAgICAgICBhd2FpdCBleHBvcnRDb2x1bW5QcmVzZXQoKTsKICAgICAgICBhbGVydCgiRXhwb3J0IGNvbXBsZXRlISIpOwogICAgICAgIGJyZWFrOwogICAgICBjYXNlICIyIjoKICAgICAgICB2YXIgZmlsZVNlbGVjdG9yID0gbmV3IEZpbGVTZWxlY3RvcihmaC5yZWFkRmlsZUFzSnNvbkFzeW5jLmJpbmQoZmgpKTsKICAgICAgICBkZWJ1ZyhgT3BlbmluZyBhbmQgcmVhZGluZyBwcmVzZXQgZmlsZS4uLmApOwogICAgICAgIHZhciBwcmVzZXRDb250ZW50ID0gYXdhaXQgZmlsZVNlbGVjdG9yLnNob3coKTsKICAgICAgICBkZWJ1ZyhgR290IHByZXNldCBmaWxlIGNvbnRlbnQhYCk7CgogICAgICAgIGNvbnN0IHVzZXJTZXR0aW5nc0lkID0gYXdhaXQgZmV0Y2hVc2VyU2V0dGluZ3NJZCgpOwogICAgICAgIGxldCBwcmVzZXRJZCA9IGF3YWl0IHVwbG9hZFByZXNldCh1c2VyU2V0dGluZ3NJZCwgcHJlc2V0Q29udGVudCk7CiAgICAgICAgYXdhaXQgc2V0RGVmYXVsdENvbHVtblByZXNldChudWxsLCBwcmVzZXRJZCk7CiAgICAgICAgYWxlcnQoIkltcG9ydCBjb21wbGV0ZSEiKTsKICAgICAgICBicmVhazsKICAgICAgY2FzZSAiMyI6CiAgICAgICAgdmFyIGZpbGVTZWxlY3RvciA9IG5ldyBGaWxlU2VsZWN0b3IoZmgucmVhZEZpbGVBc0pzb25Bc3luYy5iaW5kKGZoKSk7CiAgICAgICAgZGVidWcoYE9wZW5pbmcgYW5kIHJlYWRpbmcgcHJlc2V0IGZpbGUuLi5gKTsKICAgICAgICB2YXIgcHJlc2V0Q29udGVudCA9IGF3YWl0IGZpbGVTZWxlY3Rvci5zaG93KCk7CiAgICAgICAgZGVidWcoYEdvdCBwcmVzZXQgZmlsZSBjb250ZW50IWApOwoKICAgICAgICBsZXQgYWRBY2NvdW50cyA9IGF3YWl0IGdldEFsbEFkQWNjb3VudHMoKTsKICAgICAgICBkZWJ1ZyhgVG90YWwgYWNjb3VudCBjb3VudCBpczogJHthZEFjY291bnRzLmxlbmd0aH0uYCk7CiAgICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCBhZEFjY291bnRzLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgICBkZWJ1ZyhgUHJvY2Vzc2luZyBhY2NvdW50ICMke2l9IC0gJHthZEFjY291bnRzW2ldfS4uLmApOwogICAgICAgICAgY29uc3QgdXNlclNldHRpbmdzSWQgPSBhd2FpdCBmZXRjaFVzZXJTZXR0aW5nc0lkKGFkQWNjb3VudHNbaV0pOwogICAgICAgICAgbGV0IHByZXNldElkID0gYXdhaXQgdXBsb2FkUHJlc2V0KHVzZXJTZXR0aW5nc0lkLCBwcmVzZXRDb250ZW50KTsKICAgICAgICAgIGF3YWl0IHNldERlZmF1bHRDb2x1bW5QcmVzZXQoYWRBY2NvdW50c1tpXSwgcHJlc2V0SWQpOwogICAgICAgIH0KICAgICAgICBhbGVydCgiSW1wb3J0IGNvbXBsZXRlISIpOwogICAgICAgIGJyZWFrOwogICAgICBkZWZhdWx0OgogICAgICAgIGFsZXJ0KCJJbnZhbGlkIG9wdGlvbiEiKTsKICAgICAgICBicmVhazsKICAgIH0KICB9IGNhdGNoIChlcnJvcikgewogICAgY29uc29sZS5lcnJvcigiRXJyb3I6IiwgZXJyb3IpOwogIH0KfQoKYXN5bmMgZnVuY3Rpb24gZXhwb3J0Q29sdW1uUHJlc2V0KCkgewogIGxldCBhZEFjY291bnRJZCA9IHJlcXVpcmUoIkJ1c2luZXNzVW5pZmllZE5hdmlnYXRpb25Db250ZXh0IikuYWRBY2NvdW50SUQ7CiAgbGV0IGpzID0gYXdhaXQgQVBJLmdldFJlcXVlc3QoCiAgICBgYWN0XyR7YWRBY2NvdW50SWR9YCwKICAgIGBmaWVsZHM9WyJ1c2VyX3NldHRpbmdze2lkLGNvbHVtbl9wcmVzZXRze2F0dHJpYnV0aW9uX3dpbmRvd3MsY29sdW1ucyxpZCxuYW1lLHRpbWVfY3JlYXRlZCx0aW1lX3VwZGF0ZWR9fSJdYCwKICApOwogIGNvbnN0IHByZXNldHMgPSBqcy51c2VyX3NldHRpbmdzLmNvbHVtbl9wcmVzZXRzLmRhdGE7CiAgaWYgKHByZXNldHMubGVuZ3RoID09PSAwKSB7CiAgICBhbGVydCgiTm8gcHJlc2V0cyBhdmFpbGFibGUiKTsKICAgIHJldHVybjsKICB9CgogIGxldCBwcmVzZXRMaXN0ID0gIlNlbGVjdCBhIHByZXNldCBieSBudW1iZXI6XG4iOwogIHByZXNldHMuZm9yRWFjaCgocHJlc2V0LCBpbmRleCkgPT4gewogICAgcHJlc2V0TGlzdCArPSBgJHtpbmRleCArIDF9LiAke3ByZXNldC5uYW1lfVxuYDsKICB9KTsKCiAgY29uc3Qgc2VsZWN0ZWROdW1iZXIgPSBwYXJzZUludChwcm9tcHQocHJlc2V0TGlzdCksIDEwKTsKICBpZiAoc2VsZWN0ZWROdW1iZXIgPCAxIHx8IHNlbGVjdGVkTnVtYmVyID4gcHJlc2V0cy5sZW5ndGgpIHsKICAgIGFsZXJ0KCJJbnZhbGlkIHNlbGVjdGlvbiIpOwogICAgcmV0dXJuOwogIH0KCiAgY29uc3Qgc2VsZWN0ZWRQcmVzZXQgPSBwcmVzZXRzW3NlbGVjdGVkTnVtYmVyIC0gMV07CiAgY29uc3QgYmxvYiA9IG5ldyBCbG9iKFtKU09OLnN0cmluZ2lmeShzZWxlY3RlZFByZXNldCldLCB7CiAgICB0eXBlOiAiYXBwbGljYXRpb24vanNvbiIsCiAgfSk7CiAgY29uc3QgYSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKTsKICBhLmhyZWYgPSBVUkwuY3JlYXRlT2JqZWN0VVJMKGJsb2IpOwogIGEuZG93bmxvYWQgPSBgJHtzZWxlY3RlZFByZXNldC5uYW1lfS5qc29uYDsKICBhLmNsaWNrKCk7Cn0KCmFzeW5jIGZ1bmN0aW9uIGZldGNoVXNlclNldHRpbmdzSWQoYWRBY2NvdW50SWQpIHsKICBsZXQgYWNjSWQgPQogICAgYWRBY2NvdW50SWQgPz8gcmVxdWlyZSgiQnVzaW5lc3NVbmlmaWVkTmF2aWdhdGlvbkNvbnRleHQiKS5hZEFjY291bnRJRDsKICBkZWJ1ZyhgR2V0dGluZyB1c2VyIHNldHRpbmdzIGZvciBhY2MgJHthY2NJZH0uLi5gKTsKICBsZXQganMgPSBhd2FpdCBBUEkuZ2V0UmVxdWVzdChgYWN0XyR7YWNjSWR9YCwgYGZpZWxkcz1bXWApOwogIGxldCB1c0lkID0ganM/LnVzZXJfc2V0dGluZ3M/LmlkOwogIGlmICh1c0lkID09IG51bGwpIHsKICAgIGRlYnVnKGBObyBkZWZhdWx0IHVzZXIgc2V0dGluZ3MgZm91bmQhIENyZWF0aW5nIHRoZW0uLi5gKTsKICAgIGpzID0gYXdhaXQgQVBJLmdldFJlcXVlc3QoYGFjdF8ke2FjY0lkfS91c2VyX3NldHRpbmdzYCwgYG1ldGhvZD1wb3N0YCk7CiAgICB1c0lkID0ganMuaWQ7CiAgfQogIHJldHVybiB1c0lkOwp9Cgphc3luYyBmdW5jdGlvbiB1cGxvYWRQcmVzZXQodXNlclNldHRpbmdzSWQsIHByZXNldERhdGEpIHsKICBsZXQgZGF0YSA9IHsKICAgIG5hbWU6IHByZXNldERhdGEubmFtZSwKICAgIGF0dHJpYnV0aW9uX3dpbmRvd3M6IEpTT04uc3RyaW5naWZ5KHByZXNldERhdGEuYXR0cmlidXRpb25fd2luZG93cyksCiAgICBjb2x1bW5zOiBKU09OLnN0cmluZ2lmeShwcmVzZXREYXRhLmNvbHVtbnMpLAogIH07CiAgZGVidWcoCiAgICBgVXBsb2FkaW5nIHByZXNldCAke3ByZXNldERhdGEubmFtZX0gdG8gdXNlciBzZXR0aW5ncyAke3VzZXJTZXR0aW5nc0lkfS4uLmAsCiAgKTsKICBsZXQganMgPSBhd2FpdCBBUEkucG9zdFJlcXVlc3QoYCR7dXNlclNldHRpbmdzSWR9L2NvbHVtbl9wcmVzZXRzYCwgZGF0YSk7CiAgcmV0dXJuIGpzLmlkOwp9Cgphc3luYyBmdW5jdGlvbiBzZXREZWZhdWx0Q29sdW1uUHJlc2V0KGFkQWNjb3VudElkLCBwcmVzZXRJZCkgewogIGxldCBhY2NJZCA9CiAgICBhZEFjY291bnRJZCA/PyByZXF1aXJlKCJCdXNpbmVzc1VuaWZpZWROYXZpZ2F0aW9uQ29udGV4dCIpLmFkQWNjb3VudElEOwogIGRlYnVnKAogICAgYFNldHRpbmcgZGVmYXVsdCBjb2x1bW4gcHJlc2V0IGZvciBhY2MgJHthY2NJZH0sIHByZXNldCBpZCAke3ByZXNldElkfS4uLmAsCiAgKTsKICBsZXQgZGF0YSA9IHsKICAgIGRlZmF1bHRfY29sdW1uX3ByZXNldDogYHsgImlkIjogIiR7cHJlc2V0SWR9IiB9YCwKICAgIGRlZmF1bHRfY29sdW1uX3ByZXNldF9pZDogcHJlc2V0SWQsCiAgfTsKICBsZXQganMgPSBhd2FpdCBBUEkucG9zdFJlcXVlc3QoYGFjdF8ke2FjY0lkfS91c2VyX3NldHRpbmdzYCwgZGF0YSk7CiAgcmV0dXJuIGpzOwp9Cgphc3luYyBmdW5jdGlvbiBnZXRBbGxBZEFjY291bnRzKCkgewogIGxldCBhZEFjY291bnRzID0gW107CiAgZGVidWcoIkdldHRpbmcgcGVyc29uYWwgYWQgYWNjb3VudHMuLi4iKTsKICBsZXQganMgPSBhd2FpdCBBUEkuZ2V0QWxsUGFnZXMoIm1lL3BlcnNvbmFsX2FkX2FjY291bnRzIiwgImZpZWxkcz1pZCIpOwogIGFkQWNjb3VudHMgPSBqcy5tYXAoKGl0ZW0pID0+IGl0ZW0uaWQucmVwbGFjZSgiYWN0XyIsICIiKSk7CiAgZGVidWcoYEdvdCAke2FkQWNjb3VudHMubGVuZ3RofSBwZXJzb25hbCBhZCBhY2NvdW50cyFgKTsKICBkZWJ1ZygiR2V0dGluZyBidXNpbmVzcyBtYW5hZ2Vycy4uLiIpOwogIGpzID0gYXdhaXQgQVBJLmdldEFsbFBhZ2VzKCJtZS9idXNpbmVzc2VzIiwgImZpZWxkcz1pZCIpOwogIGxldCBibUlkcyA9IGpzLm1hcCgoaXRlbSkgPT4gaXRlbS5pZCk7CiAgZGVidWcoYEdvdCAke2JtSWRzLmxlbmd0aH0gYnVzaW5lc3MgbWFuYWdlcnMhYCk7CiAgZm9yIChsZXQgaSA9IDA7IGkgPCBibUlkcy5sZW5ndGg7IGkrKykgewogICAgbGV0IGJtSWQgPSBibUlkc1tpXTsKICAgIGRlYnVnKGBQcm9jZXNzaW5nIEJNICR7Ym1JZH0uLi5gKTsKICAgIGpzID0gYXdhaXQgQVBJLmdldEFsbFBhZ2VzKGAke2JtSWR9L293bmVkX2FkX2FjY291bnRzYCwgImZpZWxkcz1pZCIpOwogICAgbGV0IG93bmVkID0ganMubWFwKChpdGVtKSA9PiBpdGVtLmlkLnJlcGxhY2UoImFjdF8iLCAiIikpOwogICAgZGVidWcoYEdvdCAke293bmVkLmxlbmd0aH0gb3duZWQgYWNjb3VudHMuYCk7CiAgICBhZEFjY291bnRzID0gYWRBY2NvdW50cy5jb25jYXQob3duZWQpOwogICAganMgPSBhd2FpdCBBUEkuZ2V0QWxsUGFnZXMoYCR7Ym1JZH0vY2xpZW50X2FkX2FjY291bnRzYCwgImZpZWxkcz1pZCIpOwogICAgbGV0IGNsaWVudCA9IGpzLm1hcCgoaXRlbSkgPT4gaXRlbS5pZC5yZXBsYWNlKCJhY3RfIiwgIiIpKTsKICAgIGRlYnVnKGBHb3QgJHtjbGllbnQubGVuZ3RofSBjbGllbnQgYWNjb3VudHMuYCk7CiAgICBhZEFjY291bnRzID0gYWRBY2NvdW50cy5jb25jYXQoY2xpZW50KTsKICB9CiAgcmV0dXJuIGFkQWNjb3VudHM7Cn0KCmZ1bmN0aW9uIGRlYnVnKG1zZykgewogIGlmIChERUJVRykgY29uc29sZS5sb2cobXNnKTsKfQoKbWFpbigpOw==") + "})()");