refactor: api&oauth

This commit is contained in:
sevichecc 2023-04-16 01:23:23 +08:00
parent 24f904a60f
commit f50b45aaed
Signed by untrusted user who does not match committer: SevicheCC
GPG key ID: C577000000000000
5 changed files with 101 additions and 116 deletions

View file

@ -12,11 +12,31 @@ import {
UploadAttachResponse,
} from "./types";
import { client } from "./oauth";
import { RequestInit, Response } from "node-fetch";
export const fetchToken = async (params: URLSearchParams, errorMessage: string): Promise<OAuth.TokenResponse> => {
const { instance } = getPreferenceValues<Preference>();
const response = await fetch(`https://${instance}/oauth/token`, {
const CONFIG = {
tokenUrl: "/oauth/token",
appUrl: "/api/v1/apps",
statusesUrl: "/api/v1/statuses",
verifyCredentialsUrl: "/api/v1/accounts/verify_credentials",
mediaUrl: "/api/v1/media/",
};
const apiUrl = (instance: string, path: string): string => `https://${instance}${path}`;
const fetchWithAuth = async (url: string, options: RequestInit = {}): Promise<Response> => {
const tokenSet = await client.getTokens();
const headers = {
...options.headers,
Authorization: `Bearer ${tokenSet?.accessToken}`,
};
return fetch(url, { ...options, headers });
};
const fetchToken = async (params: URLSearchParams, errorMessage: string): Promise<OAuth.TokenResponse> => {
const response = await fetch(apiUrl(instance, CONFIG.tokenUrl), {
method: "POST",
body: params,
});
@ -25,14 +45,10 @@ export const fetchToken = async (params: URLSearchParams, errorMessage: string):
return (await response.json()) as OAuth.TokenResponse;
};
export const createApp = async (): Promise<Credentials> => {
const { instance } = getPreferenceValues<Preference>();
const response = await fetch(`https://${instance}/api/v1/apps`, {
const createApp = async (): Promise<Credentials> => {
const response = await fetch(apiUrl(instance, CONFIG.appUrl), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_name: "raycast-akkoma-extension",
redirect_uris: "https://raycast.com/redirect?packageName=Extension",
@ -46,60 +62,28 @@ export const createApp = async (): Promise<Credentials> => {
return (await response.json()) as Credentials;
};
export const postNewStatus = async ({
status,
visibility,
spoiler_text,
sensitive,
scheduled_at,
content_type,
media_ids,
}: Partial<Status>): Promise<StatusResponse> => {
const { instance } = getPreferenceValues<Preference>();
const tokenSet = await client.getTokens();
const response = await fetch(`https://${instance}/api/v1/statuses`, {
const postNewStatus = async (statusOptions: Partial<Status>): Promise<StatusResponse> => {
const response = await fetchWithAuth(apiUrl(instance, CONFIG.statusesUrl), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenSet?.accessToken}`,
},
body: JSON.stringify({
status,
visibility,
spoiler_text,
sensitive,
content_type,
scheduled_at,
media_ids,
}),
headers: { "Content-Type": "application/json" },
body: JSON.stringify(statusOptions),
});
if (!response.ok) throw new Error("Failed to pulish :(");
if (!response.ok) throw new Error("Failed to publish :(");
return (await response.json()) as StatusResponse;
};
export const fetchAccountInfo = async (): Promise<Account> => {
const { instance } = getPreferenceValues<Preference>();
const tokenSet = await client.getTokens();
const response = await fetch(`https://${instance}/api/v1/accounts/verify_credentials`, {
const fetchAccountInfo = async (): Promise<Account> => {
const response = await fetchWithAuth(apiUrl(instance, CONFIG.verifyCredentialsUrl), {
method: "GET",
headers: {
Authorization: `Bearer ${tokenSet?.accessToken}`,
},
});
if (!response.ok) throw new Error("Failed to fetch account's info :(");
return (await response.json()) as Account;
};
export const uploadAttachment = async ({ file, description }: StatusAttachment): Promise<UploadAttachResponse> => {
const { instance } = getPreferenceValues<Preference>();
const tokenSet = await client.getTokens();
const uploadAttachment = async ({ file, description }: StatusAttachment): Promise<UploadAttachResponse> => {
const attachment = fs.readFileSync(file);
const attachmentData = new File([attachment], file);
await attachmentData.arrayBuffer();
@ -108,15 +92,13 @@ export const uploadAttachment = async ({ file, description }: StatusAttachment):
formData.append("file", attachmentData);
formData.append("description", description ?? "");
const response = await fetch(`https://${instance}/api/v1/media/`, {
const response = await fetchWithAuth(apiUrl(instance, CONFIG.mediaUrl), {
method: "POST",
headers: {
Authorization: `Bearer ${tokenSet?.accessToken}`,
},
body: formData,
});
if (!response.ok) throw new Error("Could not upload attechments");
if (!response.ok) throw new Error("Could not upload attachments");
return (await response.json()) as UploadAttachResponse;
};
export default { fetchToken, createApp, postNewStatus, fetchAccountInfo, uploadAttachment };

View file

@ -4,11 +4,7 @@ import SimpleCommand from "./simple-status";
import { Status } from "./types";
import { useState } from "react";
interface CommandProps extends LaunchProps<{ draftValues: Status }> {
children?: React.ReactNode;
}
export default function DetailCommand(props: CommandProps) {
export default function DetailCommand(props: LaunchProps<{ draftValues: Partial<Status> }>) {
const [files, setFiles] = useState<string[]>([]);
return (

View file

@ -1,6 +1,6 @@
import { LocalStorage, OAuth, getPreferenceValues } from "@raycast/api";
import { Preference } from "./types";
import { fetchToken, createApp, fetchAccountInfo } from "./api";
import apiServer from "./api";
export const client = new OAuth.PKCEClient({
redirectMethod: OAuth.RedirectMethod.Web,
@ -10,37 +10,33 @@ export const client = new OAuth.PKCEClient({
description: "Connect to your Akkoma / Pleroma acount",
});
const requestAccessToken = async (
const requestToken = async (
clientId: string,
clientSecret: string,
authRequest: OAuth.AuthorizationRequest,
authCode: string
grantType: string,
authRequest?: OAuth.AuthorizationRequest,
authCode?: string,
refreshToken?: string
): Promise<OAuth.TokenResponse> => {
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);
params.append("grant_type", grantType);
return await fetchToken(params, "fetch tokens error:");
};
if (grantType === "authorization_code") {
params.append("code", authCode!);
params.append("code_verifier", authRequest!.codeVerifier);
params.append("redirect_uri", authRequest!.redirectURI);
} else {
params.append("refresh_token", refreshToken!);
}
const refreshToken = async (
clientId: string,
clientSecret: string,
refreshToken: string
): Promise<OAuth.TokenResponse> => {
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 tokenResponse = await fetchToken(params, "refresh tokens error:");
const tokenResponse = await apiServer.fetchToken(params, `Error while requesting ${grantType} tokens:`);
if (grantType === "refresh_token") {
tokenResponse.refresh_token = tokenResponse.refresh_token ?? refreshToken;
}
return tokenResponse;
};
@ -51,13 +47,15 @@ export const authorize = async (): Promise<void> => {
if (tokenSet?.accessToken) {
if (tokenSet.refreshToken && tokenSet.isExpired()) {
LocalStorage.clear();
const { client_id, client_secret } = await createApp();
await client.setTokens(await refreshToken(client_id, client_secret, tokenSet.refreshToken));
const { client_id, client_secret } = await apiServer.createApp();
await client.setTokens(
await requestToken(client_id, client_secret, "refresh_token", undefined, undefined, tokenSet.refreshToken)
);
}
return;
}
const { client_id, client_secret } = await createApp();
const { client_id, client_secret } = await apiServer.createApp();
const authRequest = await client.authorizationRequest({
endpoint: `https://${instance}/oauth/authorize`,
clientId: client_id,
@ -65,9 +63,13 @@ export const authorize = async (): Promise<void> => {
});
const { authorizationCode } = await client.authorize(authRequest);
await client.setTokens(await requestAccessToken(client_id, client_secret, authRequest, authorizationCode));
await client.setTokens(
await requestToken(client_id, client_secret, "authorization_code", authRequest, authorizationCode)
);
const { fqn, avatar_static } = await fetchAccountInfo();
const { fqn, avatar_static } = await apiServer.fetchAccountInfo();
await LocalStorage.setItem("account-fqn", fqn);
await LocalStorage.setItem("account-avator", avatar_static);
};
export default { authorize };

View file

@ -13,8 +13,10 @@ import {
LaunchProps,
} from "@raycast/api";
import { postNewStatus, uploadAttachment } from "./api";
import apiServer from "./api";
import { AkkomaError, StatusResponse, Preference, Status } from "./types";
import { authorize } from "./oauth";
import VisibilityDropdown from "./components/VisibilityDropdown";
@ -22,13 +24,11 @@ import StatusContent from "./components/StatusContent";
const cache = new Cache();
type SimpleStatus = Pick<Status, "content_type" | "status" | "spoiler_text" | "visibility">;
interface CommandProps extends LaunchProps<{ draftValues: SimpleStatus }> {
interface CommandProps extends LaunchProps<{ draftValues: Partial<Status> }> {
children?: React.ReactNode;
}
interface StausForm extends Status {
interface StatusForm extends Status {
files: string[];
description?: string;
}
@ -57,7 +57,7 @@ export default function SimpleCommand(props: CommandProps) {
init();
}, []);
const handleSubmit = async ({ spoiler_text, status, scheduled_at, visibility, files, description }: StausForm) => {
const handleSubmit = async ({ spoiler_text, status, scheduled_at, visibility, files, description }: StatusForm) => {
try {
if (!status) throw new Error("You might forget the content, right ? |・ω・)");
@ -65,7 +65,7 @@ export default function SimpleCommand(props: CommandProps) {
const mediaIds = await Promise.all(
files?.map(async (file) => {
const { id} = await uploadAttachment({ file, description });
const { id } = await apiServer.uploadAttachment({ file, description });
return id;
})
);
@ -79,7 +79,7 @@ export default function SimpleCommand(props: CommandProps) {
media_ids: mediaIds,
};
const response = await postNewStatus(newStatus);
const response = await apiServer.postNewStatus(newStatus);
setStatusInfo(response);
cache.set("latest_published_status", JSON.stringify(response));

View file

@ -1,23 +1,25 @@
import type { Icon } from "@raycast/api";
import type { Icon, LaunchProps } from "@raycast/api";
export interface AkkomaError {
code: string;
message: string;
}
export type VisibilityScope = "public" | "unlisted" | "direct" | "private" | "local";
export interface Preference {
instance: string;
defaultVisibility: VisibilityScope;
}
export type VisibilityScope = "public" | "unlisted" | "direct" | "private" | "local";
export interface VisibilityOption {
title: string;
value: VisibilityScope;
icon: Icon;
}
// Error
export interface AkkomaError {
code: string;
message: string;
}
// Application and Credentials
interface Application {
name: string;
website: string;
@ -33,12 +35,7 @@ export interface Credentials {
vapid_key: string;
}
export interface ApiResponse {
id: number;
created_at: string;
text: string;
}
// Statuses
interface Poll {
expired_in: number;
hide_totals?: boolean | string;
@ -63,6 +60,13 @@ export interface Status {
to?: string[];
}
// API Responses
export interface ApiResponse {
id: number;
created_at: string;
text: string;
}
export interface StatusResponse {
id: string;
create_at: Date;
@ -78,6 +82,7 @@ export interface Account {
avatar_static: string;
}
// Attachments
export interface StatusAttachment {
file: string;
description?: string;
@ -89,10 +94,10 @@ export interface UploadAttachResponse{
id: string;
pleroma: {
mime_type: string;
}
};
preview_url: string;
remote_url: string | null;
text_url: string;
type: "image" | "video" | "audio" | "unknown",
type: "image" | "video" | "audio" | "unknown";
url: string;
}