feat: upload file to remote

This commit is contained in:
sevichecc 2023-04-16 00:03:38 +08:00
parent 57cd5db168
commit 24f904a60f
Signed by untrusted user who does not match committer: SevicheCC
GPG key ID: C577000000000000
4 changed files with 98 additions and 19 deletions

View file

@ -1,6 +1,16 @@
import fetch from "node-fetch"; import fetch from "node-fetch";
import fs from "fs";
import { FormData, File } from "node-fetch";
import { OAuth, getPreferenceValues } from "@raycast/api"; import { OAuth, getPreferenceValues } from "@raycast/api";
import { Credentials, Preference, Status, StatusResponse, Account } from "./types"; import {
Credentials,
Preference,
Status,
StatusResponse,
Account,
StatusAttachment,
UploadAttachResponse,
} from "./types";
import { client } from "./oauth"; import { client } from "./oauth";
export const fetchToken = async (params: URLSearchParams, errorMessage: string): Promise<OAuth.TokenResponse> => { export const fetchToken = async (params: URLSearchParams, errorMessage: string): Promise<OAuth.TokenResponse> => {
@ -43,6 +53,7 @@ export const postNewStatus = async ({
sensitive, sensitive,
scheduled_at, scheduled_at,
content_type, content_type,
media_ids,
}: Partial<Status>): Promise<StatusResponse> => { }: Partial<Status>): Promise<StatusResponse> => {
const { instance } = getPreferenceValues<Preference>(); const { instance } = getPreferenceValues<Preference>();
const tokenSet = await client.getTokens(); const tokenSet = await client.getTokens();
@ -51,7 +62,7 @@ export const postNewStatus = async ({
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + tokenSet?.accessToken, Authorization: `Bearer ${tokenSet?.accessToken}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
status, status,
@ -60,6 +71,7 @@ export const postNewStatus = async ({
sensitive, sensitive,
content_type, content_type,
scheduled_at, scheduled_at,
media_ids,
}), }),
}); });
@ -75,7 +87,7 @@ export const fetchAccountInfo = async (): Promise<Account> => {
const response = await fetch(`https://${instance}/api/v1/accounts/verify_credentials`, { const response = await fetch(`https://${instance}/api/v1/accounts/verify_credentials`, {
method: "GET", method: "GET",
headers: { headers: {
Authorization: "Bearer " + tokenSet?.accessToken, Authorization: `Bearer ${tokenSet?.accessToken}`,
}, },
}); });
@ -83,3 +95,28 @@ export const fetchAccountInfo = async (): Promise<Account> => {
return (await response.json()) as Account; 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 attachment = fs.readFileSync(file);
const attachmentData = new File([attachment], file);
await attachmentData.arrayBuffer();
const formData = new FormData();
formData.append("file", attachmentData);
formData.append("description", description ?? "");
const response = await fetch(`https://${instance}/api/v1/media/`, {
method: "POST",
headers: {
Authorization: `Bearer ${tokenSet?.accessToken}`,
},
body: formData,
});
if (!response.ok) throw new Error("Could not upload attechments");
return (await response.json()) as UploadAttachResponse;
};

View file

@ -2,17 +2,21 @@ import { Form, LaunchProps } from "@raycast/api";
import VisibilityDropdown from "./components/VisibilityDropdown"; import VisibilityDropdown from "./components/VisibilityDropdown";
import SimpleCommand from "./simple-status"; import SimpleCommand from "./simple-status";
import { Status } from "./types"; import { Status } from "./types";
import { useState } from "react";
interface CommandProps extends LaunchProps<{ draftValues: Status }> { interface CommandProps extends LaunchProps<{ draftValues: Status }> {
children?: React.ReactNode; children?: React.ReactNode;
} }
export default function DetailCommand(props: CommandProps) { export default function DetailCommand(props: CommandProps) {
const [files, setFiles] = useState<string[]>([]);
return ( return (
<SimpleCommand {...props}> <SimpleCommand {...props}>
<Form.FilePicker id="files" value={files} onChange={setFiles}/>
{files.length !== 0 && <Form.TextArea id="description" title="Alt text" />}
<Form.DatePicker id="datepicker" title="Scheduled Time" /> <Form.DatePicker id="datepicker" title="Scheduled Time" />
<VisibilityDropdown /> <VisibilityDropdown />
<Form.FilePicker id="files" />
</SimpleCommand> </SimpleCommand>
); );
} }

View file

@ -13,7 +13,7 @@ import {
LaunchProps, LaunchProps,
} from "@raycast/api"; } from "@raycast/api";
import { postNewStatus } from "./api"; import { postNewStatus, uploadAttachment } from "./api";
import { AkkomaError, StatusResponse, Preference, Status } from "./types"; import { AkkomaError, StatusResponse, Preference, Status } from "./types";
import { authorize } from "./oauth"; import { authorize } from "./oauth";
@ -28,6 +28,11 @@ interface CommandProps extends LaunchProps<{ draftValues: SimpleStatus }> {
children?: React.ReactNode; children?: React.ReactNode;
} }
interface StausForm extends Status {
files: string[];
description?: string;
}
export default function SimpleCommand(props: CommandProps) { export default function SimpleCommand(props: CommandProps) {
const { instance } = getPreferenceValues<Preference>(); const { instance } = getPreferenceValues<Preference>();
const { draftValues } = props; const { draftValues } = props;
@ -52,15 +57,29 @@ export default function SimpleCommand(props: CommandProps) {
init(); init();
}, []); }, []);
const handleSubmit = async (values: Status) => { const handleSubmit = async ({ spoiler_text, status, scheduled_at, visibility, files, description }: StausForm) => {
try { try {
if (!values.status) throw new Error("You might forget the content, right ? |・ω・)"); if (!status) throw new Error("You might forget the content, right ? |・ω・)");
showToast(Toast.Style.Animated, "Publishing to the Fediverse ... ᕕ( ᐛ )ᕗ"); showToast(Toast.Style.Animated, "Publishing to the Fediverse ... ᕕ( ᐛ )ᕗ");
const response = await postNewStatus({ const mediaIds = await Promise.all(
...values, files?.map(async (file) => {
const { id} = await uploadAttachment({ file, description });
return id;
})
);
const newStatus: Partial<Status> = {
spoiler_text,
status,
scheduled_at,
visibility,
content_type: isMarkdown ? "text/markdown" : "text/plain", content_type: isMarkdown ? "text/markdown" : "text/plain",
}); media_ids: mediaIds,
};
const response = await postNewStatus(newStatus);
setStatusInfo(response); setStatusInfo(response);
cache.set("latest_published_status", JSON.stringify(response)); cache.set("latest_published_status", JSON.stringify(response));
@ -75,7 +94,7 @@ export default function SimpleCommand(props: CommandProps) {
} }
}; };
const handleCw = (value:boolean) => { const handleCw = (value: boolean) => {
setShowCw(value); setShowCw(value);
if (cwRef.current) { if (cwRef.current) {
cwRef.current.focus(); cwRef.current.focus();
@ -105,7 +124,7 @@ export default function SimpleCommand(props: CommandProps) {
/> />
)} )}
<StatusContent isMarkdown={isMarkdown} draftStatus={draftValues?.status} /> <StatusContent isMarkdown={isMarkdown} draftStatus={draftValues?.status} />
<VisibilityDropdown /> {!props.children && <VisibilityDropdown />}
{props.children} {props.children}
<Form.Checkbox id="markdown" title="Markdown" label="" value={isMarkdown} onChange={setIsMarkdown} storeValue /> <Form.Checkbox id="markdown" title="Markdown" label="" value={isMarkdown} onChange={setIsMarkdown} storeValue />
<Form.Checkbox id="showCw" title="Sensitive" label="" value={showCw} onChange={handleCw} storeValue /> <Form.Checkbox id="showCw" title="Sensitive" label="" value={showCw} onChange={handleCw} storeValue />

View file

@ -47,20 +47,20 @@ interface Poll {
} }
export interface Status { export interface Status {
spoiler_text: string; spoiler_text?: string;
status: string; status: string;
content_type: string; content_type: string;
expires_in: number; visibility: VisibilityScope;
expires_in?: number;
in_reply_to_conversation_id?: string; in_reply_to_conversation_id?: string;
in_reply_to_id?: string; in_reply_to_id?: string;
language: string; language?: string;
media_ids: string[]; media_ids?: string[];
poll?: Poll; poll?: Poll;
preview?: boolean | string | number; preview?: boolean | string | number;
scheduled_at: Date; scheduled_at?: Date;
sensitive: string | boolean | number; sensitive?: string | boolean | number;
to?: string[]; to?: string[];
visibility: VisibilityScope;
} }
export interface StatusResponse { export interface StatusResponse {
@ -77,3 +77,22 @@ export interface Account {
fqn: string; fqn: string;
avatar_static: string; avatar_static: string;
} }
export interface StatusAttachment {
file: string;
description?: string;
focus?: { x: number; y: number };
}
export interface UploadAttachResponse{
description: string | null;
id: string;
pleroma: {
mime_type: string;
}
preview_url: string;
remote_url: string | null;
text_url: string;
type: "image" | "video" | "audio" | "unknown",
url: string;
}