import axios, { AxiosError } from 'axios'; import { updateQueryString, applyQueryParameters } from '../utilities/url'; import { apiBaseUrl, extractResponseData, convertToNetworkError, base64, convertToIfStatusCodeIs, convertToIfErrorCodeIs, BlobWithEtag, NotModified, convertToBlobWithEtagOrNotModified, } from './common'; import { HttpUser } from './user'; export const kTimelineVisibilities = ['Public', 'Register', 'Private'] as const; export type TimelineVisibility = typeof kTimelineVisibilities[number]; export interface HttpTimelineInfo { uniqueId: string; name: string; description: string; owner: HttpUser; visibility: TimelineVisibility; members: HttpUser[]; } export interface HttpTimelineListQuery { visibility?: TimelineVisibility; relate?: string; relateType?: 'own' | 'join'; } export interface HttpTimelinePostRequest { name: string; } export interface HttpTimelinePostTextContent { type: 'text'; text: string; } export interface HttpTimelinePostImageContent { type: 'image'; } export type HttpTimelinePostContent = | HttpTimelinePostTextContent | HttpTimelinePostImageContent; export interface HttpTimelinePostInfo { id: number; content: HttpTimelinePostContent; time: Date; lastUpdated: Date; author: HttpUser; deleted: false; } export interface HttpTimelineDeletedPostInfo { id: number; time: Date; lastUpdated: Date; author: HttpUser; deleted: true; } export type HttpTimelineGenericPostInfo = | HttpTimelinePostInfo | HttpTimelineDeletedPostInfo; export interface HttpTimelinePostPostRequestTextContent { type: 'text'; text: string; } export interface HttpTimelinePostPostRequestImageContent { type: 'image'; data: Blob; } export type HttpTimelinePostPostRequestContent = | HttpTimelinePostPostRequestTextContent | HttpTimelinePostPostRequestImageContent; export interface HttpTimelinePostPostRequest { content: HttpTimelinePostPostRequestContent; time?: Date; } export interface HttpTimelinePatchRequest { visibility?: TimelineVisibility; description?: string; } export class HttpTimelineNotExistError extends Error { constructor(public innerError?: AxiosError) { super(); } } export class HttpTimelinePostNotExistError extends Error { constructor(public innerError?: AxiosError) { super(); } } export class HttpTimelineNameConflictError extends Error { constructor(public innerError?: AxiosError) { super(); } } //-------------------- begin: internal model -------------------- interface RawTimelinePostTextContent { type: 'text'; text: string; } interface RawTimelinePostImageContent { type: 'image'; url: string; } type RawTimelinePostContent = | RawTimelinePostTextContent | RawTimelinePostImageContent; interface RawTimelinePostInfo { id: number; content: HttpTimelinePostContent; time: string; lastUpdated: string; author: HttpUser; deleted: false; } interface RawTimelineDeletedPostInfo { id: number; time: string; lastUpdated: string; author: HttpUser; deleted: true; } type RawTimelineGenericPostInfo = | RawTimelinePostInfo | RawTimelineDeletedPostInfo; interface RawTimelinePostPostRequestTextContent { type: 'text'; text: string; } interface RawTimelinePostPostRequestImageContent { type: 'image'; data: string; } type RawTimelinePostPostRequestContent = | RawTimelinePostPostRequestTextContent | RawTimelinePostPostRequestImageContent; interface RawTimelinePostPostRequest { content: RawTimelinePostPostRequestContent; time?: string; } //-------------------- end: internal model -------------------- function processRawTimelinePostInfo( raw: RawTimelinePostInfo ): HttpTimelinePostInfo; function processRawTimelinePostInfo( raw: RawTimelineGenericPostInfo ): HttpTimelineGenericPostInfo; function processRawTimelinePostInfo( raw: RawTimelineGenericPostInfo ): HttpTimelineGenericPostInfo { return { ...raw, time: new Date(raw.time), lastUpdated: new Date(raw.lastUpdated), }; } export interface IHttpTimelineClient { listTimeline(query: HttpTimelineListQuery): Promise; getTimeline(timelineName: string): Promise; postTimeline( req: HttpTimelinePostRequest, token: string ): Promise; patchTimeline( timelineName: string, req: HttpTimelinePatchRequest, token: string ): Promise; deleteTimeline(timelineName: string, token: string): Promise; memberPut( timelineName: string, username: string, token: string ): Promise; memberDelete( timelineName: string, username: string, token: string ): Promise; listPost( timelineName: string, token?: string ): Promise; listPost( timelineName: string, token: string | undefined, query: { modifiedSince?: Date; includeDeleted?: false; } ): Promise; listPost( timelineName: string, token: string | undefined, query: { modifiedSince?: Date; includeDeleted: true; } ): Promise; getPostData( timelineName: string, postId: number, token?: string ): Promise; getPostData( timelineName: string, postId: number, token: string | undefined, etag: string ): Promise; postPost( timelineName: string, req: HttpTimelinePostPostRequest, token: string ): Promise; deletePost( timelineName: string, postId: number, token: string ): Promise; } export class HttpTimelineClient implements IHttpTimelineClient { listTimeline(query: HttpTimelineListQuery): Promise { return axios .get( applyQueryParameters(`${apiBaseUrl}/timelines`, query) ) .then(extractResponseData) .catch(convertToNetworkError); } getTimeline(timelineName: string): Promise { return axios .get(`${apiBaseUrl}/timelines/${timelineName}`) .then(extractResponseData) .catch(convertToIfStatusCodeIs(404, HttpTimelineNotExistError)) .catch(convertToNetworkError); } postTimeline( req: HttpTimelinePostRequest, token: string ): Promise { return axios .post(`${apiBaseUrl}/timelines?token=${token}`, req) .then(extractResponseData) .catch(convertToIfErrorCodeIs(11040101, HttpTimelineNameConflictError)) .catch(convertToNetworkError); } patchTimeline( timelineName: string, req: HttpTimelinePatchRequest, token: string ): Promise { return axios .patch( `${apiBaseUrl}/timelines/${timelineName}?token=${token}`, req ) .then(extractResponseData) .catch(convertToNetworkError); } deleteTimeline(timelineName: string, token: string): Promise { return axios .delete(`${apiBaseUrl}/timelines/${timelineName}?token=${token}`) .catch(convertToNetworkError) .then(); } memberPut( timelineName: string, username: string, token: string ): Promise { return axios .put( `${apiBaseUrl}/timelines/${timelineName}/members/${username}?token=${token}` ) .catch(convertToNetworkError) .then(); } memberDelete( timelineName: string, username: string, token: string ): Promise { return axios .delete( `${apiBaseUrl}/timelines/${timelineName}/members/${username}?token=${token}` ) .catch(convertToNetworkError) .then(); } listPost( timelineName: string, token?: string ): Promise; listPost( timelineName: string, token: string | undefined, query: { modifiedSince?: Date; includeDeleted?: false; } ): Promise; listPost( timelineName: string, token: string | undefined, query: { modifiedSince?: Date; includeDeleted: true; } ): Promise; listPost( timelineName: string, token?: string, query?: { modifiedSince?: Date; includeDeleted?: boolean; } ): Promise { let url = `${apiBaseUrl}/timelines/${timelineName}/posts`; url = updateQueryString('token', token, url); if (query != null) { if (query.modifiedSince != null) { url = updateQueryString( 'modifiedSince', query.modifiedSince.toISOString(), url ); } if (query.includeDeleted != null) { url = updateQueryString( 'includeDeleted', query.includeDeleted ? 'true' : 'false', url ); } } return axios .get(url) .then(extractResponseData) .catch(convertToNetworkError) .then((rawPosts) => rawPosts.map((raw) => processRawTimelinePostInfo(raw)) ); } getPostData( timelineName: string, postId: number, token: string ): Promise; getPostData( timelineName: string, postId: number, token?: string, etag?: string ): Promise { const headers = etag != null ? { 'If-None-Match': etag, } : undefined; let url = `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}/data`; url = updateQueryString('token', token, url); return axios .get(url, { responseType: 'blob', headers, }) .then(convertToBlobWithEtagOrNotModified) .catch(convertToIfStatusCodeIs(404, HttpTimelinePostNotExistError)) .catch(convertToNetworkError); } async postPost( timelineName: string, req: HttpTimelinePostPostRequest, token: string ): Promise { let content: RawTimelinePostPostRequestContent; if (req.content.type === 'image') { const base64Data = await base64(req.content.data); content = { ...req.content, data: base64Data, } as RawTimelinePostPostRequestImageContent; } else { content = req.content; } const rawReq: RawTimelinePostPostRequest = { content, }; if (req.time != null) { rawReq.time = req.time.toISOString(); } return await axios .post( `${apiBaseUrl}/timelines/${timelineName}/posts?token=${token}`, rawReq ) .then(extractResponseData) .catch(convertToNetworkError) .then((rawPost) => processRawTimelinePostInfo(rawPost)); } deletePost( timelineName: string, postId: number, token: string ): Promise { return axios .delete( `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}?token=${token}` ) .catch(convertToNetworkError) .then(); } } let client: IHttpTimelineClient = new HttpTimelineClient(); export function getHttpTimelineClient(): IHttpTimelineClient { return client; } export function setHttpTimelineClient( newClient: IHttpTimelineClient ): IHttpTimelineClient { const old = client; client = newClient; return old; }