Skip to content

Commit

Permalink
[Github Gists] OAuth 2.0 support (raycast#13403)
Browse files Browse the repository at this point in the history
* feat(github-gists): Adds OAuth2.0 support and deprecates tokens

closes raycast#13402

* refactor(github-gists): fixed lint issues

closes raycast#13402

* refactor(github-gists): changed the readme screencast

closes raycast#13402

* Update CHANGELOG.md and optimise images

---------

Co-authored-by: raycastbot <[email protected]>
  • Loading branch information
jfkisafk and raycastbot committed Jul 11, 2024
1 parent 91141e9 commit 997d7e7
Show file tree
Hide file tree
Showing 15 changed files with 514 additions and 306 deletions.
6 changes: 6 additions & 0 deletions extensions/github-gist/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# GitHub Gist Changelog

## [OAuth2.0 Support] - 2024-07-11

- Uses Raycast OAuth2.0 GitHub integration with `repo read:user gist` scopes.
- ⚠️Disables the personal access token preference. Users can delete their tokens after successful OAuth2.0 connection.
- Minor refactoring and updated dependencies.

## [Refactor Command] - 2024-07-03

- Simplify the code and improve performance
Expand Down
28 changes: 8 additions & 20 deletions extensions/github-gist/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,17 @@

### Getting Started

This extension brings GitHub Gist support to Raycast through the personal access tokens. To get started, first:
This extension brings GitHub Gist support to Raycast via OAuth 2.0.

- Login to your GitHub instance (Eg. [https://github.com](https://github.com))
- Click on your avatar image in the right upper corner
- From the dropdown menu, click on `Settings`
- On the left-hand side, click on `Developer settings`.
- On the left-hand side, click on `Personal access tokens`.
- On this page, we'll create a new access token. Click on `Generate new token` in the upper-right.
- Leave a `note` for your token (Eg. `Raycast`). This will help you identify it in the future.
- You'll need to check the following boxes to ensure this extension can perform properly:
- `repo`
- `gist`
- `user`
- Click `Generate token` and save this value somewhere. **You'll only be able to see this once**.
To get started, open any command and it will prompt you to connect to you GitHub instance via OAuth.
You will be led to your browser where you might need to sign in to your [github](https://github.com).
Once you accept the new/additional scopes for Raycast GitHub integration, the command will open back up
with results. **You'll only be able to see this once**.

> If you used GitHub Personal Access Token for this extension, you can delete them after connecting through OAuth.
> Future updates to this extension will offer more functionality that may require additional scopes be defined in this token.
### Screencast

- Create Gist

https://user-images.githubusercontent.com/36128970/159211152-65b06683-56a3-4db4-842f-3acb05c6ce62.mp4

- Search Gist

https://user-images.githubusercontent.com/36128970/159161962-70adae29-29c5-4026-82d0-19f23b8dcc22.mp4
<img src="media/usage.gif" alt="Usage">
Binary file added extensions/github-gist/media/usage.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
302 changes: 247 additions & 55 deletions extensions/github-gist/package-lock.json

Large diffs are not rendered by default.

10 changes: 1 addition & 9 deletions extensions/github-gist/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,14 @@
"Aayush9029",
"LunaticMuch",
"pernielsentikaer",
"stelo",
"nbbaier"
],
"categories": [
"Developer Tools",
"Productivity"
],
"license": "MIT",
"preferences": [
{
"name": "access-token",
"type": "password",
"required": true,
"title": "Personal access tokens",
"description": "GitHub personal access tokens."
}
],
"commands": [
{
"name": "search-gists",
Expand Down
148 changes: 148 additions & 0 deletions extensions/github-gist/src/api/github-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { Octokit } from "@octokit/core";
import { formatBytes, isEmpty } from "../util/utils";
import { Clipboard, open, showToast, Toast } from "@raycast/api";
import { Gist, GistFile, GithubGistTag } from "../util/gist-utils";

export class GithubClient {
constructor(public readonly octokit: Octokit) {}

public async requestGist(tag: string, page: number, perPage: number) {
const { octokit } = this;
const response = await (async () => {
switch (tag) {
case GithubGistTag.MY_GISTS: {
return await octokit.request(`GET /gists`, { page: page, per_page: perPage });
}
case GithubGistTag.ALL_GISTS: {
return await octokit.request(`GET /gists/public`, { page: page, per_page: perPage });
}
case GithubGistTag.STARRED: {
return await octokit.request(`GET /gists/starred`, { page: page, per_page: perPage });
}
default: {
return await octokit.request(`GET /gists`, { page: page, per_page: perPage });
}
}
})();
const _gists: Gist[] = [];
response.data.forEach((_data) => {
const _gist: Gist = {
gist_id: _data.id,
description: isEmpty(String(_data.description)) ? "[ No description ]" : String(_data.description),
html_url: _data.html_url,
file: [],
};
for (const value of Object.values(_data.files)) {
if (value !== undefined) {
_gist.file.push({
filename: String(value.filename),
language: String(value.language),
raw_url: String(value.raw_url),
size: formatBytes(value.size),
type: value.type,
});
}
}
_gists.push(_gist);
});
return _gists;
}

public async starGist(gist_id: string) {
return await this.octokit.request(`PUT /gists/${gist_id}/star`, {
gist_id: gist_id,
});
}

public async unStarGist(gist_id: string) {
return await this.octokit.request(`DELETE /gists/${gist_id}/star`, {
gist_id: gist_id,
});
}

public async deleteGist(gist_id: string) {
return await this.octokit.request(`DELETE /gists/${gist_id}`, {
gist_id: gist_id,
});
}

public async createGist(description: string, isPublic = false, gistFiles: GistFile[]) {
const files: { [p: string]: { content: string } } = {};
gistFiles.forEach((value) => {
files[value.filename] = { content: value.content };
});
return await this.octokit.request("POST /gists", {
description: description,
public: isPublic,
files: files,
});
}

public async updateGist(gistId: string, description: string, oldFileNames: string[], newFiles: GistFile[]) {
const files: { [p: string]: { content: string } } = {};
const newFileName = newFiles.map((value) => value.filename);
const deleteFiles = oldFileNames.filter((value) => !newFileName.includes(value));
newFiles.forEach((value) => {
files[value.filename] = { content: value.content };
});
deleteFiles.forEach((value) => {
files[value] = { content: "" };
});
return await this.octokit.request("PATCH /gists/" + gistId, {
description: description,
files: files,
});
}

public async updateOrCreateGists(
isEdit: boolean,
gist: Gist,
description: string,
isPublic: string,
gistFiles: GistFile[],
oldGistFiles: string[],
gistMutate: () => void,
) {
const toast = await showToast(Toast.Style.Animated, isEdit ? "Updating" : "Creating");
try {
let response;
if (isEdit) {
response = await this.updateGist(gist.gist_id, description, oldGistFiles, gistFiles);
} else {
response = await this.createGist(description, isPublic === "true", gistFiles);
}
if (response.status === 201 || response.status === 200) {
const options: Toast.Options = {
title: "Gist " + (isEdit ? "Updated" : "Created"),
primaryAction: {
title: "Copy Gist Link",
shortcut: { modifiers: ["shift", "cmd"], key: "l" },
onAction: (toast) => {
Clipboard.copy(String(response.data.html_url));
toast.title = "Link Copied to Clipboard";
},
},
secondaryAction: {
title: "Open in Browser",
shortcut: { modifiers: ["shift", "cmd"], key: "o" },
onAction: (toast) => {
open(String(response.data.html_url));
toast.hide();
},
},
};
toast.style = Toast.Style.Success;
toast.title = options.title;
toast.primaryAction = options.primaryAction;
toast.secondaryAction = options.secondaryAction;
gistMutate();
} else {
toast.style = Toast.Style.Failure;
toast.title = "Failed to " + (isEdit ? "Update" : "Create");
}
} catch (error) {
toast.style = Toast.Style.Failure;
toast.title = "Failed to " + (isEdit ? "Update" : "Create");
}
}
}
23 changes: 23 additions & 0 deletions extensions/github-gist/src/api/oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { OAuthService } from "@raycast/utils";
import fetch from "node-fetch";
import { Octokit } from "@octokit/core";
import { GithubClient } from "./github-client";

let client: GithubClient | undefined = undefined;

export const githubOAuthService = OAuthService.github({
scope: "repo gist read:user",
onAuthorize: ({ token }) => {
const octokit = new Octokit({ auth: token, request: { fetch } });

client = new GithubClient(octokit);
},
});

export function getGitHubClient() {
if (!client) {
throw new Error("GitHub client not initialized");
}

return client;
}
30 changes: 16 additions & 14 deletions extensions/github-gist/src/components/gist-action.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import { Action, ActionPanel, Application, Clipboard, Icon, open, showHUD, showToast, Toast } from "@raycast/api";
import { alertDialog, raySo } from "../util/utils";
import { deleteGist, Gist, GithubGistTag, starGist, unStarGist } from "../util/gist-utils";
import CreateGist from "../create-gist";
import { CreateGistForm } from "../create-gist";
import { primaryAction } from "../types/preferences";
import { MutatePromise } from "@raycast/utils";
import { Gist, GithubGistTag } from "../util/gist-utils";
import { getGitHubClient } from "../api/oauth";

export function GistAction(props: {
gist: Gist;
gistFileName: string;
gistFileContent: string;
tag: GithubGistTag;
gistMutate: MutatePromise<Gist[]>;
fronstmostApp: Application;
frontmostApp: Application;
}) {
const { gist, gistFileName, gistFileContent, tag, gistMutate, fronstmostApp } = props;
const client = getGitHubClient();
const { gist, gistFileName, gistFileContent, tag, gistMutate, frontmostApp } = props;

return (
<>
<Action
title={primaryAction === "copy" ? "Copy to Clipboard" : "Paste to " + fronstmostApp?.name}
icon={primaryAction === "copy" ? Icon.Clipboard : { fileIcon: fronstmostApp?.path }}
title={primaryAction === "copy" ? "Copy to Clipboard" : "Paste to " + frontmostApp?.name}
icon={primaryAction === "copy" ? Icon.Clipboard : { fileIcon: frontmostApp?.path }}
onAction={async () => {
if (primaryAction === "copy") {
await Clipboard.copy(gistFileContent);
Expand All @@ -31,8 +33,8 @@ export function GistAction(props: {
}}
/>
<Action
title={primaryAction === "copy" ? "Paste to " + fronstmostApp?.name : "Copy to Clipboard"}
icon={primaryAction === "copy" ? { fileIcon: fronstmostApp?.path } : Icon.Clipboard}
title={primaryAction === "copy" ? "Paste to " + frontmostApp?.name : "Copy to Clipboard"}
icon={primaryAction === "copy" ? { fileIcon: frontmostApp?.path } : Icon.Clipboard}
onAction={async () => {
if (primaryAction === "copy") {
await Clipboard.paste(gistFileContent);
Expand All @@ -55,7 +57,7 @@ export function GistAction(props: {
icon={Icon.Star}
shortcut={{ modifiers: ["cmd"], key: "s" }}
onAction={async () => {
const response = await starGist(gist.gist_id);
const response = await client.starGist(gist.gist_id);
if (response.status == 204) {
await showToast(Toast.Style.Success, "Gist Stared");
} else {
Expand All @@ -67,7 +69,7 @@ export function GistAction(props: {
title={"Edit Gist"}
icon={Icon.Pencil}
shortcut={{ modifiers: ["cmd"], key: "e" }}
target={<CreateGist gist={gist} gistMutate={gistMutate} />}
target={<CreateGistForm gist={gist} gistMutate={gistMutate} />}
/>
</>
);
Expand All @@ -79,7 +81,7 @@ export function GistAction(props: {
icon={Icon.Star}
shortcut={{ modifiers: ["cmd"], key: "s" }}
onAction={async () => {
const response = await starGist(gist.gist_id);
const response = await client.starGist(gist.gist_id);
if (response.status == 204) {
await showToast(Toast.Style.Success, "Gist Stared");
} else {
Expand All @@ -96,7 +98,7 @@ export function GistAction(props: {
icon={Icon.StarDisabled}
shortcut={{ modifiers: ["cmd"], key: "u" }}
onAction={async () => {
const response = await unStarGist(gist.gist_id);
const response = await client.unStarGist(gist.gist_id);
if (response.status == 204) {
await showToast(Toast.Style.Success, "Gist Unstared");
await gistMutate();
Expand All @@ -115,7 +117,7 @@ export function GistAction(props: {
title={"Create Gist"}
icon={Icon.PlusTopRightSquare}
shortcut={{ modifiers: ["cmd"], key: "n" }}
target={<CreateGist gist={undefined} gistMutate={gistMutate} />}
target={<CreateGistForm gist={undefined} gistMutate={gistMutate} />}
/>
<Action
title={"Clone Gist"}
Expand All @@ -140,7 +142,7 @@ export function GistAction(props: {
"Are you sure you want to delete this gist?",
"Confirm",
async () => {
const response = await deleteGist(gist.gist_id);
const response = await client.deleteGist(gist.gist_id);
if (response.status == 204) {
await showToast(Toast.Style.Success, "Gist Deleted");
await gistMutate();
Expand Down
7 changes: 7 additions & 0 deletions extensions/github-gist/src/components/with-github-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { withAccessToken } from "@raycast/utils";
import React from "react";
import { githubOAuthService } from "../api/oauth";

export function withGitHubClient(Component: React.ComponentType) {
return withAccessToken(githubOAuthService)(Component);
}
Loading

0 comments on commit 997d7e7

Please sign in to comment.