mirror of
https://github.com/Sevichecc/raycast-akkoma-extension.git
synced 2025-04-30 06:39:30 +08:00
init
This commit is contained in:
parent
a810f1b9ee
commit
2e0377f7f4
12 changed files with 1920 additions and 0 deletions
4
.eslintrc.json
Normal file
4
.eslintrc.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": ["@raycast"]
|
||||||
|
}
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
4
.prettierrc
Normal file
4
.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"singleQuote": false
|
||||||
|
}
|
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Post to Akkoma Changelog
|
||||||
|
|
||||||
|
## [Initial Version] - 2023-04-06
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Post to Akkoma
|
||||||
|
|
||||||
|
Write new post and send it to Akkoma
|
BIN
assets/akkoma-icon.png
Normal file
BIN
assets/akkoma-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.9 KiB |
78
package.json
Normal file
78
package.json
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://www.raycast.com/schemas/extension.json",
|
||||||
|
"name": "akkoma",
|
||||||
|
"title": "Post to Akkoma",
|
||||||
|
"description": "Send your post from Raycast to Akkoma / Pleroma / Mastodon",
|
||||||
|
"icon": "akkoma-icon.png",
|
||||||
|
"author": "SevicheCC",
|
||||||
|
"categories": [
|
||||||
|
"Communication"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "index",
|
||||||
|
"title": "Post to Fediverse",
|
||||||
|
"description": "Send your post from Raycast to Akkoma / Pleroma / Mastodon",
|
||||||
|
"mode": "view"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"preferences": [
|
||||||
|
{
|
||||||
|
"name": "instance",
|
||||||
|
"type": "textfield",
|
||||||
|
"required": true,
|
||||||
|
"title": "Akkoma instance's URL",
|
||||||
|
"description": "Your Akkoma / Pleroma / Mastodon instance's URL",
|
||||||
|
"link": "https://github.com/Sevichecc/raycast-akkoma-extension",
|
||||||
|
"placeholder": "such as: example.dev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "defaultVisbility",
|
||||||
|
"type": "dropdown",
|
||||||
|
"required": false,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"title": "🌎 Public",
|
||||||
|
"value": "public"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "👥 Private",
|
||||||
|
"value": "private"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "🙈 Unlist",
|
||||||
|
"value": "unlist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "✉️ Direct",
|
||||||
|
"value": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Akkoma instance's URL",
|
||||||
|
"description": "Your Akkoma / Pleroma / Mastodon instance's URL",
|
||||||
|
"link": "https://github.com/Sevichecc/raycast-akkoma-extension",
|
||||||
|
"placeholder": "such as: example.dev"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@raycast/api": "^1.49.2",
|
||||||
|
"masto": "^5.10.0",
|
||||||
|
"node-fetch": "^3.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@raycast/eslint-config": "1.0.5",
|
||||||
|
"@types/node": "18.8.3",
|
||||||
|
"@types/react": "18.0.9",
|
||||||
|
"eslint": "^7.32.0",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"typescript": "^4.4.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "ray build -e dist",
|
||||||
|
"dev": "ray develop",
|
||||||
|
"fix-lint": "ray lint --fix",
|
||||||
|
"lint": "ray lint",
|
||||||
|
"publish": "npx @raycast/api@latest publish"
|
||||||
|
}
|
||||||
|
}
|
1565
pnpm-lock.yaml
Normal file
1565
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
78
src/index.tsx
Normal file
78
src/index.tsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
import { Form, ActionPanel, Action, showToast } from "@raycast/api";
|
||||||
|
import { authorize, client } from "./oauth";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import { getPreferenceValues } from "@raycast/api";
|
||||||
|
import { Preference,ApiResponse } from "./types";
|
||||||
|
|
||||||
|
type Values = {
|
||||||
|
textfield: string;
|
||||||
|
textarea: string;
|
||||||
|
datepicker: Date;
|
||||||
|
sensitive: boolean;
|
||||||
|
dropdown: string;
|
||||||
|
files: { name: string; url: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Command() {
|
||||||
|
const { instance } = getPreferenceValues<Preference>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
authorize();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
async (values: Values) => {
|
||||||
|
try {
|
||||||
|
const token = await client.getTokens();
|
||||||
|
|
||||||
|
const response = await fetch(`https://${instance}/api/v1/statuses`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token?.accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
status: values.textarea,
|
||||||
|
visibility: values.dropdown,
|
||||||
|
spoiler_text: values.textfield,
|
||||||
|
sensitive: values.sensitive,
|
||||||
|
scheduled_at: values.datepicker.toISOString(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = (await response.json()) as ApiResponse;
|
||||||
|
console.log(data);
|
||||||
|
showToast({ title: "Submitted form", message: "Status has been posted!" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
showToast({ title: "Error", message: "Something went wrong!" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[instance]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form
|
||||||
|
actions={
|
||||||
|
<ActionPanel>
|
||||||
|
<Action.SubmitForm onSubmit={handleSubmit} />
|
||||||
|
</ActionPanel>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form.TextField id="textfield" title="Content Warning" placeholder="" />
|
||||||
|
<Form.TextArea id="textarea" title="Post detail" placeholder="" enableMarkdown={true} />
|
||||||
|
<Form.Separator />
|
||||||
|
<Form.DatePicker id="datepicker" title="Scheduled Time" />
|
||||||
|
<Form.Dropdown id="dropdown" title="Visibility" storeValue={true} defaultValue="">
|
||||||
|
<Form.Dropdown.Item value="direct" title="Direct" />
|
||||||
|
<Form.Dropdown.Item value="private" title="Private" />
|
||||||
|
<Form.Dropdown.Item value="unlisted" title="Unlisted" />
|
||||||
|
<Form.Dropdown.Item value="public" title="Public" />
|
||||||
|
</Form.Dropdown>
|
||||||
|
<Form.FilePicker id="files" />
|
||||||
|
<Form.Checkbox id="sensitive" title="Sensitive" label="Sensitive" />
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
116
src/oauth.ts
Normal file
116
src/oauth.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import { OAuth, getPreferenceValues } from "@raycast/api";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import { Preference, AppResponse } from "./types";
|
||||||
|
|
||||||
|
const redirectUri = "https://raycast.com/redirect?packageName=Extension";
|
||||||
|
|
||||||
|
export const client = new OAuth.PKCEClient({
|
||||||
|
redirectMethod: OAuth.RedirectMethod.Web,
|
||||||
|
providerName: "Akkoma",
|
||||||
|
providerIcon: "akkoma-icon.png",
|
||||||
|
providerId: "akkoma",
|
||||||
|
description: "Connect to your Akkoma | Pleroma | Mastodon account",
|
||||||
|
});
|
||||||
|
|
||||||
|
const createAkkomaApp = async (): Promise<AppResponse> => {
|
||||||
|
const { instance } = getPreferenceValues<Preference>();
|
||||||
|
|
||||||
|
const response = await fetch(`https://${instance}/api/v1/apps`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_name: "raycast-akkoma-extension",
|
||||||
|
redirect_uris: redirectUri,
|
||||||
|
scopes: "read write push",
|
||||||
|
website: "https://raycast.com",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to create Akkoma app");
|
||||||
|
}
|
||||||
|
|
||||||
|
const appResponse = await response.json();
|
||||||
|
return appResponse as AppResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestAccessToken = async (
|
||||||
|
clientId: string,
|
||||||
|
clientSecret: string,
|
||||||
|
authRequest: OAuth.AuthorizationRequest,
|
||||||
|
authCode: string
|
||||||
|
): Promise<OAuth.TokenResponse> => {
|
||||||
|
const { instance } = getPreferenceValues<Preference>();
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("client_id", clientId);
|
||||||
|
params.append("client_secret", clientSecret);
|
||||||
|
params.append("code", authCode);
|
||||||
|
params.append("code_verifier", authRequest.codeVerifier);
|
||||||
|
params.append("grant_type", "authorization_code");
|
||||||
|
params.append("redirect_uri", authRequest.redirectURI);
|
||||||
|
|
||||||
|
const response = await fetch(`https://${instance}/oauth/token`, {
|
||||||
|
method: "POST",
|
||||||
|
body: params,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error("fetch tokens error:", await response.text());
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
return (await response.json()) as OAuth.TokenResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshToken = async (
|
||||||
|
clientId: string,
|
||||||
|
clientSecret: string,
|
||||||
|
refreshToken: string
|
||||||
|
): Promise<OAuth.TokenResponse> => {
|
||||||
|
const { instance } = getPreferenceValues<Preference>();
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("client_id", clientId);
|
||||||
|
params.append("client_secret", clientSecret);
|
||||||
|
params.append("refresh_token", refreshToken);
|
||||||
|
params.append("grant_type", "refresh_token");
|
||||||
|
|
||||||
|
const response = await fetch(`https://${instance}/oauth/token`, {
|
||||||
|
method: "POST",
|
||||||
|
body: params,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error("refresh tokens error:", await response.text());
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenResponse = (await response.json()) as OAuth.TokenResponse;
|
||||||
|
tokenResponse.refresh_token = tokenResponse.refresh_token ?? refreshToken;
|
||||||
|
return tokenResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 授权过程
|
||||||
|
export const authorize = async (): Promise<void> => {
|
||||||
|
const { instance } = getPreferenceValues<Preference>();
|
||||||
|
const tokenSet = await client.getTokens();
|
||||||
|
|
||||||
|
if (tokenSet?.accessToken) {
|
||||||
|
if (tokenSet.refreshToken && tokenSet.isExpired()) {
|
||||||
|
const { client_id, client_secret } = await createAkkomaApp();
|
||||||
|
await client.setTokens(await refreshToken(client_id, client_secret, tokenSet.refreshToken));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { client_id, client_secret } = await createAkkomaApp();
|
||||||
|
|
||||||
|
const authRequest = await client.authorizationRequest({
|
||||||
|
endpoint: `https://${instance}/oauth/authorize`,
|
||||||
|
clientId: client_id,
|
||||||
|
scope: "read write push",
|
||||||
|
});
|
||||||
|
const { authorizationCode } = await client.authorize(authRequest);
|
||||||
|
|
||||||
|
await client.setTokens(await requestAccessToken(client_id, client_secret, authRequest, authorizationCode));
|
||||||
|
};
|
45
src/types.ts
Normal file
45
src/types.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
export interface Preference {
|
||||||
|
instance: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Credentials {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
redirect_uri: string;
|
||||||
|
website: string;
|
||||||
|
vapid_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse {
|
||||||
|
id: number;
|
||||||
|
created_at: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AppResponse {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
redirect_uri: string;
|
||||||
|
website: string;
|
||||||
|
vapid_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Status {
|
||||||
|
content_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
language: string;
|
||||||
|
media_ids: string[];
|
||||||
|
preview: boolean | string | number;
|
||||||
|
scheduled_at: string;
|
||||||
|
sensitive: string | boolean | number;
|
||||||
|
spoiler_text: string;
|
||||||
|
status: string;
|
||||||
|
to: string[];
|
||||||
|
visibility: "direct" | "private" | "unlisted" | "public";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "Node 16",
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["es2021"],
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2021",
|
||||||
|
"strict": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue