diff --git a/keyserver/src/deleters/farcaster-channel-tag-deleters.js b/keyserver/src/deleters/farcaster-channel-tag-deleters.js index 7834ec703..2c93a0934 100644 --- a/keyserver/src/deleters/farcaster-channel-tag-deleters.js +++ b/keyserver/src/deleters/farcaster-channel-tag-deleters.js @@ -1,66 +1,103 @@ // @flow import { farcasterChannelTagBlobHash } from 'lib/shared/community-utils.js'; -import type { DeleteFarcasterChannelTagRequest } from 'lib/types/community-types.js'; +import { + NEXT_CODE_VERSION, + hasMinCodeVersion, +} from 'lib/shared/version-utils.js'; +import type { + DeleteFarcasterChannelTagRequest, + DeleteFarcasterChannelTagResponse, +} from 'lib/types/community-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { dbQuery, SQL } from '../database/database.js'; +import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js'; import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js'; import { deleteBlob } from '../services/blob.js'; import type { Viewer } from '../session/viewer'; +import { updateThread } from '../updaters/thread-updaters.js'; async function deleteFarcasterChannelTag( viewer: Viewer, request: DeleteFarcasterChannelTagRequest, -): Promise { +): Promise { const hasPermission = await checkThreadPermission( viewer, request.commCommunityID, threadPermissions.MANAGE_FARCASTER_CHANNEL_TAGS, ); if (!hasPermission) { throw new ServerError('invalid_credentials'); } const query = SQL` START TRANSACTION; SELECT blob_holder INTO @currentBlobHolder FROM communities WHERE id = ${request.commCommunityID} AND farcaster_channel_id = ${request.farcasterChannelID} FOR UPDATE; UPDATE communities SET farcaster_channel_id = NULL, blob_holder = NULL WHERE id = ${request.commCommunityID} AND farcaster_channel_id = ${request.farcasterChannelID}; - + COMMIT; SELECT @currentBlobHolder AS blobHolder; `; const [transactionResult] = await dbQuery(query, { multipleStatements: true, }); const selectResult = transactionResult.pop(); const [row] = selectResult; if (row?.blobHolder) { await deleteBlob( { hash: farcasterChannelTagBlobHash(request.farcasterChannelID), holder: row.blobHolder, }, true, ); } + + const serverThreadInfos = await fetchServerThreadInfos({ + threadID: request.commCommunityID, + }); + const threadInfo = serverThreadInfos.threadInfos[request.commCommunityID]; + if (!threadInfo) { + return null; + } + const { avatar } = threadInfo; + if (avatar?.type !== 'farcaster') { + return null; + } + + const changeThreadSettingsResult = await updateThread(viewer, { + threadID: request.commCommunityID, + changes: { avatar: { type: 'remove' } }, + }); + + if ( + !hasMinCodeVersion(viewer.platformDetails, { + native: NEXT_CODE_VERSION, + web: NEXT_CODE_VERSION, + }) + ) { + return null; + } + + return changeThreadSettingsResult; } export { deleteFarcasterChannelTag }; diff --git a/keyserver/src/responders/farcaster-channel-tag-responders.js b/keyserver/src/responders/farcaster-channel-tag-responders.js index 09970e381..c0cc69124 100644 --- a/keyserver/src/responders/farcaster-channel-tag-responders.js +++ b/keyserver/src/responders/farcaster-channel-tag-responders.js @@ -1,47 +1,48 @@ // @flow import t, { type TInterface } from 'tcomb'; import type { CreateOrUpdateFarcasterChannelTagRequest, CreateOrUpdateFarcasterChannelTagResponse, DeleteFarcasterChannelTagRequest, + DeleteFarcasterChannelTagResponse, } from 'lib/types/community-types'; import { tShape, tID } from 'lib/utils/validation-utils.js'; import { createOrUpdateFarcasterChannelTag } from '../creators/farcaster-channel-tag-creator.js'; import { deleteFarcasterChannelTag } from '../deleters/farcaster-channel-tag-deleters.js'; import type { Viewer } from '../session/viewer'; const createOrUpdateFarcasterChannelTagInputValidator: TInterface = tShape({ commCommunityID: tID, farcasterChannelID: t.String, }); -async function createOrUpdateFarcasterChannelTagResponder( +function createOrUpdateFarcasterChannelTagResponder( viewer: Viewer, request: CreateOrUpdateFarcasterChannelTagRequest, ): Promise { - return await createOrUpdateFarcasterChannelTag(viewer, request); + return createOrUpdateFarcasterChannelTag(viewer, request); } const deleteFarcasterChannelTagInputValidator: TInterface = tShape({ commCommunityID: tID, farcasterChannelID: t.String, }); -async function deleteFarcasterChannelTagResponder( +function deleteFarcasterChannelTagResponder( viewer: Viewer, request: DeleteFarcasterChannelTagRequest, -): Promise { - await deleteFarcasterChannelTag(viewer, request); +): Promise { + return deleteFarcasterChannelTag(viewer, request); } export { createOrUpdateFarcasterChannelTagResponder, createOrUpdateFarcasterChannelTagInputValidator, deleteFarcasterChannelTagResponder, deleteFarcasterChannelTagInputValidator, }; diff --git a/lib/actions/community-actions.js b/lib/actions/community-actions.js index 7ab32d555..ec19de1e3 100644 --- a/lib/actions/community-actions.js +++ b/lib/actions/community-actions.js @@ -1,160 +1,166 @@ // @flow import type { CallSingleKeyserverEndpoint } from '../keyserver-conn/call-single-keyserver-endpoint.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { useKeyserverCall } from '../keyserver-conn/keyserver-call.js'; import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import type { ServerCommunityInfo, FetchCommunityInfosResponse, FetchAllCommunityInfosWithNamesResponse, CreateOrUpdateFarcasterChannelTagRequest, CreateOrUpdateFarcasterChannelTagResponse, DeleteFarcasterChannelTagRequest, DeleteFarcasterChannelTagPayload, } from '../types/community-types.js'; const updateCalendarCommunityFilter = 'UPDATE_CALENDAR_COMMUNITY_FILTER'; const clearCalendarCommunityFilter = 'CLEAR_CALENDAR_COMMUNITY_FILTER'; const updateChatCommunityFilter = 'UPDATE_CHAT_COMMUNITY_FILTER'; const clearChatCommunityFilter = 'CLEAR_CHAT_COMMUNITY_FILTER'; const addCommunityActionType = 'ADD_COMMUNITY'; const fetchCommunityInfosActionTypes = Object.freeze({ started: 'FETCH_COMMUNITY_INFOS_STARTED', success: 'FETCH_COMMUNITY_INFOS_SUCCESS', failed: 'FETCH_COMMUNITY_INFOS_FAILED', }); const fetchCommunityInfos = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): (() => Promise) => async () => { const requests: { [string]: void } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = undefined; } const responses = await callKeyserverEndpoint( 'fetch_community_infos', requests, ); let communityInfos: $ReadOnlyArray = []; for (const keyserverID in responses) { communityInfos = communityInfos.concat( responses[keyserverID].communityInfos, ); } return { communityInfos, }; }; function useFetchCommunityInfos(): () => Promise { return useKeyserverCall(fetchCommunityInfos); } const fetchAllCommunityInfosWithNamesActionTypes = Object.freeze({ started: 'FETCH_ALL_COMMUNITY_INFOS_WITH_NAMES_STARTED', success: 'FETCH_ALL_COMMUNITY_INFOS_WITH_NAMES_SUCCESS', failed: 'FETCH_ALL_COMMUNITY_INFOS_WITH_NAMES_FAILED', }); const fetchAllCommunityInfosWithNames = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (() => Promise) => () => callSingleKeyserverEndpoint('fetch_all_community_infos_with_names'); const createOrUpdateFarcasterChannelTagActionTypes = Object.freeze({ started: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_STARTED', success: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_SUCCESS', failed: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_FAILED', }); const createOrUpdateFarcasterChannelTag = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: CreateOrUpdateFarcasterChannelTagRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.commCommunityID); const requests = { [keyserverID]: { commCommunityID: input.commCommunityID, farcasterChannelID: input.farcasterChannelID, }, }; const responses = await callKeyserverEndpoint( 'create_or_update_farcaster_channel_tag', requests, ); return responses[keyserverID]; }; function useCreateOrUpdateFarcasterChannelTag(): ( input: CreateOrUpdateFarcasterChannelTagRequest, ) => Promise { return useKeyserverCall(createOrUpdateFarcasterChannelTag); } const deleteFarcasterChannelTagActionTypes = Object.freeze({ started: 'DELETE_FARCASTER_CHANNEL_TAG_STARTED', success: 'DELETE_FARCASTER_CHANNEL_TAG_SUCCESS', failed: 'DELETE_FARCASTER_CHANNEL_TAG_FAILED', }); const deleteFarcasterChannelTag = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: DeleteFarcasterChannelTagRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.commCommunityID); const requests = { [keyserverID]: { commCommunityID: input.commCommunityID, farcasterChannelID: input.farcasterChannelID, }, }; - await callKeyserverEndpoint('delete_farcaster_channel_tag', requests); + const responses = await callKeyserverEndpoint( + 'delete_farcaster_channel_tag', + requests, + ); + + const response = responses[keyserverID]; return { commCommunityID: input.commCommunityID, + ...response, }; }; function useDeleteFarcasterChannelTag(): ( input: DeleteFarcasterChannelTagRequest, ) => Promise { return useKeyserverCall(deleteFarcasterChannelTag); } export { updateCalendarCommunityFilter, clearCalendarCommunityFilter, updateChatCommunityFilter, clearChatCommunityFilter, addCommunityActionType, fetchCommunityInfosActionTypes, useFetchCommunityInfos, fetchAllCommunityInfosWithNamesActionTypes, fetchAllCommunityInfosWithNames, createOrUpdateFarcasterChannelTagActionTypes, useCreateOrUpdateFarcasterChannelTag, deleteFarcasterChannelTagActionTypes, useDeleteFarcasterChannelTag, }; diff --git a/lib/types/community-types.js b/lib/types/community-types.js index 9ba85fca7..7b0d94f1b 100644 --- a/lib/types/community-types.js +++ b/lib/types/community-types.js @@ -1,91 +1,98 @@ // @flow import t, { type TInterface } from 'tcomb'; import type { RawMessageInfo } from './message-types.js'; +import type { ChangeThreadSettingsResult } from './thread-types.js'; import type { ServerUpdateInfo } from './update-types.js'; import { tID, tShape } from '../utils/validation-utils.js'; export type CommunityInfo = { +farcasterChannelID: ?string, }; export type CommunityInfos = { +[threadID: string]: CommunityInfo }; export type CommunityStore = { +communityInfos: CommunityInfos, }; export type AddCommunityPayload = { +id: string, +newCommunityInfo: CommunityInfo, }; export type ServerCommunityInfo = { +id: string, +farcasterChannelID: ?string, }; export const serverCommunityInfoValidator: TInterface = tShape({ id: tID, farcasterChannelID: t.maybe(t.String), }); export type ServerCommunityInfoWithCommunityName = $ReadOnly<{ ...ServerCommunityInfo, +communityName: string, }>; export const serverCommunityInfoWithCommunityNameValidator: TInterface = tShape({ id: tID, farcasterChannelID: t.maybe(t.String), communityName: t.String, }); export type FetchCommunityInfosResponse = { +communityInfos: $ReadOnlyArray, }; export type FetchAllCommunityInfosWithNamesResponse = { +allCommunityInfosWithNames: $ReadOnlyArray, }; export type CreateOrUpdateFarcasterChannelTagRequest = { +commCommunityID: string, +farcasterChannelID: string, }; export type CreateOrUpdateFarcasterChannelTagResponse = { +commCommunityID: string, +farcasterChannelID: string, +updatesResult?: ?{ +newUpdates: $ReadOnlyArray, }, +newMessageInfos?: ?$ReadOnlyArray, }; export type DeleteFarcasterChannelTagRequest = { +commCommunityID: string, +farcasterChannelID: string, }; +export type DeleteFarcasterChannelTagResponse = ChangeThreadSettingsResult; + export type DeleteFarcasterChannelTagPayload = { +commCommunityID: string, + +updatesResult?: ?{ + +newUpdates: $ReadOnlyArray, + }, + +newMessageInfos?: ?$ReadOnlyArray, }; export type OngoingJoinCommunityData = { +resolve: () => mixed, +reject: () => mixed, +communityID: string, +threadID: ?string, }; export type JoinCommunityStep = | 'inactive' | 'add_keyserver' | 'auth_to_keyserver' | 'join_community' | 'join_thread' | 'finished'; diff --git a/lib/types/validators/endpoint-validators.js b/lib/types/validators/endpoint-validators.js index 50e205dad..eb1c1d5a8 100644 --- a/lib/types/validators/endpoint-validators.js +++ b/lib/types/validators/endpoint-validators.js @@ -1,197 +1,202 @@ // @flow import t from 'tcomb'; import { fetchCommunityInfosResponseValidator, fetchAllCommunityInfosWithNamesResponseValidator, } from './community-validators.js'; import { saveEntryResponseValidator, deltaEntryInfosResultValidator, fetchEntryInfosResponseValidator, fetchEntryRevisionInfosResultValidator, deleteEntryResponseValidator, restoreEntryResponseValidator, } from './entry-validators.js'; -import { createOrUpdateFarcasterChannelTagResponseValidator } from './farcaster-channel-tag-validators.js'; +import { + createOrUpdateFarcasterChannelTagResponseValidator, + deleteFarcasterChannelTagResponseValidator, +} from './farcaster-channel-tag-validators.js'; import { fetchInviteLinksResponseValidator, inviteLinkVerificationResponseValidator, } from './link-validators.js'; import { messageReportCreationResultValidator } from './message-report-validators.js'; import { fetchMessageInfosResponseValidator, fetchPinnedMessagesResultValidator, sendEditMessageResponseValidator, sendMessageResponseValidator, searchMessagesResponseValidator, } from './message-validators.js'; import { initialReduxStateValidator } from './redux-state-validators.js'; import { relationshipErrorsValidator } from './relationship-validators.js'; import { fetchErrorReportInfosResponseValidator, reportCreationResponseValidator, } from './report-validators.js'; import { exactUserSearchResultValidator, userSearchResultValidator, } from './search-validators.js'; import { siweNonceResponseValidator } from './siwe-nonce-validators.js'; import { changeThreadSettingsResultValidator, leaveThreadResultValidator, newThreadResponseValidator, roleDeletionResultValidator, roleModificationResultValidator, threadFetchMediaResultValidator, threadJoinResultValidator, toggleMessagePinResultValidator, } from './thread-validators.js'; import { MultimediaUploadResultValidator } from './upload-validators.js'; import { claimUsernameResponseValidator, subscriptionUpdateResponseValidator, updateUserAvatarResponderValidator, logInResponseValidator, logOutResponseValidator, registerResponseValidator, } from './user-validators.js'; import { versionResponseValidator } from './version-validators.js'; import { setThreadUnreadStatusResultValidator, updateActivityResultValidator, } from '../activity-types.js'; import { inviteLinkValidator } from '../link-types.js'; import { uploadMultimediaResultValidator } from '../media-types.js'; import { getOlmSessionInitializationDataResponseValidator } from '../olm-session-types.js'; import { serverStateSyncSocketPayloadValidator } from '../socket-types.js'; const sessionChangingEndpoints = Object.freeze({ log_out: { validator: logOutResponseValidator, }, delete_account: { validator: logOutResponseValidator, }, create_account: { validator: registerResponseValidator, }, log_in: { validator: logInResponseValidator, }, update_password: { validator: logInResponseValidator, }, policy_acknowledgment: { validator: t.Nil, }, keyserver_auth: { validator: logInResponseValidator, }, }); const uploadEndpoints = Object.freeze({ upload_multimedia: { validator: MultimediaUploadResultValidator, }, }); const largeDataFetchEndpoints = Object.freeze({ get_initial_redux_state: { validator: initialReduxStateValidator, }, fetch_pending_updates: { validator: serverStateSyncSocketPayloadValidator, }, }); const socketOnlyEndpoints = Object.freeze({ update_activity: { validator: updateActivityResultValidator, }, update_calendar_query: { validator: deltaEntryInfosResultValidator, }, }); const socketPreferredEndpoints = Object.freeze({}); const httpPreferredEndpoints = Object.freeze({ create_report: { validator: reportCreationResponseValidator, }, create_reports: { validator: t.Nil }, create_entry: { validator: saveEntryResponseValidator }, create_error_report: { validator: reportCreationResponseValidator }, create_message_report: { validator: messageReportCreationResultValidator }, create_multimedia_message: { validator: sendMessageResponseValidator }, create_or_update_public_link: { validator: inviteLinkValidator }, create_reaction_message: { validator: sendMessageResponseValidator }, edit_message: { validator: sendEditMessageResponseValidator }, create_text_message: { validator: sendMessageResponseValidator }, create_thread: { validator: newThreadResponseValidator }, delete_entry: { validator: deleteEntryResponseValidator }, delete_community_role: { validator: roleDeletionResultValidator }, delete_thread: { validator: leaveThreadResultValidator }, delete_upload: { validator: t.Nil }, disable_invite_link: { validator: t.Nil }, exact_search_user: { validator: exactUserSearchResultValidator }, fetch_entries: { validator: fetchEntryInfosResponseValidator }, fetch_entry_revisions: { validator: fetchEntryRevisionInfosResultValidator }, fetch_error_report_infos: { validator: fetchErrorReportInfosResponseValidator, }, fetch_messages: { validator: fetchMessageInfosResponseValidator }, fetch_pinned_messages: { validator: fetchPinnedMessagesResultValidator }, fetch_primary_invite_links: { validator: fetchInviteLinksResponseValidator }, fetch_thread_media: { validator: threadFetchMediaResultValidator }, join_thread: { validator: threadJoinResultValidator }, leave_thread: { validator: leaveThreadResultValidator }, modify_community_role: { validator: roleModificationResultValidator }, remove_members: { validator: changeThreadSettingsResultValidator }, restore_entry: { validator: restoreEntryResponseValidator }, search_users: { validator: userSearchResultValidator }, send_password_reset_email: { validator: t.Nil }, send_verification_email: { validator: t.Nil }, set_thread_unread_status: { validator: setThreadUnreadStatusResultValidator }, toggle_message_pin: { validator: toggleMessagePinResultValidator }, update_account: { validator: t.Nil }, update_user_settings: { validator: t.Nil }, update_device_token: { validator: t.Nil }, update_entry: { validator: saveEntryResponseValidator }, update_relationships: { validator: relationshipErrorsValidator }, update_relationships2: { validator: relationshipErrorsValidator }, update_role: { validator: changeThreadSettingsResultValidator }, update_thread: { validator: changeThreadSettingsResultValidator }, update_user_subscription: { validator: subscriptionUpdateResponseValidator }, verify_code: { validator: t.Nil }, verify_invite_link: { validator: inviteLinkVerificationResponseValidator }, siwe_nonce: { validator: siweNonceResponseValidator }, siwe_auth: { validator: logInResponseValidator }, claim_username: { validator: claimUsernameResponseValidator }, update_user_avatar: { validator: updateUserAvatarResponderValidator }, upload_media_metadata: { validator: uploadMultimediaResultValidator }, search_messages: { validator: searchMessagesResponseValidator }, get_olm_session_initialization_data: { validator: getOlmSessionInitializationDataResponseValidator, }, version: { validator: versionResponseValidator }, fetch_community_infos: { validator: fetchCommunityInfosResponseValidator }, fetch_all_community_infos_with_names: { validator: fetchAllCommunityInfosWithNamesResponseValidator, }, create_or_update_farcaster_channel_tag: { validator: createOrUpdateFarcasterChannelTagResponseValidator, }, - delete_farcaster_channel_tag: { validator: t.Nil }, + delete_farcaster_channel_tag: { + validator: deleteFarcasterChannelTagResponseValidator, + }, }); export const endpointValidators = Object.freeze({ ...sessionChangingEndpoints, ...uploadEndpoints, ...largeDataFetchEndpoints, ...socketOnlyEndpoints, ...socketPreferredEndpoints, ...httpPreferredEndpoints, }); diff --git a/lib/types/validators/farcaster-channel-tag-validators.js b/lib/types/validators/farcaster-channel-tag-validators.js index 4fe34fcea..a0769b0a9 100644 --- a/lib/types/validators/farcaster-channel-tag-validators.js +++ b/lib/types/validators/farcaster-channel-tag-validators.js @@ -1,27 +1,34 @@ // @flow -import t, { type TInterface } from 'tcomb'; +import t, { type TInterface, type TMaybe } from 'tcomb'; +import { changeThreadSettingsResultValidator } from './thread-validators.js'; import { tShape, tID } from '../../utils/validation-utils.js'; -import type { CreateOrUpdateFarcasterChannelTagResponse } from '../community-types'; +import type { + CreateOrUpdateFarcasterChannelTagResponse, + DeleteFarcasterChannelTagResponse, +} from '../community-types'; import { rawMessageInfoValidator } from '../message-types.js'; import { serverUpdateInfoValidator, type ServerUpdateInfo, } from '../update-types.js'; const updatesResultValidator: TInterface<{ +newUpdates: $ReadOnlyArray, }> = tShape<{ +newUpdates: $ReadOnlyArray, }>({ newUpdates: t.list(serverUpdateInfoValidator), }); export const createOrUpdateFarcasterChannelTagResponseValidator: TInterface = tShape({ commCommunityID: tID, farcasterChannelID: t.String, newMessageInfos: t.maybe(t.list(rawMessageInfoValidator)), updatesResult: t.maybe(updatesResultValidator), }); + +export const deleteFarcasterChannelTagResponseValidator: TMaybe = + t.maybe(changeThreadSettingsResultValidator);