mirror of
https://github.com/Sevichecc/raycast-akkoma-extension.git
synced 2025-04-29 22:29: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