Page MenuHomePhabricator

D7786.id26360.diff
No OneTemporary

D7786.id26360.diff

diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js
--- a/keyserver/src/responders/website-responders.js
+++ b/keyserver/src/responders/website-responders.js
@@ -4,10 +4,12 @@
import { detect as detectBrowser } from 'detect-browser';
import type { $Response, $Request } from 'express';
import fs from 'fs';
+import _isEqual from 'lodash/fp/isEqual.js';
import _keyBy from 'lodash/fp/keyBy.js';
import * as React from 'react';
// eslint-disable-next-line import/extensions
import ReactDOMServer from 'react-dom/server';
+import t from 'tcomb';
import { promisify } from 'util';
import { baseLegalPolicies } from 'lib/facts/policies.js';
@@ -23,17 +25,28 @@
createPendingThread,
} from 'lib/shared/thread-utils.js';
import { defaultWebEnabledApps } from 'lib/types/enabled-apps.js';
+import { entryStoreValidator } from 'lib/types/entry-types.js';
import { defaultCalendarFilters } from 'lib/types/filter-types.js';
-import { defaultNumberPerThread } from 'lib/types/message-types.js';
+import {
+ defaultNumberPerThread,
+ messageStoreValidator,
+} from 'lib/types/message-types.js';
import { defaultEnabledReports } from 'lib/types/report-types.js';
import { defaultConnectionInfo } from 'lib/types/socket-types.js';
import { threadPermissions } from 'lib/types/thread-permission-types.js';
import { threadTypes } from 'lib/types/thread-types-enum.js';
+import { threadStoreValidator } from 'lib/types/thread-types.js';
+import {
+ currentUserInfoValidator,
+ userInfosValidator,
+} from 'lib/types/user-types.js';
import { currentDateInTimeZone } from 'lib/utils/date-utils.js';
import { ServerError } from 'lib/utils/errors.js';
import { promiseAll } from 'lib/utils/promises.js';
import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js';
+import { tBool, tNumber, tShape, tString } from 'lib/utils/validation-utils.js';
import getTitle from 'web/title/getTitle.js';
+import { navInfoValidator } from 'web/types/nav-types.js';
import { navInfoFromURL } from 'web/url-utils.js';
import { fetchEntryInfos } from '../fetchers/entry-fetchers.js';
@@ -49,6 +62,7 @@
import { Viewer } from '../session/viewer.js';
import { streamJSON, waitForStream } from '../utils/json-stream.js';
import { getAppURLFactsFromRequestURL } from '../utils/urls.js';
+import { validateOutput } from '../utils/validation-utils.js';
const { renderToNodeStream } = ReactDOMServer;
@@ -141,6 +155,86 @@
}
}
+const initialReduxStateValidator = tShape({
+ navInfo: navInfoValidator,
+ deviceID: t.Nil,
+ currentUserInfo: currentUserInfoValidator,
+ draftStore: t.irreducible('default draftStore', _isEqual({ drafts: {} })),
+ sessionID: t.maybe(t.String),
+ entryStore: entryStoreValidator,
+ threadStore: threadStoreValidator,
+ userStore: tShape({
+ userInfos: userInfosValidator,
+ inconsistencyReports: t.irreducible(
+ 'default inconsistencyReports',
+ _isEqual([]),
+ ),
+ }),
+ messageStore: messageStoreValidator,
+ updatesCurrentAsOf: t.Number,
+ loadingStatuses: t.irreducible('default loadingStatuses', _isEqual({})),
+ calendarFilters: t.irreducible(
+ 'defaultCalendarFilters',
+ _isEqual(defaultCalendarFilters),
+ ),
+ urlPrefix: tString(''),
+ windowDimensions: t.irreducible(
+ 'default windowDimensions',
+ _isEqual({ width: 0, height: 0 }),
+ ),
+ baseHref: t.String,
+ notifPermissionAlertInfo: t.irreducible(
+ 'default notifPermissionAlertInfo',
+ _isEqual(defaultNotifPermissionAlertInfo),
+ ),
+ connection: tShape({
+ status: tString('connecting'),
+ queuedActivityUpdates: t.irreducible(
+ 'default queuedActivityUpdates',
+ _isEqual([]),
+ ),
+ actualizedCalendarQuery: tShape({
+ startDate: t.String,
+ endDate: t.String,
+ filters: t.irreducible(
+ 'default filters',
+ _isEqual(defaultCalendarFilters),
+ ),
+ }),
+ lateResponses: t.irreducible('default lateResponses', _isEqual([])),
+ showDisconnectedBar: tBool(false),
+ }),
+ watchedThreadIDs: t.irreducible('default watchedThreadIDs', _isEqual([])),
+ lifecycleState: tString('active'),
+ enabledApps: t.irreducible(
+ 'defaultWebEnabledApps',
+ _isEqual(defaultWebEnabledApps),
+ ),
+ reportStore: t.irreducible(
+ 'default reportStore',
+ _isEqual({
+ enabledReports: defaultEnabledReports,
+ queuedReports: [],
+ }),
+ ),
+ nextLocalID: tNumber(0),
+ cookie: t.Nil,
+ deviceToken: t.Nil,
+ dataLoaded: t.Boolean,
+ windowActive: tBool(true),
+ userPolicies: t.irreducible('default userPolicies', _isEqual({})),
+ cryptoStore: t.irreducible(
+ 'default cryptoStore',
+ _isEqual({
+ primaryIdentityKeys: null,
+ notificationIdentityKeys: null,
+ }),
+ ),
+ pushApiPublicKey: t.maybe(t.String),
+ _persist: t.Nil,
+ commServicesAccessToken: t.Nil,
+});
+
async function websiteResponder(
viewer: Viewer,
req: $Request,
@@ -432,7 +526,12 @@
_persist: null,
commServicesAccessToken: null,
});
- const jsonStream = streamJSON(res, initialReduxState);
+ const validatedInitialReduxState = validateOutput(
+ viewer,
+ initialReduxStateValidator,
+ initialReduxState,
+ );
+ const jsonStream = streamJSON(res, validatedInitialReduxState);
await waitForStream(jsonStream);
res.end(html`
diff --git a/lib/types/entry-types.js b/lib/types/entry-types.js
--- a/lib/types/entry-types.js
+++ b/lib/types/entry-types.js
@@ -61,6 +61,11 @@
+daysToEntries: { +[day: string]: string[] },
+lastUserInteractionCalendar: number,
};
+export const entryStoreValidator: TInterface<EntryStore> = tShape<EntryStore>({
+ entryInfos: t.dict(tID, rawEntryInfoValidator),
+ daysToEntries: t.dict(t.String, t.list(tID)),
+ lastUserInteractionCalendar: t.Number,
+});
export type CalendarQuery = {
+startDate: string,
diff --git a/lib/types/message-types.js b/lib/types/message-types.js
--- a/lib/types/message-types.js
+++ b/lib/types/message-types.js
@@ -390,6 +390,13 @@
lastNavigatedTo: number, // millisecond timestamp
lastPruned: number, // millisecond timestamp
};
+const threadMessageInfoValidator: TInterface<ThreadMessageInfo> =
+ tShape<ThreadMessageInfo>({
+ messageIDs: t.list(tID),
+ startReached: t.Boolean,
+ lastNavigatedTo: t.Number,
+ lastPruned: t.Number,
+ });
// Tracks client-local information about a message that hasn't been assigned an
// ID by the server yet. As soon as the client gets an ack from the server for
@@ -397,10 +404,18 @@
export type LocalMessageInfo = {
+sendFailed?: boolean,
};
+const localMessageInfoValidator: TInterface<LocalMessageInfo> =
+ tShape<LocalMessageInfo>({
+ sendFailed: t.maybe(t.Boolean),
+ });
export type MessageStoreThreads = {
+[threadID: string]: ThreadMessageInfo,
};
+const messageStoreThreadsValidator: TDict<MessageStoreThreads> = t.dict(
+ tID,
+ threadMessageInfoValidator,
+);
export type MessageStore = {
+messages: { +[id: string]: RawMessageInfo },
@@ -408,6 +423,13 @@
+local: { +[id: string]: LocalMessageInfo },
+currentAsOf: number,
};
+export const messageStoreValidator: TInterface<MessageStore> =
+ tShape<MessageStore>({
+ messages: t.dict(tID, rawMessageInfoValidator),
+ threads: messageStoreThreadsValidator,
+ local: t.dict(t.String, localMessageInfoValidator),
+ currentAsOf: t.Number,
+ });
// MessageStore messages ops
export type RemoveMessageOperation = {
diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js
--- a/lib/types/thread-types.js
+++ b/lib/types/thread-types.js
@@ -28,7 +28,10 @@
import { type ThreadType, threadTypeValidator } from './thread-types-enum.js';
import type { ClientUpdateInfo, ServerUpdateInfo } from './update-types.js';
import type { UserInfo, UserInfos } from './user-types.js';
-import { type ThreadEntity } from '../utils/entity-text.js';
+import {
+ type ThreadEntity,
+ threadEntityValidator,
+} from '../utils/entity-text.js';
import { tID, tShape } from '../utils/validation-utils.js';
export type MemberInfo = {
@@ -49,6 +52,11 @@
+username: ?string,
+isViewer: boolean,
};
+const relativeMemberInfoValidator = tShape<RelativeMemberInfo>({
+ ...memberInfoValidator.meta.props,
+ username: t.maybe(t.String),
+ isViewer: t.Boolean,
+});
export type RoleInfo = {
+id: string,
@@ -133,6 +141,25 @@
+repliesCount: number,
+pinnedCount?: number,
};
+export const threadInfoValidator: TInterface<ThreadInfo> = tShape<ThreadInfo>({
+ id: tID,
+ type: threadTypeValidator,
+ name: t.maybe(t.String),
+ uiName: t.union([t.String, threadEntityValidator]),
+ avatar: t.maybe(clientAvatarValidator),
+ description: t.maybe(t.String),
+ color: t.String,
+ creationTime: t.Number,
+ parentThreadID: t.maybe(tID),
+ containingThreadID: t.maybe(tID),
+ community: t.maybe(tID),
+ members: t.list(relativeMemberInfoValidator),
+ roles: t.dict(tID, roleInfoValidator),
+ currentUser: threadCurrentUserInfoValidator,
+ sourceMessageID: t.maybe(tID),
+ repliesCount: t.Number,
+ pinnedCount: t.maybe(t.Number),
+});
export type ResolvedThreadInfo = {
+id: string,
@@ -185,6 +212,10 @@
export type ThreadStore = {
+threadInfos: { +[id: string]: RawThreadInfo },
};
+export const threadStoreValidator: TInterface<ThreadStore> =
+ tShape<ThreadStore>({
+ threadInfos: t.dict(tID, rawThreadInfoValidator),
+ });
export type RemoveThreadOperation = {
+type: 'remove',
diff --git a/lib/utils/entity-text.js b/lib/utils/entity-text.js
--- a/lib/utils/entity-text.js
+++ b/lib/utils/entity-text.js
@@ -2,12 +2,18 @@
import invariant from 'invariant';
import * as React from 'react';
+import t, { type TInterface, type TUnion } from 'tcomb';
import type { GetENSNames } from './ens-helpers.js';
+import { tID, tShape, tString } from './validation-utils.js';
import { useENSNames } from '../hooks/ens-cache.js';
import { threadNoun } from '../shared/thread-utils.js';
import { stringForUser } from '../shared/user-utils.js';
-import { type ThreadType, threadTypes } from '../types/thread-types-enum.js';
+import {
+ type ThreadType,
+ threadTypes,
+ threadTypeValidator,
+} from '../types/thread-types-enum.js';
import { type RawThreadInfo, type ThreadInfo } from '../types/thread-types.js';
import { basePluralize } from '../utils/text-utils.js';
@@ -18,6 +24,13 @@
+isViewer?: ?boolean,
+possessive?: ?boolean, // eg. `user's` instead of `user`
};
+export const userEntityValidator: TInterface<UserEntity> = tShape<UserEntity>({
+ type: tString('user'),
+ id: t.String,
+ username: t.maybe(t.String),
+ isViewer: t.maybe(t.Boolean),
+ possessive: t.maybe(t.Boolean),
+});
// Comments explain how thread name will appear from user4's perspective
export type ThreadEntity =
@@ -48,6 +61,28 @@
+possessive?: ?boolean, // eg. `this thread's` instead of `this thread`
};
+export const threadEntityValidator: TUnion<ThreadEntity> = t.union([
+ tShape({
+ type: tString('thread'),
+ id: tID,
+ name: t.maybe(t.String),
+ display: tString('uiName'),
+ uiName: t.union([t.list(userEntityValidator), t.String]),
+ ifJustViewer: t.maybe(t.enums.of(['just_you_string', 'viewer_username'])),
+ }),
+ tShape({
+ type: tString('thread'),
+ id: tID,
+ name: t.maybe(t.String),
+ display: tString('shortName'),
+ threadType: t.maybe(threadTypeValidator),
+ parentThreadID: t.maybe(tID),
+ alwaysDisplayShortName: t.maybe(t.Boolean),
+ subchannel: t.maybe(t.Boolean),
+ possessive: t.maybe(t.Boolean),
+ }),
+]);
+
type ColorEntity = {
+type: 'color',
+hex: string,
diff --git a/web/types/nav-types.js b/web/types/nav-types.js
--- a/web/types/nav-types.js
+++ b/web/types/nav-types.js
@@ -1,13 +1,25 @@
// @flow
+import t from 'tcomb';
+import type { TInterface } from 'tcomb';
-import type { BaseNavInfo } from 'lib/types/nav-types.js';
-import type { ThreadInfo } from 'lib/types/thread-types.js';
+import { type BaseNavInfo } from 'lib/types/nav-types.js';
+import {
+ type ThreadInfo,
+ threadInfoValidator,
+} from 'lib/types/thread-types.js';
+import { tID, tShape } from 'lib/utils/validation-utils.js';
export type NavigationTab = 'calendar' | 'chat' | 'settings';
+const navigationTabValidator = t.enums.of(['calendar', 'chat', 'settings']);
export type NavigationSettingsSection = 'account' | 'danger-zone';
+const navigationSettingsSectionValidator = t.enums.of([
+ 'account',
+ 'danger-zone',
+]);
export type NavigationChatMode = 'view' | 'create';
+const navigationChatModeValidator = t.enums.of(['view', 'create']);
export type NavInfo = {
...$Exact<BaseNavInfo>,
@@ -18,3 +30,14 @@
+selectedUserList?: $ReadOnlyArray<string>,
+chatMode?: NavigationChatMode,
};
+
+export const navInfoValidator: TInterface<NavInfo> = tShape<$Exact<NavInfo>>({
+ startDate: t.String,
+ endDate: t.String,
+ tab: navigationTabValidator,
+ activeChatThreadID: t.maybe(tID),
+ pendingThread: t.maybe(threadInfoValidator),
+ settingsSection: t.maybe(navigationSettingsSectionValidator),
+ selectedUserList: t.maybe(t.list(t.String)),
+ chatMode: t.maybe(navigationChatModeValidator),
+});

File Metadata

Mime Type
text/plain
Expires
Wed, Dec 25, 9:11 PM (12 h, 42 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2702020
Default Alt Text
D7786.id26360.diff (12 KB)

Event Timeline