doggo.js
Version:
Typed client for Doggo.Ninja's public API
355 lines (312 loc) • 7.64 kB
text/typescript
import 'isomorphic-unfetch'
export interface File {
url: string
shortName: string
originalName: string
mimeType: string
uncachedHits: number
updatedAt: number
size: number
parent?: string
private: boolean
ephemeralTimestamp: number | null
hasPassword: boolean
}
export interface Folder {
id: string
name: string
parent?: string
}
export type User = {
id: string
name: string
email?: string
admin: boolean
usage: number
preferredDomain: string
}
export type UsageInfo = {
username: string
usage: number
email: string
}
export class PatClient {
private baseUrl: string
token?: string
constructor(baseUrl?: string) {
this.baseUrl = baseUrl ?? 'https://pat.doggo.ninja'
}
async files(parent: string | undefined): Promise<File[]> {
return await this.makeRequest('get', '/v1/files', { parent })
}
async upload(
file: globalThis.File,
parent: string | undefined,
onProgress?: (loaded: number, total: number) => void
): Promise<File> {
const url = new URL(`${this.baseUrl}/v1/upload`)
if (parent) url.searchParams.set('parent', parent)
url.searchParams.set('mimeType', file.type)
if (file.name) url.searchParams.set('originalName', file.name)
return await this.makeUploadRequest(
'post',
url.toString(),
file,
onProgress
)
}
async replace(
shortName: string,
file: globalThis.File,
onProgress?: (loaded: number, total: number) => void
): Promise<File> {
const url = new URL(
`${this.baseUrl}/v1/file/${encodeURIComponent(shortName)}`
)
url.searchParams.set('mimeType', file.type)
if (file.name) url.searchParams.set('originalName', file.name)
return await this.makeUploadRequest('put', url.toString(), file, onProgress)
}
async getDownloadToken(
shortName: string,
password?: string
): Promise<string> {
const { downloadToken } = await this.makeRequest<{ downloadToken: string }>(
'post',
'/v1/files/token',
{},
{ shortName, password }
)
return downloadToken
}
async updateFileSharing(
shortName: string,
isPrivate: boolean,
details: { ephemeralTimestamp: number | null; password: string | boolean }
): Promise<File> {
return await this.makeRequest(
'post',
'/v1/files/sharing',
{},
{ shortName, private: isPrivate, ...details }
)
}
async moveFile(
shortName: string,
details: { originalName?: string; parent?: string | undefined },
copy?: boolean
): Promise<File> {
return await this.makeRequest(
'post',
'/v1/files/move',
{},
{
shortName,
...details,
forceMove: 'parent' in details,
copy
}
)
}
async getFile(shortName: string): Promise<File> {
return await this.makeRequest(
'get',
`/v1/file/${encodeURIComponent(shortName)}`
)
}
async deleteFile(shortName: string): Promise<File> {
return await this.makeRequest(
'delete',
`/v1/file/${encodeURIComponent(shortName)}`
)
}
async deleteFolder(id: string): Promise<File> {
return await this.makeRequest(
'delete',
`/v1/folder/${encodeURIComponent(id)}`
)
}
async folders(parent: string | undefined): Promise<Folder[]> {
return await this.makeRequest('get', '/v1/folders', { parent })
}
async createFolder(
name: string,
parent: string | undefined
): Promise<Folder> {
return await this.makeRequest(
'post',
'/v1/folders/create',
{},
{ name, parent }
)
}
async moveFolder(
id: string,
details: { name?: string; parent?: string | undefined }
): Promise<File> {
return await this.makeRequest(
'post',
'/v1/folders/move',
{},
{
id,
...details,
forceMove: 'parent' in details
}
)
}
async getFolder(id: string): Promise<Folder[]> {
return await this.makeRequest('get', `/v1/folder/${encodeURIComponent(id)}`)
}
async checkAuth(): Promise<boolean> {
try {
await this.makeRequest('get', '/v1/auth/check')
return true
} catch {
return false
}
}
async login(
username: string,
password: string
): Promise<{ sessionToken: string; expiration: Date }> {
const response = await this.makeRequest<{
sessionToken: string
expiration: number
}>(
'post',
'/v1/auth/login',
{},
{
name: username,
password
}
)
return {
sessionToken: response.sessionToken,
expiration: new Date(response.expiration)
}
}
async resetPassword(nameOrEmail: string): Promise<void> {
await this.makeRequest('post', '/v1/auth/reset', {}, { nameOrEmail })
}
async completeResetPassword(
resetToken: string,
newPassword: string,
regenerateAccessToken: boolean
): Promise<void> {
await this.makeRequest(
'post',
'/v1/auth/reset/complete',
{},
{
resetToken,
newPassword,
regenerateAccessToken
}
)
}
async invalidateSession(): Promise<void> {
await this.makeRequest('get', '/v1/auth/invalidate')
}
async regenerateAccessToken(): Promise<string> {
const { newToken } = await this.makeRequest<{ newToken: string }>(
'get',
'/v1/auth/regenerate'
)
return newToken
}
async me(): Promise<User> {
return await this.makeRequest('get', '/v1/me')
}
async setDomain(domain: 'doggo.ninja' | 'ninja.dog'): Promise<User> {
return await this.makeRequest('post', '/v1/domain', {}, { domain })
}
async adminUsage(): Promise<UsageInfo[]> {
return await this.makeRequest('get', '/v1/admin/usage')
}
async makeUser(username: string, email: string): Promise<void> {
await this.makeRequest(
'post',
'/v1/admin/mkuser',
{},
{
name: username,
email
}
)
}
async makeUploadRequest<Type = {}>(
method: 'post' | 'put' | 'delete',
url: string,
file: globalThis.File,
onProgress?: (loaded: number, total: number) => void
): Promise<Type> {
if (typeof 'window' === 'undefined') {
throw new Error('File uploads only supported in browser')
}
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.upload.addEventListener('progress', event => {
onProgress && onProgress(event.loaded, event.total)
})
xhr.addEventListener('readystatechange', () => {
if (xhr.readyState === 4) {
const body = xhr.responseText
try {
const json = JSON.parse(body)
if (xhr.status >= 200 && xhr.status < 300) {
resolve(json)
} else {
reject(
new Error(json.message || xhr.statusText || 'No error info')
)
}
} catch {
reject(new Error('Unable to parse json'))
}
}
})
xhr.addEventListener('error', () => {
reject(new Error('An unexpected error occurred'))
})
xhr.open(method, url)
xhr.setRequestHeader('Authorization', `Bearer ${this.token}`)
xhr.setRequestHeader('Content-Type', 'application/octet-stream')
xhr.send(file)
})
}
async makeRequest<Type = {}>(
method: 'get' | 'post' | 'put' | 'delete',
path: string,
query: Record<string, string | undefined> = {},
body?: Record<string, unknown>
): Promise<Type> {
const headers: HeadersInit = {
Accept: 'application/json'
}
if (this.token) headers['Authorization'] = `Bearer ${this.token}`
if (body) headers['Content-Type'] = 'application/json'
const url = new URL(this.baseUrl.concat(path))
for (const [key, value] of Object.entries(query)) {
if (value) url.searchParams.set(key, value)
}
const res = await fetch(url.toString(), {
method,
headers,
body: JSON.stringify(body)
})
if (!res.ok) {
let message
try {
message = (await res.json()).message
} catch {
message = res.statusText
}
message = message ?? 'No error info'
throw new Error(message)
}
const json = await res.json()
return json as Type
}
}