Merge remote-tracking branch 'soapbox/develop' into lexical

This commit is contained in:
marcin mikołajczak 2023-05-22 16:51:34 +02:00
commit b6f4897450
12 changed files with 143 additions and 35 deletions

View File

@ -127,7 +127,7 @@ const fetchConfig = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
return api(getState)
.get('/api/pleroma/admin/config')
.get('/api/v1/pleroma/admin/config')
.then(({ data }) => {
dispatch({ type: ADMIN_CONFIG_FETCH_SUCCESS, configs: data.configs, needsReboot: data.need_reboot });
}).catch(error => {
@ -139,7 +139,7 @@ const updateConfig = (configs: Record<string, any>[]) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST, configs });
return api(getState)
.post('/api/pleroma/admin/config', { configs })
.post('/api/v1/pleroma/admin/config', { configs })
.then(({ data }) => {
dispatch({ type: ADMIN_CONFIG_UPDATE_SUCCESS, configs: data.configs, needsReboot: data.need_reboot });
}).catch(error => {
@ -178,7 +178,7 @@ const fetchMastodonReports = (params: Record<string, any>) =>
const fetchPleromaReports = (params: Record<string, any>) =>
(dispatch: AppDispatch, getState: () => RootState) =>
api(getState)
.get('/api/pleroma/admin/reports', { params })
.get('/api/v1/pleroma/admin/reports', { params })
.then(({ data: { reports } }) => {
reports.forEach((report: APIEntity) => {
dispatch(importFetchedAccount(report.account));
@ -224,7 +224,7 @@ const patchMastodonReports = (reports: { id: string, state: string }[]) =>
const patchPleromaReports = (reports: { id: string, state: string }[]) =>
(dispatch: AppDispatch, getState: () => RootState) =>
api(getState)
.patch('/api/pleroma/admin/reports', { reports })
.patch('/api/v1/pleroma/admin/reports', { reports })
.then(() => {
dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports });
}).catch(error => {
@ -286,7 +286,7 @@ const fetchPleromaUsers = (filters: string[], page: number, query?: string | nul
if (query) params.query = query;
return api(getState)
.get('/api/pleroma/admin/users', { params })
.get('/api/v1/pleroma/admin/users', { params })
.then(({ data: { users, count, page_size: pageSize } }) => {
dispatch(fetchRelationships(users.map((user: APIEntity) => user.id)));
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users, count, pageSize, filters, page });
@ -331,7 +331,7 @@ const deactivatePleromaUsers = (accountIds: string[]) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const nicknames = nicknamesFromIds(getState, accountIds);
return api(getState)
.patch('/api/pleroma/admin/users/deactivate', { nicknames })
.patch('/api/v1/pleroma/admin/users/deactivate', { nicknames })
.then(({ data: { users } }) => {
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, users, accountIds });
}).catch(error => {
@ -360,7 +360,7 @@ const deleteUsers = (accountIds: string[]) =>
const nicknames = nicknamesFromIds(getState, accountIds);
dispatch({ type: ADMIN_USERS_DELETE_REQUEST, accountIds });
return api(getState)
.delete('/api/pleroma/admin/users', { data: { nicknames } })
.delete('/api/v1/pleroma/admin/users', { data: { nicknames } })
.then(({ data: nicknames }) => {
dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames, accountIds });
}).catch(error => {
@ -384,7 +384,7 @@ const approvePleromaUsers = (accountIds: string[]) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const nicknames = nicknamesFromIds(getState, accountIds);
return api(getState)
.patch('/api/pleroma/admin/users/approve', { nicknames })
.patch('/api/v1/pleroma/admin/users/approve', { nicknames })
.then(({ data: { users } }) => {
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users, accountIds });
}).catch(error => {
@ -412,7 +412,7 @@ const deleteStatus = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_STATUS_DELETE_REQUEST, id });
return api(getState)
.delete(`/api/pleroma/admin/statuses/${id}`)
.delete(`/api/v1/pleroma/admin/statuses/${id}`)
.then(() => {
dispatch({ type: ADMIN_STATUS_DELETE_SUCCESS, id });
}).catch(error => {
@ -424,7 +424,7 @@ const toggleStatusSensitivity = (id: string, sensitive: boolean) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST, id });
return api(getState)
.put(`/api/pleroma/admin/statuses/${id}`, { sensitive: !sensitive })
.put(`/api/v1/pleroma/admin/statuses/${id}`, { sensitive: !sensitive })
.then(() => {
dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS, id });
}).catch(error => {
@ -436,7 +436,7 @@ const fetchModerationLog = (params?: Record<string, any>) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_LOG_FETCH_REQUEST });
return api(getState)
.get('/api/pleroma/admin/moderation_log', { params })
.get('/api/v1/pleroma/admin/moderation_log', { params })
.then(({ data }) => {
dispatch({ type: ADMIN_LOG_FETCH_SUCCESS, items: data.items, total: data.total });
return data;
@ -567,7 +567,7 @@ const suggestUsers = (accountIds: string[]) =>
const nicknames = nicknamesFromIds(getState, accountIds);
dispatch({ type: ADMIN_USERS_SUGGEST_REQUEST, accountIds });
return api(getState)
.patch('/api/pleroma/admin/users/suggest', { nicknames })
.patch('/api/v1/pleroma/admin/users/suggest', { nicknames })
.then(({ data: { users } }) => {
dispatch({ type: ADMIN_USERS_SUGGEST_SUCCESS, users, accountIds });
}).catch(error => {
@ -580,7 +580,7 @@ const unsuggestUsers = (accountIds: string[]) =>
const nicknames = nicknamesFromIds(getState, accountIds);
dispatch({ type: ADMIN_USERS_UNSUGGEST_REQUEST, accountIds });
return api(getState)
.patch('/api/pleroma/admin/users/unsuggest', { nicknames })
.patch('/api/v1/pleroma/admin/users/unsuggest', { nicknames })
.then(({ data: { users } }) => {
dispatch({ type: ADMIN_USERS_UNSUGGEST_SUCCESS, users, accountIds });
}).catch(error => {
@ -636,7 +636,7 @@ const fetchAdminAnnouncements = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_REQUEST });
return api(getState)
.get('/api/pleroma/admin/announcements', { params: { limit: 50 } })
.get('/api/v1/pleroma/admin/announcements', { params: { limit: 50 } })
.then(({ data }) => {
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS, announcements: data });
return data;
@ -651,7 +651,7 @@ const expandAdminAnnouncements = () =>
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST });
return api(getState)
.get('/api/pleroma/admin/announcements', { params: { limit: 50, offset: page * 50 } })
.get('/api/v1/pleroma/admin/announcements', { params: { limit: 50, offset: page * 50 } })
.then(({ data }) => {
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS, announcements: data });
return data;
@ -687,7 +687,7 @@ const handleCreateAnnouncement = () =>
const { id, content, starts_at, ends_at, all_day } = getState().admin_announcements.form;
return api(getState)[id ? 'patch' : 'post'](
id ? `/api/pleroma/admin/announcements/${id}` : '/api/pleroma/admin/announcements',
id ? `/api/v1/pleroma/admin/announcements/${id}` : '/api/v1/pleroma/admin/announcements',
{ content, starts_at, ends_at, all_day },
).then(({ data }) => {
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_SUCCESS, announcement: data });
@ -703,7 +703,7 @@ const deleteAnnouncement = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_REQUEST, id });
return api(getState).delete(`/api/pleroma/admin/announcements/${id}`).then(({ data }) => {
return api(getState).delete(`/api/v1/pleroma/admin/announcements/${id}`).then(({ data }) => {
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_SUCCESS, id });
toast.success(messages.announcementDeleteSuccess);
dispatch(fetchAdminAnnouncements());

View File

@ -6,9 +6,11 @@
* @see module:soapbox/actions/auth
*/
import { getBaseURL } from 'soapbox/utils/state';
import { baseClient } from '../api';
import type { AppDispatch } from 'soapbox/store';
import type { AppDispatch, RootState } from 'soapbox/store';
export const OAUTH_TOKEN_CREATE_REQUEST = 'OAUTH_TOKEN_CREATE_REQUEST';
export const OAUTH_TOKEN_CREATE_SUCCESS = 'OAUTH_TOKEN_CREATE_SUCCESS';
@ -31,9 +33,10 @@ export const obtainOAuthToken = (params: Record<string, string | undefined>, bas
};
export const revokeOAuthToken = (params: Record<string, string>) =>
(dispatch: AppDispatch) => {
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: OAUTH_TOKEN_REVOKE_REQUEST, params });
return baseClient().post('/oauth/revoke', params).then(({ data }) => {
const baseURL = getBaseURL(getState());
return baseClient(null, baseURL).post('/oauth/revoke', params).then(({ data }) => {
dispatch({ type: OAUTH_TOKEN_REVOKE_SUCCESS, params, data });
return data;
}).catch(error => {

View File

@ -95,7 +95,7 @@ const connectTimelineStream = (
dispatch(disconnectTimeline(timelineId));
},
onReceive(data: any) {
onReceive(websocket, data: any) {
switch (data.event) {
case 'update':
dispatch(processTimelineUpdate(timelineId, JSON.parse(data.payload), accept));
@ -181,6 +181,11 @@ const connectTimelineStream = (
case 'marker':
dispatch({ type: MARKER_FETCH_SUCCESS, marker: JSON.parse(data.payload) });
break;
case 'nostr.sign':
window.nostr?.signEvent(JSON.parse(data.payload))
.then((data) => websocket.send(JSON.stringify({ type: 'nostr.sign', data })))
.catch(() => console.warn('Failed to sign Nostr event.'));
break;
}
},
};
@ -215,6 +220,9 @@ const connectListStream = (id: string) =>
const connectGroupStream = (id: string) =>
connectTimelineStream(`group:${id}`, `group&group=${id}`);
const connectNostrStream = () =>
connectTimelineStream('nostr', 'nostr');
export {
STREAMING_CHAT_UPDATE,
STREAMING_FOLLOW_RELATIONSHIPS_UPDATE,
@ -227,4 +235,5 @@ export {
connectDirectStream,
connectListStream,
connectGroupStream,
connectNostrStream,
};

View File

@ -45,7 +45,7 @@ const messages = defineMessages({
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
report: { id: 'account.report', defaultMessage: 'Report @{name}' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to profile' },
copy: { id: 'account.copy', defaultMessage: 'Copy link to profile' },
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
media: { id: 'account.media', defaultMessage: 'Media' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },

View File

@ -113,7 +113,7 @@ const HotkeysModal: React.FC<IHotkeysModal> = ({ onClose }) => {
<TableCell><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></TableCell>
</tr>
<tr>
<TableCell><Hotkey>s</Hotkey></TableCell>
<TableCell><Hotkey>s</Hotkey>, <Hotkey>/</Hotkey></TableCell>
<TableCell><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></TableCell>
</tr>
<tr>

View File

@ -16,7 +16,7 @@ import { openModal } from 'soapbox/actions/modals';
import { expandNotifications } from 'soapbox/actions/notifications';
import { register as registerPushNotifications } from 'soapbox/actions/push-notifications';
import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses';
import { connectUserStream } from 'soapbox/actions/streaming';
import { connectNostrStream, connectUserStream } from 'soapbox/actions/streaming';
import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions';
import { expandHomeTimeline } from 'soapbox/actions/timelines';
import GroupLookupHoc from 'soapbox/components/hoc/group-lookup-hoc';
@ -156,7 +156,7 @@ const EmptyPage = HomePage;
const keyMap = {
help: '?',
new: 'n',
search: 's',
search: ['s', '/'],
forceNew: 'option+n',
reply: 'r',
favourite: 'f',
@ -391,7 +391,8 @@ const UI: React.FC<IUI> = ({ children }) => {
const instance = useInstance();
const statContext = useStatContext();
const disconnect = useRef<any>(null);
const userStream = useRef<any>(null);
const nostrStream = useRef<any>(null);
const node = useRef<HTMLDivElement | null>(null);
const hotkeys = useRef<HTMLDivElement | null>(null);
@ -416,15 +417,24 @@ const UI: React.FC<IUI> = ({ children }) => {
};
const connectStreaming = () => {
if (!disconnect.current && accessToken && streamingUrl) {
disconnect.current = dispatch(connectUserStream({ statContext }));
if (accessToken && streamingUrl) {
if (!userStream.current) {
userStream.current = dispatch(connectUserStream({ statContext }));
}
if (!nostrStream.current && window.nostr) {
nostrStream.current = dispatch(connectNostrStream());
}
}
};
const disconnectStreaming = () => {
if (disconnect.current) {
disconnect.current();
disconnect.current = null;
if (userStream.current) {
userStream.current();
userStream.current = null;
}
if (nostrStream.current) {
nostrStream.current();
nostrStream.current = null;
}
};

View File

@ -10,6 +10,7 @@
"account.block_domain": "Hide everything from {domain}",
"account.blocked": "Blocked",
"account.chat": "Chat with @{name}",
"account.copy": "Copy link to profile",
"account.deactivated": "Deactivated",
"account.direct": "Direct message @{name}",
"account.domain_blocked": "Domain hidden",

View File

@ -8,10 +8,18 @@ import type { AppDispatch, RootState } from 'soapbox/store';
const randomIntUpTo = (max: number) => Math.floor(Math.random() * Math.floor(max));
interface ConnectStreamCallbacks {
onConnect(): void
onDisconnect(): void
onReceive(websocket: WebSocket, data: unknown): void
}
type PollingRefreshFn = (dispatch: AppDispatch, done?: () => void) => void
export function connectStream(
path: string,
pollingRefresh: ((dispatch: AppDispatch, done?: () => void) => void) | null = null,
callbacks: (dispatch: AppDispatch, getState: () => RootState) => Record<string, any> = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} }),
pollingRefresh: PollingRefreshFn | null = null,
callbacks: (dispatch: AppDispatch, getState: () => RootState) => ConnectStreamCallbacks,
) {
return (dispatch: AppDispatch, getState: () => RootState) => {
const streamingAPIBaseURL = getState().instance.urls.get('streaming_api');
@ -35,7 +43,7 @@ export function connectStream(
}
};
let subscription: WebSocketClient;
let subscription: WebSocket;
// If the WebSocket fails to be created, don't crash the whole page,
// just proceed without a subscription.
@ -58,7 +66,7 @@ export function connectStream(
},
received(data) {
onReceive(data);
onReceive(subscription, data);
},
reconnected() {

View File

@ -0,0 +1,8 @@
import type { Event, EventTemplate } from 'nostr-tools';
interface Nostr {
getPublicKey(): Promise<string>
signEvent(event: EventTemplate): Promise<Event>
}
export default Nostr;

7
app/soapbox/types/window.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import type Nostr from './nostr';
declare global {
interface Window {
nostr?: Nostr
}
}

View File

@ -143,6 +143,7 @@
"localforage": "^1.10.0",
"lodash": "^4.7.11",
"mini-css-extract-plugin": "^2.6.0",
"nostr-tools": "^1.8.1",
"path-browserify": "^1.0.1",
"postcss": "^8.4.14",
"postcss-loader": "^7.0.0",

View File

@ -2637,6 +2637,28 @@
dependencies:
eslint-scope "5.1.1"
"@noble/curves@~0.8.3":
version "0.8.3"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-0.8.3.tgz#ad6d48baf2599cf1d58dcb734c14d5225c8996e0"
integrity sha512-OqaOf4RWDaCRuBKJLDURrgVxjLmneGsiCXGuzYB5y95YithZMA6w4uk34DHSm0rKMrrYiaeZj48/81EvaAScLQ==
dependencies:
"@noble/hashes" "1.3.0"
"@noble/hashes@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.0.0.tgz#d5e38bfbdaba174805a4e649f13be9a9ed3351ae"
integrity sha512-DZVbtY62kc3kkBtMHqwCOfXrT/hnoORy5BJ4+HU1IR59X0KWAOqsfzQPcUl/lQLlG7qXbe/fZ3r/emxtAl+sqg==
"@noble/hashes@1.3.0", "@noble/hashes@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1"
integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==
"@noble/secp256k1@^1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c"
integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@ -2923,6 +2945,28 @@
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.2.1.tgz#812edd4104a15a493dda1ccac0b352270d7a188c"
integrity sha512-XiY0IsyHR+DXYS5vBxpoBe/8veTeoRpMHP+vDosLZxL5bnpetzI0igkxkLZS235ldLzyfkxF+2divEwWHP3vMQ==
"@scure/base@^1.1.1", "@scure/base@~1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
"@scure/bip32@^1.1.5":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.2.0.tgz#35692d8f8cc3207200239fc119f9e038e5f465df"
integrity sha512-O+vT/hBVk+ag2i6j2CDemwd1E1MtGt+7O1KzrPNsaNvSsiEK55MyPIxJIMI2PS8Ijj464B2VbQlpRoQXxw1uHg==
dependencies:
"@noble/curves" "~0.8.3"
"@noble/hashes" "~1.3.0"
"@scure/base" "~1.1.0"
"@scure/bip39@^1.1.1":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.0.tgz#a207e2ef96de354de7d0002292ba1503538fc77b"
integrity sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg==
dependencies:
"@noble/hashes" "~1.3.0"
"@scure/base" "~1.1.0"
"@sentry/browser@7.37.2", "@sentry/browser@^7.37.2":
version "7.37.2"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.37.2.tgz#355dd28ad12677d63e0b12c5209d12b3f98ac3a4"
@ -13082,6 +13126,18 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
nostr-tools@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.8.1.tgz#4e54a354cc88ea0200634da3ee5a1c3466e1794c"
integrity sha512-/2IUe5xINUYT5hYBoEz51dfRaodbRHnyF8n+ZbKWCoh0ZRX6AL88OoDNrWaWWo7tP5j5OyzSL9g/z4TP7bshEA==
dependencies:
"@noble/hashes" "1.0.0"
"@noble/secp256k1" "^1.7.1"
"@scure/base" "^1.1.1"
"@scure/bip32" "^1.1.5"
"@scure/bip39" "^1.1.1"
prettier "^2.8.4"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@ -14260,6 +14316,11 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==
prettier@^2.8.4:
version "2.8.7"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450"
integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==
pretty-error@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6"