Skip to content

Commit

Permalink
feat: add chrome extension to support batch delete github repos
Browse files Browse the repository at this point in the history
  • Loading branch information
AaronConlon committed Jul 30, 2024
1 parent 442462a commit 2d2628a
Show file tree
Hide file tree
Showing 12 changed files with 437 additions and 0 deletions.
34 changes: 34 additions & 0 deletions apps/v-git-batch/.github/workflows/submit.yml
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
33 changes: 33 additions & 0 deletions apps/v-git-batch/.gitignore
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
25 changes: 25 additions & 0 deletions apps/v-git-batch/.prettierrc.mjs
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/(.*)$",
"",
"^~(.*)$",
"",
"^[./]"
]
}
33 changes: 33 additions & 0 deletions apps/v-git-batch/README.md
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!
Binary file added apps/v-git-batch/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
118 changes: 118 additions & 0 deletions apps/v-git-batch/background.ts
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
}
}
}
})
3 changes: 3 additions & 0 deletions apps/v-git-batch/const.ts
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"
129 changes: 129 additions & 0 deletions apps/v-git-batch/contents/index.ts
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
})
}
}
})()
9 changes: 9 additions & 0 deletions apps/v-git-batch/keys.json
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"
}
}
Loading

0 comments on commit 2d2628a

Please sign in to comment.