mirror of
https://github.com/Sevichecc/raycast-akkoma-extension.git
synced 2025-04-30 14:49:29 +08:00
refactor: seperate api and oauth
This commit is contained in:
parent
2e0377f7f4
commit
f9f8bbe3b6
6 changed files with 297 additions and 140 deletions
|
@ -1,3 +1,3 @@
|
|||
# Post to Akkoma
|
||||
|
||||
Write new post and send it to Akkoma
|
||||
Write a new post and send it to Akkoma
|
14
package.json
14
package.json
|
@ -28,7 +28,7 @@
|
|||
"placeholder": "such as: example.dev"
|
||||
},
|
||||
{
|
||||
"name": "defaultVisbility",
|
||||
"name": "defaultVisibility",
|
||||
"type": "dropdown",
|
||||
"required": false,
|
||||
"data": [
|
||||
|
@ -36,17 +36,21 @@
|
|||
"title": "🌎 Public",
|
||||
"value": "public"
|
||||
},
|
||||
{
|
||||
"title": "👥 Private",
|
||||
"value": "private"
|
||||
},
|
||||
{
|
||||
"title": "🙈 Unlist",
|
||||
"value": "unlist"
|
||||
},
|
||||
{
|
||||
"title": "👥 Private",
|
||||
"value": "private"
|
||||
},
|
||||
{
|
||||
"title": "✉️ Direct",
|
||||
"value": "direct"
|
||||
},
|
||||
{
|
||||
"title": "📍 Local",
|
||||
"value": "local"
|
||||
}
|
||||
],
|
||||
"title": "Akkoma instance's URL",
|
||||
|
|
75
src/api.ts
Normal file
75
src/api.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import fetch from "node-fetch";
|
||||
import { OAuth, getPreferenceValues } from "@raycast/api";
|
||||
import { AppResponse, Preference, Status } from "./types";
|
||||
import { authorize } from "./oauth";
|
||||
|
||||
export const fetchToken = async (params: URLSearchParams, errorMessage: string): Promise<OAuth.TokenResponse> => {
|
||||
const { instance } = getPreferenceValues<Preference>();
|
||||
|
||||
const response = await fetch(`https://${instance}/oauth/token`, {
|
||||
method: "POST",
|
||||
body: params,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(errorMessage, await response.text());
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return (await response.json()) as OAuth.TokenResponse;
|
||||
};
|
||||
|
||||
export const createApp = 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: "https://raycast.com/redirect?packageName=Extension",
|
||||
scopes: "read write push",
|
||||
website: "https://raycast.com",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create Akkoma app");
|
||||
}
|
||||
|
||||
return (await response.json()) as AppResponse;
|
||||
};
|
||||
|
||||
export const postNewStatus = async <T>({
|
||||
status,
|
||||
visibility,
|
||||
spoiler_text,
|
||||
sensitive,
|
||||
scheduled_at,
|
||||
}: Status): Promise<T> => {
|
||||
const { instance } = getPreferenceValues<Preference>();
|
||||
const token = await authorize();
|
||||
|
||||
const response = await fetch(`https://${instance}/api/v1/statuses`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer" + token,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
status,
|
||||
visibility,
|
||||
spoiler_text,
|
||||
sensitive,
|
||||
scheduled_at,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to pulish new status");
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
};
|
|
@ -1,58 +1,33 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
import { 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 }[];
|
||||
};
|
||||
import { authorize } from "./oauth";
|
||||
import { postNewStatus } from "./api";
|
||||
import { Status, VisibilityOption } from "./types";
|
||||
|
||||
export default function Command() {
|
||||
const { instance } = getPreferenceValues<Preference>();
|
||||
|
||||
useEffect(() => {
|
||||
authorize();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (values: Values) => {
|
||||
try {
|
||||
const token = await client.getTokens();
|
||||
const visibilityOptions: VisibilityOption[] = [
|
||||
{ value: "direct", title: "Direct" },
|
||||
{ value: "private", title: "Private" },
|
||||
{ value: "unlisted", title: "Unlisted" },
|
||||
{ value: "public", title: "Public" },
|
||||
{ value: "local", title: "Local" },
|
||||
];
|
||||
|
||||
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);
|
||||
const handleSubmit = async (values: Status) => {
|
||||
try {
|
||||
await postNewStatus({ ...values });
|
||||
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>
|
||||
|
@ -61,18 +36,16 @@ export default function Command() {
|
|||
}
|
||||
>
|
||||
<Form.TextField id="textfield" title="Content Warning" placeholder="" />
|
||||
<Form.TextArea id="textarea" title="Post detail" placeholder="" enableMarkdown={true} />
|
||||
<Form.TextArea id="staus" 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 id="visibility" title="Visibility" storeValue={true} defaultValue="">
|
||||
{visibilityOptions.map(({value, title}) => (
|
||||
<Form.Dropdown.Item key={value} value={value} title={title} />
|
||||
))}
|
||||
</Form.Dropdown>
|
||||
<Form.FilePicker id="files" />
|
||||
<Form.Checkbox id="sensitive" title="Sensitive" label="Sensitive" />
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
65
src/oauth.ts
65
src/oauth.ts
|
@ -1,10 +1,8 @@
|
|||
import { OAuth, getPreferenceValues } from "@raycast/api";
|
||||
import fetch from "node-fetch";
|
||||
import { Preference, AppResponse } from "./types";
|
||||
import { Preference } from "./types";
|
||||
import { fetchToken,createApp} from "./api";
|
||||
|
||||
const redirectUri = "https://raycast.com/redirect?packageName=Extension";
|
||||
|
||||
export const client = new OAuth.PKCEClient({
|
||||
const client = new OAuth.PKCEClient({
|
||||
redirectMethod: OAuth.RedirectMethod.Web,
|
||||
providerName: "Akkoma",
|
||||
providerIcon: "akkoma-icon.png",
|
||||
|
@ -12,37 +10,12 @@ export const client = new OAuth.PKCEClient({
|
|||
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 (
|
||||
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);
|
||||
|
@ -52,23 +25,14 @@ export const requestAccessToken = async (
|
|||
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;
|
||||
return await fetchToken(params, "fetch tokens error:");
|
||||
};
|
||||
|
||||
export const refreshToken = async (
|
||||
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);
|
||||
|
@ -76,40 +40,33 @@ export const refreshToken = async (
|
|||
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 fetchToken(params, "refresh tokens error:");
|
||||
|
||||
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();
|
||||
const { client_id, client_secret } = await createApp();
|
||||
await client.setTokens(await refreshToken(client_id, client_secret, tokenSet.refreshToken));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { client_id, client_secret } = await createAkkomaApp();
|
||||
const { client_id, client_secret } = await createApp();
|
||||
|
||||
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));
|
||||
|
|
174
src/types.ts
174
src/types.ts
|
@ -1,5 +1,13 @@
|
|||
export type VisibilityScope = "public" | "unlisted" | "direct" | "private" | "local"
|
||||
|
||||
export interface VisibilityOption {
|
||||
title: string;
|
||||
value: VisibilityScope
|
||||
}
|
||||
|
||||
export interface Preference {
|
||||
instance: string
|
||||
instance: string;
|
||||
defaultVisibility: VisibilityScope;
|
||||
}
|
||||
|
||||
export interface Credentials {
|
||||
|
@ -28,18 +36,158 @@ export interface AppResponse {
|
|||
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";
|
||||
interface Poll {
|
||||
expired_in: number;
|
||||
hide_totals?: boolean | string;
|
||||
multiple?: boolean | string | number;
|
||||
options: string[]
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
content_type?: string;
|
||||
expires_in?: number;
|
||||
in_reply_to_conversation_id?: string;
|
||||
in_reply_to_id?: string;
|
||||
language?: string;
|
||||
media_ids?: string[];
|
||||
poll?: Poll;
|
||||
preview?: boolean | string | number;
|
||||
scheduled_at?: string;
|
||||
sensitive?: string | boolean | number;
|
||||
spoiler_text?: string;
|
||||
status?: string;
|
||||
to?: string[];
|
||||
visibility?: VisibilityScope;
|
||||
}
|
||||
|
||||
// interface Account {
|
||||
// acct: string;
|
||||
// avatar: string;
|
||||
// avatar_static: string;
|
||||
// bot: boolean;
|
||||
// created_at: string;
|
||||
// display_name: string;
|
||||
// emojis: Emoji[];
|
||||
// fields: Field[];
|
||||
// followers_count: number;
|
||||
// following_count: number;
|
||||
// header: string;
|
||||
// header_static: string;
|
||||
// id: string;
|
||||
// is_confirmed: boolean;
|
||||
// note: string;
|
||||
// pleroma: Pleroma;
|
||||
// source: Source;
|
||||
// statuses_count: number;
|
||||
// url: string;
|
||||
// username: string;
|
||||
// }
|
||||
|
||||
// interface Emoji {
|
||||
// shortcode: string;
|
||||
// static_url: string;
|
||||
// url: string;
|
||||
// visible_in_picker: boolean;
|
||||
// }
|
||||
// interface Field {
|
||||
// name: string;
|
||||
// value: string;
|
||||
// verified_at: string | null;
|
||||
// }
|
||||
|
||||
// interface Pleroma {
|
||||
// background_image: null;
|
||||
// hide_favorites: boolean;
|
||||
// hide_followers: boolean;
|
||||
// hide_followers_count: boolean;
|
||||
// hide_follows: boolean;
|
||||
// hide_follows_count: boolean;
|
||||
// is_admin: boolean;
|
||||
// is_confirmed: boolean;
|
||||
// is_moderator: boolean;
|
||||
// relationship: Relationship;
|
||||
// skip_thread_containment: boolean;
|
||||
// tags: any[];
|
||||
// }
|
||||
// type Actor = "Application" | "Group" | "Organization" | "Person" | "Service"
|
||||
|
||||
// interface Relationship {
|
||||
// blocked_by: boolean;
|
||||
// blocking: boolean;
|
||||
// domain_blocking: boolean;
|
||||
// endorsed: boolean;
|
||||
// followed_by: boolean;
|
||||
// following: boolean;
|
||||
// id: string;
|
||||
// muting: boolean;
|
||||
// muting_notifications: boolean;
|
||||
// note: string;
|
||||
// notifying: boolean;
|
||||
// requested: boolean;
|
||||
// showing_reblogs: boolean;
|
||||
// subscribing: boolean;
|
||||
// }
|
||||
|
||||
// interface Source {
|
||||
// fields: Field[];
|
||||
// note: string;
|
||||
// pleroma: SourcePleroma;
|
||||
// privacy: VisibilityScope;
|
||||
// sensitive: boolean;
|
||||
// }
|
||||
|
||||
// interface SourcePleroma {
|
||||
// actor_type: Actor;
|
||||
// discoverable: boolean;
|
||||
// no_rich_text: boolean;
|
||||
// show_role: boolean;
|
||||
// }
|
||||
|
||||
// interface StatusResponse {
|
||||
// account: Account;
|
||||
// application: null;
|
||||
// bookmarked: boolean;
|
||||
// card: null;
|
||||
// content: string;
|
||||
// created_at: string;
|
||||
// emojis: any[];
|
||||
// favourited: boolean;
|
||||
// favourites_count: number;
|
||||
// id: string;
|
||||
// in_reply_to_account_id: null;
|
||||
// in_reply_to_id: null;
|
||||
// language: null;
|
||||
// media_attachments: any[];
|
||||
// mentions: any[];
|
||||
// muted: boolean;
|
||||
// pinned: boolean;
|
||||
// pleroma: StatusPleroma;
|
||||
// poll: null;
|
||||
// reblog: null;
|
||||
// reblogged: boolean;
|
||||
// reblogs_count: number;
|
||||
// replies_count: number;
|
||||
// sensitive: boolean;
|
||||
// spoiler_text: string;
|
||||
// tags: any[];
|
||||
// uri: string;
|
||||
// url: string;
|
||||
// visibility: string;
|
||||
// }
|
||||
|
||||
// interface StatusPleroma {
|
||||
// content: PleromaContent;
|
||||
// context: string;
|
||||
// conversation_id: number;
|
||||
// direct_conversation_id: null;
|
||||
// emoji_reactions: any[];
|
||||
// expires_at: null;
|
||||
// in_reply_to_account_acct: null;
|
||||
// local: boolean;
|
||||
// spoiler_text: PleromaContent;
|
||||
// thread_muted: boolean;
|
||||
// }
|
||||
|
||||
// interface PleromaContent {
|
||||
// "text/plain": string;
|
||||
// }
|
Loading…
Reference in a new issue