-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add chrome extension to support batch delete github repos
- Loading branch information
1 parent
442462a
commit 2d2628a
Showing
12 changed files
with
437 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
name: "Submit to Web Store" | ||
on: | ||
workflow_dispatch: | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Cache pnpm modules | ||
uses: actions/cache@v3 | ||
with: | ||
path: ~/.pnpm-store | ||
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} | ||
restore-keys: | | ||
${{ runner.os }}- | ||
- uses: pnpm/[email protected] | ||
with: | ||
version: latest | ||
run_install: true | ||
- name: Use Node.js 16.x | ||
uses: actions/[email protected] | ||
with: | ||
node-version: 16.x | ||
cache: "pnpm" | ||
- name: Build the extension | ||
run: pnpm build | ||
- name: Package the extension into a zip artifact | ||
run: pnpm package | ||
- name: Browser Platform Publish | ||
uses: PlasmoHQ/bpp@v3 | ||
with: | ||
keys: ${{ secrets.SUBMIT_KEYS }} | ||
artifact: build/chrome-mv3-prod.zip |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
|
||
# testing | ||
/coverage | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
.pnpm-debug.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
out/ | ||
build/ | ||
dist/ | ||
|
||
# plasmo | ||
.plasmo | ||
|
||
# typescript | ||
.tsbuildinfo | ||
.env |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/** | ||
* @type {import('prettier').Options} | ||
*/ | ||
export default { | ||
printWidth: 80, | ||
tabWidth: 2, | ||
useTabs: false, | ||
semi: false, | ||
singleQuote: false, | ||
trailingComma: "none", | ||
bracketSpacing: true, | ||
bracketSameLine: true, | ||
importOrder: [ | ||
"<BUILTIN_MODULES>", // Node.js built-in modules | ||
"<THIRD_PARTY_MODULES>", // Imports not matched by other special words or groups. | ||
"", // Empty line | ||
"^@plasmo/(.*)$", | ||
"", | ||
"^@plasmohq/(.*)$", | ||
"", | ||
"^~(.*)$", | ||
"", | ||
"^[./]" | ||
] | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
This is a [Plasmo extension](https://docs.plasmo.com/) project bootstrapped with [`plasmo init`](https://www.npmjs.com/package/plasmo). | ||
|
||
## Getting Started | ||
|
||
First, run the development server: | ||
|
||
```bash | ||
pnpm dev | ||
# or | ||
npm run dev | ||
``` | ||
|
||
Open your browser and load the appropriate development build. For example, if you are developing for the chrome browser, using manifest v3, use: `build/chrome-mv3-dev`. | ||
|
||
You can start editing the popup by modifying `popup.tsx`. It should auto-update as you make changes. To add an options page, simply add a `options.tsx` file to the root of the project, with a react component default exported. Likewise to add a content page, add a `content.ts` file to the root of the project, importing some module and do some logic, then reload the extension on your browser. | ||
|
||
For further guidance, [visit our Documentation](https://docs.plasmo.com/) | ||
|
||
## Making production build | ||
|
||
Run the following: | ||
|
||
```bash | ||
pnpm build | ||
# or | ||
npm run build | ||
``` | ||
|
||
This should create a production bundle for your extension, ready to be zipped and published to the stores. | ||
|
||
## Submit to the webstores | ||
|
||
The easiest way to deploy your Plasmo extension is to use the built-in [bpp](https://bpp.browser.market) GitHub action. Prior to using this action however, make sure to build your extension and upload the first version to the store to establish the basic credentials. Then, simply follow [this setup instruction](https://docs.plasmo.com/framework/workflows/submit) and you should be on your way for automated submission! |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { APP_NAME } from "~const" | ||
|
||
export {} | ||
|
||
function authenticate(tabId: number) { | ||
const redirectUri = chrome.identity.getRedirectURL() | ||
const authUrl = `https://github.com/login/oauth/authorize?client_id=${process.env.PLASMO_PUBLIC_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=delete_repo` | ||
|
||
chrome.identity.launchWebAuthFlow( | ||
{ url: authUrl, interactive: true }, | ||
function (redirectUrl) { | ||
if (chrome.runtime.lastError || !redirectUrl) { | ||
console.error(chrome.runtime.lastError) | ||
return | ||
} | ||
// 解析重定向 URL 获取授权码 | ||
const urlParams = new URLSearchParams(new URL(redirectUrl).search) | ||
const code = urlParams.get("code") | ||
|
||
// 使用授权码交换访问令牌 | ||
fetch("https://github.com/login/oauth/access_token", { | ||
method: "POST", | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/json" | ||
}, | ||
body: JSON.stringify({ | ||
client_id: process.env.PLASMO_PUBLIC_CLIENT_ID, | ||
client_secret: process.env.PLASMO_PUBLIC_SECRET_ID, | ||
code: code | ||
}) | ||
}) | ||
.then((response) => response.json()) | ||
.then((data) => { | ||
if (data?.access_token) { | ||
const access_token = data.access_token | ||
chrome.storage.sync.set({ access_token }) | ||
// refresh the tab | ||
chrome.tabs.reload(tabId) | ||
} | ||
}) | ||
} | ||
) | ||
} | ||
|
||
function deleteRepos(account: string, repos: string[], access_token: string) { | ||
return repos.map((repo) => { | ||
// 调用 api 删除仓库 | ||
return fetch(`https://api.github.com/repos/${account}/${repo}`, { | ||
method: "DELETE", | ||
headers: { | ||
Authorization: `Bearer ${access_token}` | ||
} | ||
}).then((response) => { | ||
if (response.status === 204) { | ||
return repo | ||
} else { | ||
console.error(`Failed to delete repository ${repo}`) | ||
throw Error(`Failed to delete repository ${repo}`) | ||
} | ||
}) | ||
}) | ||
} | ||
|
||
chrome.runtime.onMessage.addListener(async function ( | ||
request: { | ||
name: string | ||
action: "check_logged_in" | "delete" | "authenticate" | ||
payload?: { | ||
account: string | ||
repos: string[] | ||
} | ||
}, | ||
sender, | ||
sendResponse | ||
) { | ||
const { name, payload, action } = request | ||
if (name === APP_NAME) { | ||
const { access_token } = await chrome.storage.sync.get("access_token") | ||
try { | ||
if (action === "check_logged_in") { | ||
const { origin } = sender | ||
const cookies = await chrome.cookies.getAll({ | ||
url: origin, | ||
domain: "github.com" | ||
}) | ||
|
||
const logged_in_item = cookies.find((i) => i.name === "logged_in") | ||
// get access token | ||
sendResponse({ | ||
logged_in: logged_in_item?.value === "yes", | ||
access_token | ||
}) | ||
} else if (action === "delete" && payload?.repos.length) { | ||
const batchTask = await Promise.allSettled( | ||
deleteRepos(payload.account, payload.repos, access_token) | ||
) | ||
|
||
const deletedRepos = batchTask | ||
.filter((i) => i.status === "fulfilled") | ||
.map((i) => i.value) | ||
sendResponse({ | ||
name: APP_NAME, | ||
message: "deleted", | ||
deletedRepos | ||
}) | ||
} else if (action === "authenticate") { | ||
authenticate(sender.tab.id) | ||
sendResponse() | ||
} | ||
} catch (error) { | ||
console.log("background init error:", error) | ||
return { | ||
error: error.message | ||
} | ||
} | ||
} | ||
}) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export const APP_NAME = "v-git-batch" | ||
export const ACTION_BTN_ID = "v-git-batch-action-btn" | ||
export const AUTH_SUCCESS = "AUTH_SUCCESS" |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import type { PlasmoCSConfig } from "plasmo" | ||
|
||
import { ACTION_BTN_ID, APP_NAME } from "~const" | ||
|
||
export {} | ||
export const config: PlasmoCSConfig = { | ||
matches: ["https://github.com/*"], | ||
all_frames: false | ||
} | ||
|
||
const createCheckbox = (parentNode: HTMLElement) => { | ||
const input = document.createElement("input") | ||
input.type = "checkbox" | ||
input.style.marginRight = "6px" | ||
parentNode.prepend(input) | ||
input.setAttribute("v-git-batch-checkbox", "true") | ||
} | ||
|
||
;(async function () { | ||
const actionButtonId = ACTION_BTN_ID | ||
const resp: { | ||
logged_in: boolean | ||
access_token?: string | ||
} = await chrome.runtime.sendMessage({ | ||
name: APP_NAME, | ||
action: "check_logged_in" | ||
}) | ||
|
||
const { logged_in, access_token } = resp | ||
|
||
if (logged_in) { | ||
// query items | ||
const { href } = location | ||
let isAuth = access_token !== undefined | ||
|
||
if ( | ||
href.includes("?tab=repositories") && | ||
href.startsWith("https://github.com/") | ||
) { | ||
// create delete button after | ||
const filterContainer = document.querySelector( | ||
'#user-profile-frame form[role="search"]' | ||
)?.parentElement | ||
|
||
if (filterContainer) { | ||
const button = document.createElement("button") | ||
button.id = actionButtonId | ||
button.textContent = isAuth ? "Delete" : "Authorization" | ||
button.className = `text-center btn ml-2 ${isAuth ? "btn-danger" : "btn-primary"}` | ||
button.onclick = function () { | ||
// 如果没有认证,就认证 | ||
if (!isAuth) { | ||
chrome.runtime.sendMessage({ | ||
name: APP_NAME, | ||
action: "authenticate" | ||
}) | ||
return | ||
} | ||
|
||
const list = document.querySelectorAll( | ||
'input[type="checkbox"][v-git-batch-checkbox]:checked' | ||
) | ||
if (list.length) { | ||
const repos = Array.from(list).map((item) => { | ||
const li = item.parentElement | ||
return li.querySelector("a").textContent.trim() | ||
}) | ||
|
||
chrome.runtime | ||
.sendMessage({ | ||
name: APP_NAME, | ||
action: "delete", | ||
payload: { | ||
account: "AaronConlon", | ||
repos | ||
} | ||
}) | ||
.then((res) => { | ||
const { deletedRepos } = res | ||
const container = document.querySelector( | ||
"#user-repositories-list" | ||
) | ||
deletedRepos.forEach((repo) => { | ||
container | ||
.querySelector(`li[data-v-git-batch-item-name='${repo}']`) | ||
?.remove() | ||
}) | ||
}) | ||
} else { | ||
alert("Please select at least one repository") | ||
} | ||
} | ||
|
||
filterContainer.appendChild(button) | ||
} | ||
|
||
const container = document.querySelector("#user-repositories-list") | ||
|
||
const list = container.querySelectorAll("li") | ||
if (list.length) { | ||
// add checkbox | ||
list.forEach((item) => { | ||
const repoName = item.querySelector("h3 a")?.textContent.trim() | ||
item.setAttribute("data-v-git-batch-item-name", repoName) | ||
const h3 = item.querySelector("h3") | ||
if (h3.querySelector("input")) return | ||
createCheckbox(h3) | ||
}) | ||
} | ||
|
||
// 监听 DOM 变化 | ||
const observer = new MutationObserver(function (mutations) { | ||
mutations.forEach(function () { | ||
container.querySelectorAll("li").forEach((item) => { | ||
const repoName = item.querySelector("h3 a")?.textContent.trim() | ||
item.setAttribute("data-v-git-batch-item-name", repoName) | ||
const h3 = item.querySelector("h3") | ||
if (h3.querySelector("input")) return | ||
createCheckbox(h3) | ||
}) | ||
}) | ||
}) | ||
|
||
observer.observe(container, { | ||
childList: true | ||
}) | ||
} | ||
} | ||
})() |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"$schema": "https://raw.githubusercontent.com/PlasmoHQ/bpp/v3/keys.schema.json", | ||
"chrome": { | ||
"clientId": "123", | ||
"refreshToken": "789", | ||
"extId": "abcd", | ||
"clientSecret": "efgh" | ||
} | ||
} |
Oops, something went wrong.