From 24f904a60fe19284272f3a77a25d6e471b67ac2e Mon Sep 17 00:00:00 2001 From: sevichecc <91365763+Sevichecc@users.noreply.github.com> Date: Sun, 16 Apr 2023 00:03:38 +0800 Subject: [PATCH] feat: upload file to remote --- src/api.ts | 43 ++++++++++++++++++++++++++++++++++++++++--- src/detail-status.tsx | 6 +++++- src/simple-status.tsx | 35 +++++++++++++++++++++++++++-------- src/types.ts | 33 ++++++++++++++++++++++++++------- 4 files changed, 98 insertions(+), 19 deletions(-) diff --git a/src/api.ts b/src/api.ts index 2a4ee1b..bc448f7 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,6 +1,16 @@ import fetch from "node-fetch"; +import fs from "fs"; +import { FormData, File } from "node-fetch"; 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"; export const fetchToken = async (params: URLSearchParams, errorMessage: string): Promise => { @@ -43,6 +53,7 @@ export const postNewStatus = async ({ sensitive, scheduled_at, content_type, + media_ids, }: Partial): Promise => { const { instance } = getPreferenceValues(); const tokenSet = await client.getTokens(); @@ -51,7 +62,7 @@ export const postNewStatus = async ({ method: "POST", headers: { "Content-Type": "application/json", - Authorization: "Bearer " + tokenSet?.accessToken, + Authorization: `Bearer ${tokenSet?.accessToken}`, }, body: JSON.stringify({ status, @@ -60,6 +71,7 @@ export const postNewStatus = async ({ sensitive, content_type, scheduled_at, + media_ids, }), }); @@ -75,7 +87,7 @@ export const fetchAccountInfo = async (): Promise => { const response = await fetch(`https://${instance}/api/v1/accounts/verify_credentials`, { method: "GET", headers: { - Authorization: "Bearer " + tokenSet?.accessToken, + Authorization: `Bearer ${tokenSet?.accessToken}`, }, }); @@ -83,3 +95,28 @@ export const fetchAccountInfo = async (): Promise => { return (await response.json()) as Account; }; + +export const uploadAttachment = async ({ file, description }: StatusAttachment): Promise => { + const { instance } = getPreferenceValues(); + 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; +}; diff --git a/src/detail-status.tsx b/src/detail-status.tsx index 55fcd86..048ff27 100644 --- a/src/detail-status.tsx +++ b/src/detail-status.tsx @@ -2,17 +2,21 @@ import { Form, LaunchProps } from "@raycast/api"; import VisibilityDropdown from "./components/VisibilityDropdown"; 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) { + const [files, setFiles] = useState([]); + return ( + + {files.length !== 0 && } - ); } diff --git a/src/simple-status.tsx b/src/simple-status.tsx index 7fb3186..a2229ed 100644 --- a/src/simple-status.tsx +++ b/src/simple-status.tsx @@ -13,7 +13,7 @@ import { LaunchProps, } from "@raycast/api"; -import { postNewStatus } from "./api"; +import { postNewStatus, uploadAttachment } from "./api"; import { AkkomaError, StatusResponse, Preference, Status } from "./types"; import { authorize } from "./oauth"; @@ -28,6 +28,11 @@ interface CommandProps extends LaunchProps<{ draftValues: SimpleStatus }> { children?: React.ReactNode; } +interface StausForm extends Status { + files: string[]; + description?: string; +} + export default function SimpleCommand(props: CommandProps) { const { instance } = getPreferenceValues(); const { draftValues } = props; @@ -52,15 +57,29 @@ export default function SimpleCommand(props: CommandProps) { init(); }, []); - const handleSubmit = async (values: Status) => { + const handleSubmit = async ({ spoiler_text, status, scheduled_at, visibility, files, description }: StausForm) => { 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 ... ᕕ( ᐛ )ᕗ"); - const response = await postNewStatus({ - ...values, + const mediaIds = await Promise.all( + files?.map(async (file) => { + const { id} = await uploadAttachment({ file, description }); + return id; + }) + ); + + const newStatus: Partial = { + spoiler_text, + status, + scheduled_at, + visibility, content_type: isMarkdown ? "text/markdown" : "text/plain", - }); + media_ids: mediaIds, + }; + + const response = await postNewStatus(newStatus); setStatusInfo(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); if (cwRef.current) { cwRef.current.focus(); @@ -105,7 +124,7 @@ export default function SimpleCommand(props: CommandProps) { /> )} - + {!props.children && } {props.children} diff --git a/src/types.ts b/src/types.ts index d8b2405..99980e7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -47,20 +47,20 @@ interface Poll { } export interface Status { - spoiler_text: string; + spoiler_text?: string; status: string; content_type: string; - expires_in: number; + visibility: VisibilityScope; + expires_in?: number; in_reply_to_conversation_id?: string; in_reply_to_id?: string; - language: string; - media_ids: string[]; + language?: string; + media_ids?: string[]; poll?: Poll; preview?: boolean | string | number; - scheduled_at: Date; - sensitive: string | boolean | number; + scheduled_at?: Date; + sensitive?: string | boolean | number; to?: string[]; - visibility: VisibilityScope; } export interface StatusResponse { @@ -77,3 +77,22 @@ export interface Account { fqn: 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; +} \ No newline at end of file