diff --git a/README.md b/README.md index b5e9182..203f148 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Post to Akkoma -Write new post and send it to Akkoma \ No newline at end of file +Write a new post and send it to Akkoma \ No newline at end of file diff --git a/package.json b/package.json index 0171800..669b461 100644 --- a/package.json +++ b/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", diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..1c1a3d3 --- /dev/null +++ b/src/api.ts @@ -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 => { + const { instance } = getPreferenceValues(); + + 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 => { + const { instance } = getPreferenceValues(); + + 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 ({ + status, + visibility, + spoiler_text, + sensitive, + scheduled_at, +}: Status): Promise => { + const { instance } = getPreferenceValues(); + 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; +}; diff --git a/src/index.tsx b/src/index.tsx index a0f8eb7..c770f75 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,78 +1,51 @@ -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(); - 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] - ); + const visibilityOptions: VisibilityOption[] = [ + { value: "direct", title: "Direct" }, + { value: "private", title: "Private" }, + { value: "unlisted", title: "Unlisted" }, + { value: "public", title: "Public" }, + { value: "local", title: "Local" }, + ]; + + 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!" }); + } + }; return ( - <> -
- - - } - > - - - - - - - - - - - - - - +
+ + + } + > + + + + + + {visibilityOptions.map(({value, title}) => ( + + ))} + + + + ); } diff --git a/src/oauth.ts b/src/oauth.ts index 16ee75c..1f97b9b 100644 --- a/src/oauth.ts +++ b/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 => { - const { instance } = getPreferenceValues(); - - 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 => { - const { instance } = getPreferenceValues(); 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 => { - const { instance } = getPreferenceValues(); 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 => { const { instance } = getPreferenceValues(); 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)); diff --git a/src/types.ts b/src/types.ts index 2192d87..f570d6a 100644 --- a/src/types.ts +++ b/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; +// } \ No newline at end of file