diff --git a/lib/types/relationship-types.js b/lib/types/relationship-types.js index 94929c729..a356a503f 100644 --- a/lib/types/relationship-types.js +++ b/lib/types/relationship-types.js @@ -1,50 +1,60 @@ // @flow import { values } from '../utils/objects'; export const undirectedStatus = Object.freeze({ KNOW_OF: 0, FRIEND: 2, }); export type UndirectedStatus = $Values; export const directedStatus = Object.freeze({ PENDING_FRIEND: 1, BLOCKED: 3, }); export type DirectedStatus = $Values; +export const userRelationshipStatus = Object.freeze({ + REQUEST_SENT: 1, + REQUEST_RECEIVED: 2, + FRIEND: 3, + BLOCKED_BY_VIEWER: 4, + BLOCKED_VIEWER: 5, + BOTH_BLOCKED: 6, +}); +export type UserRelationshipStatus = $Values; + export const relationshipActions = Object.freeze({ FRIEND: 'friend', UNFRIEND: 'unfriend', BLOCK: 'block', UNBLOCK: 'unblock', }); type RelationshipAction = $Values; export const relationshipActionsList: $ReadOnlyArray = values( relationshipActions, ); export type RelationshipRequest = {| action: RelationshipAction, userIDs: $ReadOnlyArray, |}; type SharedRelationshipRow = {| user1: string, user2: string, |}; export type DirectedRelationshipRow = {| ...SharedRelationshipRow, status: DirectedStatus, |}; export type UndirectedRelationshipRow = {| ...SharedRelationshipRow, status: UndirectedStatus, |}; export type RelationshipErrors = $Shape<{| invalid_user: string[], already_friends: string[], user_blocked: string[], |}>; diff --git a/lib/types/user-types.js b/lib/types/user-types.js index f6eb40fc3..d90677734 100644 --- a/lib/types/user-types.js +++ b/lib/types/user-types.js @@ -1,90 +1,95 @@ // @flow import type { UserInconsistencyReportCreationRequest } from './report-types'; +import type { UserRelationshipStatus } from './relationship-types'; import PropTypes from 'prop-types'; export type UserInfo = {| id: string, username: ?string, + relationshipStatus?: UserRelationshipStatus, |}; export type UserInfos = { [id: string]: UserInfo }; export const userInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, username: PropTypes.string, + relationshipStatus: PropTypes.number, }); export type AccountUserInfo = {| id: string, username: string, + relationshipStatus?: UserRelationshipStatus, |} & UserInfo; export const accountUserInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, username: PropTypes.string.isRequired, + relationshipStatus: PropTypes.number, }); export type UserStore = {| userInfos: { [id: string]: UserInfo }, inconsistencyReports: $ReadOnlyArray, |}; export type RelativeUserInfo = {| id: string, username: ?string, isViewer: boolean, |}; export const relativeUserInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, username: PropTypes.string, isViewer: PropTypes.bool.isRequired, }); export type LoggedInUserInfo = {| id: string, username: string, email: string, emailVerified: boolean, |}; export type LoggedOutUserInfo = {| id: string, anonymous: true, |}; export type CurrentUserInfo = LoggedInUserInfo | LoggedOutUserInfo; export const currentUserPropType = PropTypes.oneOfType([ PropTypes.shape({ id: PropTypes.string.isRequired, username: PropTypes.string.isRequired, email: PropTypes.string.isRequired, emailVerified: PropTypes.bool.isRequired, }), PropTypes.shape({ id: PropTypes.string.isRequired, anonymous: PropTypes.oneOf([true]).isRequired, }), ]); export type AccountUpdate = {| updatedFields: {| email?: ?string, password?: ?string, |}, currentPassword: string, |}; export type UserListItem = {| id: string, username: string, memberOfParentThread: boolean, |}; export const userListItemPropType = PropTypes.shape({ id: PropTypes.string.isRequired, username: PropTypes.string.isRequired, memberOfParentThread: PropTypes.bool.isRequired, }); diff --git a/server/src/fetchers/user-fetchers.js b/server/src/fetchers/user-fetchers.js index 4d721d03a..fe92f0c51 100644 --- a/server/src/fetchers/user-fetchers.js +++ b/server/src/fetchers/user-fetchers.js @@ -1,161 +1,226 @@ // @flow import type { UserInfos, CurrentUserInfo, LoggedInUserInfo, } from 'lib/types/user-types'; +import { + undirectedStatus, + directedStatus, + userRelationshipStatus, +} from 'lib/types/relationship-types'; import type { Viewer } from '../session/viewer'; import { ServerError } from 'lib/utils/errors'; import { dbQuery, SQL } from '../database'; async function fetchUserInfos(userIDs: string[]): Promise { if (userIDs.length <= 0) { return {}; } const query = SQL` SELECT id, username FROM users WHERE id IN (${userIDs}) `; const [result] = await dbQuery(query); const userInfos = {}; for (let row of result) { const id = row.id.toString(); userInfos[id] = { id, username: row.username, }; } for (let userID of userIDs) { if (!userInfos[userID]) { userInfos[userID] = { id: userID, username: null, }; } } return userInfos; } async function fetchKnownUserInfos( viewer: Viewer, userIDs?: $ReadOnlyArray, ): Promise { if (!viewer.loggedIn) { return {}; } if (userIDs && userIDs.length === 0) { return {}; } const query = SQL` - SELECT DISTINCT u.id, u.username FROM relationships_undirected r - LEFT JOIN users u ON r.user1 = u.id OR r.user2 = u.id + SELECT ru.user1, ru.user2, ru.status AS undirected_status, + rd1.status AS user1_directed_status, rd2.status AS user2_directed_status, + u.username + FROM relationships_undirected ru + LEFT JOIN relationships_directed rd1 + ON rd1.user1 = ru.user1 AND rd1.user2 = ru.user2 + LEFT JOIN relationships_directed rd2 + ON rd2.user1 = ru.user2 AND rd2.user2 = ru.user1 + LEFT JOIN users u + ON u.id != ${viewer.userID} AND (u.id = ru.user1 OR u.id = ru.user2) `; if (userIDs) { query.append(SQL` - WHERE (r.user1 = ${viewer.userID} AND r.user2 IN (${userIDs})) OR - (r.user1 IN (${userIDs}) AND r.user2 = ${viewer.userID}) + WHERE (ru.user1 = ${viewer.userID} AND ru.user2 IN (${userIDs})) OR + (ru.user1 IN (${userIDs}) AND ru.user2 = ${viewer.userID}) `); } else { query.append(SQL` - WHERE r.user1 = ${viewer.userID} OR r.user2 = ${viewer.userID} + WHERE ru.user1 = ${viewer.userID} OR ru.user2 = ${viewer.userID} `); } + query.append(SQL` + UNION SELECT id AS user1, NULL AS user2, NULL AS undirected_status, + NULL AS user1_directed_status, NULL AS user2_directed_status, + username + FROM users + WHERE id = ${viewer.userID} + `); const [result] = await dbQuery(query); const userInfos = {}; - for (let row of result) { - const id = row.id.toString(); - userInfos[id] = { + for (const row of result) { + const user1 = row.user1.toString(); + const user2 = row.user2 ? row.user2.toString() : null; + const id = user1 === viewer.userID && user2 ? user2 : user1; + const userInfo = { id, username: row.username, }; + + if (!user2) { + userInfos[id] = userInfo; + continue; + } + + let viewerDirectedStatus; + let targetDirectedStatus; + if (user1 === viewer.userID) { + viewerDirectedStatus = row.user1_directed_status; + targetDirectedStatus = row.user2_directed_status; + } else { + viewerDirectedStatus = row.user2_directed_status; + targetDirectedStatus = row.user1_directed_status; + } + + const viewerBlockedTarget = viewerDirectedStatus === directedStatus.BLOCKED; + const targetBlockedViewer = targetDirectedStatus === directedStatus.BLOCKED; + const friendshipExists = row.undirected_status === undirectedStatus.FRIEND; + const viewerRequestedTargetFriendship = + viewerDirectedStatus === directedStatus.PENDING_FRIEND; + const targetRequestedViewerFriendship = + targetDirectedStatus === directedStatus.PENDING_FRIEND; + + let relationshipStatus; + if (viewerBlockedTarget && targetBlockedViewer) { + relationshipStatus = userRelationshipStatus.BOTH_BLOCKED; + } else if (targetBlockedViewer) { + relationshipStatus = userRelationshipStatus.BLOCKED_VIEWER; + } else if (viewerBlockedTarget) { + relationshipStatus = userRelationshipStatus.BLOCKED_BY_VIEWER; + } else if (friendshipExists) { + relationshipStatus = userRelationshipStatus.FRIEND; + } else if (targetRequestedViewerFriendship) { + relationshipStatus = userRelationshipStatus.REQUEST_RECEIVED; + } else if (viewerRequestedTargetFriendship) { + relationshipStatus = userRelationshipStatus.REQUEST_SENT; + } + + userInfos[id] = userInfo; + if (relationshipStatus) { + userInfos[id].relationshipStatus = relationshipStatus; + } } return userInfos; } async function verifyUserIDs( userIDs: $ReadOnlyArray, ): Promise { if (userIDs.length === 0) { return []; } const query = SQL`SELECT id FROM users WHERE id IN (${userIDs})`; const [result] = await dbQuery(query); return result.map(row => row.id.toString()); } async function verifyUserOrCookieIDs( ids: $ReadOnlyArray, ): Promise { if (ids.length === 0) { return []; } const query = SQL` SELECT id FROM users WHERE id IN (${ids}) UNION SELECT id FROM cookies WHERE id IN (${ids}) `; const [result] = await dbQuery(query); return result.map(row => row.id.toString()); } async function fetchCurrentUserInfo(viewer: Viewer): Promise { if (!viewer.loggedIn) { return { id: viewer.cookieID, anonymous: true }; } const currentUserInfos = await fetchLoggedInUserInfos([viewer.userID]); if (currentUserInfos.length === 0) { throw new ServerError('unknown_error'); } return currentUserInfos[0]; } async function fetchLoggedInUserInfos( userIDs: $ReadOnlyArray, ): Promise { const query = SQL` SELECT id, username, email, email_verified FROM users WHERE id IN (${userIDs}) `; const [result] = await dbQuery(query); return result.map(row => ({ id: row.id.toString(), username: row.username, email: row.email, emailVerified: !!row.email_verified, })); } async function fetchAllUserIDs(): Promise { const query = SQL`SELECT id FROM users`; const [result] = await dbQuery(query); return result.map(row => row.id.toString()); } async function fetchUsername(id: string): Promise { const query = SQL`SELECT username FROM users WHERE id = ${id}`; const [result] = await dbQuery(query); if (result.length === 0) { return null; } const row = result[0]; return row.username; } export { fetchUserInfos, verifyUserIDs, verifyUserOrCookieIDs, fetchCurrentUserInfo, fetchLoggedInUserInfos, fetchAllUserIDs, fetchUsername, fetchKnownUserInfos, };