import { AxiosError } from "axios"; import { applyQueryParameters } from "../utilities/url"; import { axios, apiBaseUrl, extractResponseData, convertToNetworkError, base64, convertToIfStatusCodeIs, convertToIfErrorCodeIs, BlobWithEtag, NotModified, convertToNotModified, convertToForbiddenError, convertToBlobWithEtag, } 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; title: string; name: string; description: string; owner: HttpUser; visibility: TimelineVisibility; lastModified: Date; members: HttpUser[]; isHighlight: boolean; isBookmark: boolean; } 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 { title?: string; 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 -------------------- export interface RawHttpTimelineInfo { uniqueId: string; title: string; name: string; description: string; owner: HttpUser; visibility: TimelineVisibility; lastModified: string; members: HttpUser[]; isHighlight: boolean; isBookmark: boolean; } interface RawTimelinePostTextContent { type: "text"; text: string; } interface RawTimelinePostImageContent { type: "image"; url: string; } type RawTimelinePostContent = | RawTimelinePostTextContent | RawTimelinePostImageContent; interface RawTimelinePostInfo { id: number; content: RawTimelinePostContent; 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 -------------------- export function processRawTimelineInfo( raw: RawHttpTimelineInfo ): HttpTimelineInfo { return { ...raw, lastModified: new Date(raw.lastModified), }; } 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; getTimeline( timelineName: string, query: { checkUniqueId?: string; } ): Promise; getTimeline( timelineName: string, query: { checkUniqueId?: string; ifModifiedSince: Date; } ): Promise; postTimeline(req: HttpTimelinePostRequest): Promise; patchTimeline( timelineName: string, req: HttpTimelinePatchRequest ): Promise; deleteTimeline(timelineName: string): Promise; memberPut(timelineName: string, username: string): Promise; memberDelete(timelineName: string, username: string): Promise; listPost(timelineName: string): Promise; listPost( timelineName: string, query: { modifiedSince?: Date; includeDeleted?: false; } ): Promise; listPost( timelineName: string, query: { modifiedSince?: Date; includeDeleted: true; } ): Promise; getPostData(timelineName: string, postId: number): Promise; getPostData( timelineName: string, postId: number, etag: string ): Promise; postPost( timelineName: string, req: HttpTimelinePostPostRequest ): Promise; deletePost(timelineName: string, postId: number): Promise; } export class HttpTimelineClient implements IHttpTimelineClient { listTimeline(query: HttpTimelineListQuery): Promise { return axios .get( applyQueryParameters(`${apiBaseUrl}/timelines`, query) ) .then(extractResponseData) .then((list) => list.map(processRawTimelineInfo)) .catch(convertToNetworkError); } getTimeline(timelineName: string): Promise; getTimeline( timelineName: string, query: { checkUniqueId?: string; } ): Promise; getTimeline( timelineName: string, query: { checkUniqueId?: string; ifModifiedSince: Date; } ): Promise; getTimeline( timelineName: string, query?: { checkUniqueId?: string; ifModifiedSince?: Date; } ): Promise { return axios .get( applyQueryParameters(`${apiBaseUrl}/timelines/${timelineName}`, query) ) .then((res) => { if (res.status === 304) { return new NotModified(); } else { return processRawTimelineInfo(res.data); } }) .catch(convertToIfStatusCodeIs(404, HttpTimelineNotExistError)) .catch(convertToNetworkError); } postTimeline(req: HttpTimelinePostRequest): Promise { return axios .post(`${apiBaseUrl}/timelines`, req) .then(extractResponseData) .then(processRawTimelineInfo) .catch(convertToIfErrorCodeIs(11040101, HttpTimelineNameConflictError)) .catch(convertToNetworkError); } patchTimeline( timelineName: string, req: HttpTimelinePatchRequest ): Promise { return axios .patch( `${apiBaseUrl}/timelines/${timelineName}`, req ) .then(extractResponseData) .then(processRawTimelineInfo) .catch(convertToNetworkError); } deleteTimeline(timelineName: string): Promise { return axios .delete(`${apiBaseUrl}/timelines/${timelineName}`) .catch(convertToNetworkError) .then(); } memberPut(timelineName: string, username: string): Promise { return axios .put(`${apiBaseUrl}/timelines/${timelineName}/members/${username}`) .catch(convertToNetworkError) .then(); } memberDelete(timelineName: string, username: string): Promise { return axios .delete(`${apiBaseUrl}/timelines/${timelineName}/members/${username}`) .catch(convertToNetworkError) .then(); } listPost(timelineName: string): Promise; listPost( timelineName: string, query: { modifiedSince?: Date; includeDeleted?: false; } ): Promise; listPost( timelineName: string, query: { modifiedSince?: Date; includeDeleted: true; } ): Promise; listPost( timelineName: string, query?: { modifiedSince?: Date; includeDeleted?: boolean; } ): Promise { return axios .get( applyQueryParameters( `${apiBaseUrl}/timelines/${timelineName}/posts`, query ) ) .then(extractResponseData) .catch(convertToIfStatusCodeIs(404, HttpTimelineNotExistError)) .catch(convertToForbiddenError) .catch(convertToNetworkError) .then((rawPosts) => rawPosts.map((raw) => processRawTimelinePostInfo(raw)) ); } getPostData(timelineName: string, postId: number): Promise; getPostData( timelineName: string, postId: number, etag?: string ): Promise { const headers = etag != null ? { "If-None-Match": etag, } : undefined; const url = `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}/data`; return axios .get(url, { responseType: "blob", headers, }) .then(convertToBlobWithEtag) .catch(convertToNotModified) .catch(convertToIfStatusCodeIs(404, HttpTimelinePostNotExistError)) .catch(convertToNetworkError); } async postPost( timelineName: string, req: HttpTimelinePostPostRequest ): 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`, rawReq ) .then(extractResponseData) .catch(convertToNetworkError) .then((rawPost) => processRawTimelinePostInfo(rawPost)); } deletePost(timelineName: string, postId: number): Promise { return axios .delete(`${apiBaseUrl}/timelines/${timelineName}/posts/${postId}`) .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; }