import { random, without, range } from "lodash"; import { BlobWithEtag, NotModified } from "../common"; import { IHttpTimelineClient, HttpTimelineInfo, TimelineVisibility, HttpTimelineListQuery, HttpTimelineNotExistError, HttpTimelinePostRequest, HttpTimelineNameConflictError, HttpTimelinePatchRequest, HttpTimelinePostInfo, HttpTimelinePostContent, HttpTimelinePostPostRequest, HttpTimelinePostNotExistError, HttpTimelineGenericPostInfo, } from "../timeline"; import { HttpUser } from "../user"; import { mockStorage, sha1, mockPrepare } from "./common"; import { getUser, MockUserNotExistError, checkToken } from "./user"; async function getTimelineNameList(): Promise { return (await mockStorage.getItem("timelines")) ?? []; } async function setTimelineNameList(newOne: string[]): Promise { await mockStorage.setItem("timelines", newOne); } type TimelinePropertyKey = | "uniqueId" | "lastModified" | "owner" | "description" | "visibility" | "members" | "currentPostId"; function getTimelinePropertyKey( name: string, property: TimelinePropertyKey ): string { return `timeline.${name}.${property}`; } function getTimelinePropertyValue( name: string, property: TimelinePropertyKey ): Promise { return mockStorage.getItem( getTimelinePropertyKey(name, property) ) as Promise; } function setTimelinePropertyValue( name: string, property: TimelinePropertyKey, value: T ): Promise { return mockStorage .setItem(getTimelinePropertyKey(name, property), value) .then(); } function updateTimelineLastModified(name: string): Promise { return setTimelinePropertyValue( name, "lastModified", new Date().toISOString() ); } interface HttpTimelineInfoEx extends HttpTimelineInfo { memberUsernames: string[]; } function createUniqueId(): string { const s = "abcdefghijklmnopqrstuvwxz0123456789"; let result = ""; for (let i = 0; i < 16; i++) { result += s[random(0, s.length - 1)]; } return result; } class MockTimelineNotExistError extends Error { constructor() { super("Timeline not exist."); } } class MockTimelineAlreadyExistError extends Error { constructor() { super("Timeline already exist."); } } async function getTimelineInfo(name: string): Promise { let owner: HttpUser; if (name.startsWith("@")) { const ownerUsername = name.substr(1); owner = await getUser(ownerUsername); const optionalUniqueId = await getTimelinePropertyValue( name, "uniqueId" ); if (optionalUniqueId == null) { await setTimelineNameList([...(await getTimelineNameList()), name]); await setTimelinePropertyValue(name, "uniqueId", createUniqueId()); await updateTimelineLastModified(name); } } else { const optionalOwnerUsername = await getTimelinePropertyValue( name, "owner" ); if (optionalOwnerUsername == null) { throw new MockTimelineNotExistError(); } else { owner = await getUser(optionalOwnerUsername); } } const memberUsernames = (await getTimelinePropertyValue(name, "members")) ?? []; const members = await Promise.all( memberUsernames.map(async (username) => { return await getUser(username); }) ); return { name, uniqueId: await getTimelinePropertyValue(name, "uniqueId"), owner, description: (await getTimelinePropertyValue(name, "description")) ?? "", visibility: (await getTimelinePropertyValue( name, "visibility" )) ?? "Register", lastModified: new Date( await getTimelinePropertyValue(name, "lastModified") ), members, memberUsernames, }; } async function createTimeline(name: string, owner: string): Promise { const optionalOwnerUsername = await getTimelinePropertyValue( name, "owner" ); if (optionalOwnerUsername != null) { throw new MockTimelineAlreadyExistError(); } await setTimelineNameList([...(await getTimelineNameList()), name]); await setTimelinePropertyValue(name, "uniqueId", createUniqueId()); await setTimelinePropertyValue(name, "owner", owner); await updateTimelineLastModified(name); } type TimelinePostPropertyKey = | "type" | "data" | "etag" | "author" | "time" | "lastUpdated"; function getTimelinePostPropertyKey( timelineName: string, id: number, propertyKey: TimelinePostPropertyKey ): string { return `timeline.${timelineName}.posts.${id}.${propertyKey}`; } function getTimelinePostPropertyValue( timelineName: string, id: number, propertyKey: TimelinePostPropertyKey ): Promise { return mockStorage.getItem( getTimelinePostPropertyKey(timelineName, id, propertyKey) ) as Promise; } function setTimelinePostPropertyValue( timelineName: string, id: number, propertyKey: TimelinePostPropertyKey, value: T ): Promise { return mockStorage.setItem( getTimelinePostPropertyKey(timelineName, id, propertyKey), value ); } function removeTimelinePostProperty( timelineName: string, id: number, propertyKey: TimelinePostPropertyKey ): Promise { return mockStorage.removeItem( getTimelinePostPropertyKey(timelineName, id, propertyKey) ); } async function getTimelinePostInfo( timelineName: string, id: number ): Promise { const currentPostId = await getTimelinePropertyValue( timelineName, "currentPostId" ); if (currentPostId == null || id > currentPostId) { throw new HttpTimelinePostNotExistError(); } const type = await getTimelinePostPropertyValue( timelineName, id, "type" ); if (type == null) { return { id, author: await getUser( await getTimelinePostPropertyValue(timelineName, id, "author") ), time: new Date( await getTimelinePostPropertyValue(timelineName, id, "time") ), lastUpdated: new Date( await getTimelinePostPropertyValue( timelineName, id, "lastUpdated" ) ), deleted: true, }; } else { let content: HttpTimelinePostContent; if (type === "text") { content = { type: "text", text: await getTimelinePostPropertyValue(timelineName, id, "data"), }; } else { content = { type: "image", }; } return { id, author: await getUser( await getTimelinePostPropertyValue(timelineName, id, "author") ), time: new Date( await getTimelinePostPropertyValue(timelineName, id, "time") ), lastUpdated: new Date( await getTimelinePostPropertyValue( timelineName, id, "lastUpdated" ) ), content, deleted: false, }; } } export class MockHttpTimelineClient implements IHttpTimelineClient { async listTimeline( query: HttpTimelineListQuery ): Promise { await mockPrepare("timeline.list"); return ( await Promise.all( (await getTimelineNameList()).map((name) => getTimelineInfo(name)) ) ).filter((timeline) => { if ( query.visibility != null && query.visibility !== timeline.visibility ) { return false; } if (query.relate != null) { if (query.relateType === "own") { if (timeline.owner.username !== query.relate) { return false; } } else if (query.relateType === "join") { if (!timeline.memberUsernames.includes(query.relate)) { return false; } } else if ( timeline.owner.username !== query.relate && !timeline.memberUsernames.includes(query.relate) ) { return false; } } return true; }); } getTimeline(timelineName: string): Promise; getTimeline( timelineName: string, query: { checkUniqueId?: string; } ): Promise; getTimeline( timelineName: string, query: { checkUniqueId?: string; ifModifiedSince: Date; } ): Promise; async getTimeline( timelineName: string, query?: { checkUniqueId?: string; ifModifiedSince?: Date; } ): Promise { await mockPrepare("timeline.get"); try { const timeline = await getTimelineInfo(timelineName); if (query != null && query.ifModifiedSince != null) { if (timeline.lastModified >= query.ifModifiedSince) { return timeline; } else { if ( query.checkUniqueId != null && timeline.uniqueId != query.checkUniqueId ) { return timeline; } else { return new NotModified(); } } } return timeline; } catch (e) { if ( e instanceof MockTimelineNotExistError || e instanceof MockUserNotExistError ) { throw new HttpTimelineNotExistError(); } throw e; } } async postTimeline( req: HttpTimelinePostRequest, token: string ): Promise { await mockPrepare("timeline.post"); const user = checkToken(token); try { await createTimeline(req.name, user); } catch (e) { if (e instanceof MockTimelineAlreadyExistError) { throw new HttpTimelineNameConflictError(); } throw e; } return await getTimelineInfo(req.name); } async patchTimeline( timelineName: string, req: HttpTimelinePatchRequest, _token: string ): Promise { await mockPrepare("timeline.patch"); let modified = false; if (req.description != null) { modified = true; await setTimelinePropertyValue( timelineName, "description", req.description ); } if (req.visibility != null) { modified = true; await setTimelinePropertyValue( timelineName, "visibility", req.visibility ); } if (modified) { await updateTimelineLastModified(timelineName); } return await getTimelineInfo(timelineName); } async deleteTimeline(timelineName: string, _token: string): Promise { await mockPrepare("timeline.delete"); await setTimelineNameList( without(await getTimelineNameList(), timelineName) ); await mockStorage.removeItem( getTimelinePropertyKey(timelineName, "uniqueId") ); // TODO: remove other things } async memberPut( timelineName: string, username: string, _token: string ): Promise { await mockPrepare("timeline.member.put"); const oldMembers = (await getTimelinePropertyValue( timelineName, "members" )) ?? []; if (!oldMembers.includes(username)) { await setTimelinePropertyValue(timelineName, "members", [ ...oldMembers, username, ]); await updateTimelineLastModified(timelineName); } } async memberDelete( timelineName: string, username: string, _token: string ): Promise { await mockPrepare("timeline.member.delete"); const oldMembers = (await getTimelinePropertyValue( timelineName, "members" )) ?? []; if (oldMembers.includes(username)) { await setTimelinePropertyValue( timelineName, "members", without(oldMembers, username) ); await updateTimelineLastModified(timelineName); } } 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; async listPost( timelineName: string, _token?: string, query?: { modifiedSince?: Date; includeDeleted?: boolean; } ): Promise { await mockPrepare("timeline.post.list"); // TODO: Permission check. const currentPostId = await getTimelinePropertyValue( timelineName, "currentPostId" ); return ( await Promise.all( range(1, currentPostId == null ? 1 : currentPostId + 1).map( async (id) => { return await getTimelinePostInfo(timelineName, id); } ) ) ) .filter((post) => { if (query?.includeDeleted !== true && post.deleted) { return false; } return true; }) .filter((post) => { if (query?.modifiedSince != null) { return post.lastUpdated >= query.modifiedSince; } return true; }); } getPostData( timelineName: string, postId: number, token: string ): Promise; async getPostData( timelineName: string, postId: number, _token?: string, etag?: string ): Promise { await mockPrepare("timeline.post.data.get"); // TODO: Permission check. const optionalSavedEtag = await getTimelinePostPropertyValue( timelineName, postId, "etag" ); if (optionalSavedEtag == null) { const optionalType = await getTimelinePostPropertyValue( timelineName, postId, "type" ); if (optionalType != null) { throw new Error("Post of this type has no data."); } else { throw new HttpTimelinePostNotExistError(); } } if (etag === optionalSavedEtag) { return new NotModified(); } return { data: await getTimelinePostPropertyValue( timelineName, postId, "data" ), etag: optionalSavedEtag, }; } async postPost( timelineName: string, req: HttpTimelinePostPostRequest, token: string ): Promise { await mockPrepare("timeline.post.post"); const user = checkToken(token); const savedId = await getTimelinePropertyValue( timelineName, "currentPostId" ); const id = savedId ? savedId + 1 : 1; await setTimelinePropertyValue(timelineName, "currentPostId", id); await setTimelinePostPropertyValue(timelineName, id, "author", user); const currentTimeString = new Date().toISOString(); await setTimelinePostPropertyValue( timelineName, id, "lastUpdated", currentTimeString ); await setTimelinePostPropertyValue( timelineName, id, "time", req.time != null ? req.time.toISOString() : currentTimeString ); const { content } = req; if (content.type === "text") { await setTimelinePostPropertyValue(timelineName, id, "type", "text"); await setTimelinePostPropertyValue( timelineName, id, "data", content.text ); } else { await setTimelinePostPropertyValue(timelineName, id, "type", "image"); await setTimelinePostPropertyValue( timelineName, id, "data", content.data ); await setTimelinePostPropertyValue( timelineName, id, "etag", await sha1(content.data) ); } return (await getTimelinePostInfo( timelineName, id )) as HttpTimelinePostInfo; } async deletePost( timelineName: string, postId: number, _token: string ): Promise { await mockPrepare("timeline.post.delete"); // TODO: permission check await removeTimelinePostProperty(timelineName, postId, "type"); await removeTimelinePostProperty(timelineName, postId, "data"); await removeTimelinePostProperty(timelineName, postId, "etag"); await setTimelinePostPropertyValue( timelineName, postId, "lastUpdated", new Date().toISOString() ); } }