Adding revisions for actor merges.
This commit is contained in:
@@ -128,6 +128,14 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (delta.key === 'mergeTarget' || delta.key === 'mergeSource') {
|
||||||
|
// new socials don't have IDs yet, so we need to compare the values
|
||||||
|
return {
|
||||||
|
...delta,
|
||||||
|
value: `#${delta.actorId} ${delta.actorName} (${delta.profileIds.length} profiles${delta.profileIds.length > 0 ? `: ${delta.profileIds.join()}` : ''})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (dateKeys.includes(delta.key) && delta.value) {
|
if (dateKeys.includes(delta.key) && delta.value) {
|
||||||
return {
|
return {
|
||||||
...delta,
|
...delta,
|
||||||
|
|||||||
477
src/actors.js
477
src/actors.js
@@ -597,6 +597,205 @@ function curateActorEntry(actor, context) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBaseActor(actor) {
|
||||||
|
return Object.fromEntries(Object.entries(actor).map(([key, values]) => {
|
||||||
|
if ([
|
||||||
|
'scenes',
|
||||||
|
'likes',
|
||||||
|
'stashes',
|
||||||
|
'profiles',
|
||||||
|
].includes(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([
|
||||||
|
'socials',
|
||||||
|
].includes(key)) {
|
||||||
|
return [key, values];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values?.id) {
|
||||||
|
return [key, values.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values?.metric) {
|
||||||
|
return [key, values.metric];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(values)) {
|
||||||
|
return [key, values.map((value) => value?.hash || value?.id || value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [key, values];
|
||||||
|
}).filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeltas(edits, baseActor, options) {
|
||||||
|
return Promise.all(Object.entries(edits).map(async ([key, value]) => {
|
||||||
|
if (baseActor[key] === value || typeof value === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'originCountry' && edits.originPlace) {
|
||||||
|
// place overrides country
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'residenceCountry' && edits.residencePlace) {
|
||||||
|
// place overrides country
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['originPlace', 'residencePlace'].includes(key)) {
|
||||||
|
if (!value && !baseActor[key]) {
|
||||||
|
// don't pollute deltas if value is already unset
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return [
|
||||||
|
// { key: key.includes('origin') ? 'originCountry' : 'residenceCountry', value: null },
|
||||||
|
{ key: key.includes('origin') ? 'originState' : 'residenceState', value: null },
|
||||||
|
{ key: key.includes('origin') ? 'originCity' : 'residenceCity', value: null },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedLocation = await resolvePlace(value, {
|
||||||
|
knex,
|
||||||
|
redis,
|
||||||
|
logger,
|
||||||
|
slugify,
|
||||||
|
unprint,
|
||||||
|
}, {
|
||||||
|
userAgent: 'contact via https://traxxx.me/',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolvedLocation) {
|
||||||
|
throw new Error(`Failed to resolve ${key} ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryKey = key.includes('origin') ? 'originCountry' : 'residenceCountry';
|
||||||
|
|
||||||
|
if (!resolvedLocation.country) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: countryKey,
|
||||||
|
value: resolvedLocation.country,
|
||||||
|
comment: edits[countryKey] && edits[countryKey] !== resolvedLocation.country
|
||||||
|
? `${countryKey} overridden by resolved ${key}`
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: key.includes('origin') ? 'originState' : 'residenceState',
|
||||||
|
value: resolvedLocation.state || null, // explicitly unset to prevent outcomes like Los Angeles, Greenland
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: key.includes('origin') ? 'originCity' : 'residenceCity',
|
||||||
|
value: resolvedLocation.city || null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'socials') {
|
||||||
|
const convertedSocials = curateSocials(value);
|
||||||
|
|
||||||
|
const convertedUrls = value
|
||||||
|
.filter((social) => social.url && !convertedSocials.some((convertedSocial) => convertedSocial.url === social.url))
|
||||||
|
.map((social) => social.url);
|
||||||
|
|
||||||
|
const conversionComment = convertedUrls.length > 0
|
||||||
|
? `curated URLs ${convertedUrls.join(', ')} as social handles`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value: convertedSocials,
|
||||||
|
comment: conversionComment,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['cup', 'bust', 'waist', 'hip'].includes(key)) {
|
||||||
|
const convertedValue = convertFigure(key, value, options.figureUnits);
|
||||||
|
|
||||||
|
const conversionComment = !value || convertedValue === value
|
||||||
|
? null
|
||||||
|
: `${key} converted from ${value} ${options.figureUnits?.toUpperCase() || 'US'} to ${convertedValue} US`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value: convertedValue,
|
||||||
|
comment: conversionComment,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['height'].includes(key)) {
|
||||||
|
const convertedValue = convertHeight(value, options.sizeUnits);
|
||||||
|
|
||||||
|
if (baseActor[key] === convertedValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversionComment = !value || convertedValue === value
|
||||||
|
? null
|
||||||
|
: `${key} converted from ${value[0]} in ${value[1]} ft to ${convertedValue} cm`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value: convertedValue,
|
||||||
|
comment: conversionComment,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['weight'].includes(key)) {
|
||||||
|
const convertedValue = convertWeight(value, options.sizeUnits);
|
||||||
|
|
||||||
|
if (baseActor[key] === convertedValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversionComment = !value || convertedValue === value
|
||||||
|
? null
|
||||||
|
: `${key} converted from ${value} lbs to ${convertedValue} kg`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value: convertedValue,
|
||||||
|
comment: conversionComment,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['penisLength', 'penisGirth'].includes(key) && options.penisUnits === 'imperial') {
|
||||||
|
const convertedValue = Math.round(convert(value, 'inches').to('cm'));
|
||||||
|
|
||||||
|
if (baseActor[key] === convertedValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value: convertedValue,
|
||||||
|
comment: `${key} converted from ${value} inches to ${convertedValue} cm`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const valueSet = new Set(value);
|
||||||
|
const baseSet = new Set(baseActor[key]);
|
||||||
|
|
||||||
|
if (valueSet.size === baseSet.size && baseActor[key].every((id) => valueSet.has(id))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key, value: Array.from(valueSet) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key, value };
|
||||||
|
})).then((rawDeltas) => rawDeltas.flat().filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
export async function createActor(newActor, context, reqUser) {
|
export async function createActor(newActor, context, reqUser) {
|
||||||
if (!reqUser || reqUser.role === 'user') {
|
if (!reqUser || reqUser.role === 'user') {
|
||||||
throw new HttpError('You are not permitted to create actors', 403);
|
throw new HttpError('You are not permitted to create actors', 403);
|
||||||
@@ -619,7 +818,7 @@ export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
|
|||||||
throw new HttpError('Cannot merge actor profile into itself', 400);
|
throw new HttpError('Cannot merge actor profile into itself', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [targetActor, sourceActors] = await Promise.all([
|
const [targetActorEntry, sourceActorEntries] = await Promise.all([
|
||||||
knex('actors')
|
knex('actors')
|
||||||
.where('id', targetActorId)
|
.where('id', targetActorId)
|
||||||
.whereNull('entity_id')
|
.whereNull('entity_id')
|
||||||
@@ -629,14 +828,27 @@ export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
|
|||||||
.whereIn('id', sourceActorIds),
|
.whereIn('id', sourceActorIds),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!targetActor) {
|
if (!targetActorEntry) {
|
||||||
throw new HttpError('Target actor not found', 404);
|
throw new HttpError('Target actor not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceActors.length < sourceActorIds.length) {
|
if (sourceActorEntries.length < sourceActorIds.length) {
|
||||||
throw new HttpError('Source actor not found', 404);
|
throw new HttpError('Source actor not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [[targetActor], sourceActors] = await Promise.all([
|
||||||
|
fetchActorsById([targetActorEntry.id], {
|
||||||
|
reqUser,
|
||||||
|
includeAssets: true,
|
||||||
|
includePartOf: true,
|
||||||
|
}),
|
||||||
|
fetchActorsById(sourceActorEntries.map((sourceActorEntry) => sourceActorEntry.id), {
|
||||||
|
reqUser,
|
||||||
|
includeAssets: true,
|
||||||
|
includePartOf: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const trx = await knex.transaction();
|
const trx = await knex.transaction();
|
||||||
|
|
||||||
let mergedProfiles = [];
|
let mergedProfiles = [];
|
||||||
@@ -674,7 +886,7 @@ export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
|
|||||||
.whereIn('actor_id', sourceActorIds)
|
.whereIn('actor_id', sourceActorIds)
|
||||||
.whereNotIn('entity_id', existingProfiles.map((profile) => profile.entity_id))
|
.whereNotIn('entity_id', existingProfiles.map((profile) => profile.entity_id))
|
||||||
.whereNotIn('id', duplicateSourceProfiles.map((profile) => profile.id))
|
.whereNotIn('id', duplicateSourceProfiles.map((profile) => profile.id))
|
||||||
.returning('id');
|
.returning(['id', 'actor_id']);
|
||||||
|
|
||||||
// find releases that have more than one source actor assigned
|
// find releases that have more than one source actor assigned
|
||||||
duplicateSourceActors = await trx('releases_actors')
|
duplicateSourceActors = await trx('releases_actors')
|
||||||
@@ -748,11 +960,61 @@ export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
|
|||||||
.whereIn('actor_id', sourceActorIds)
|
.whereIn('actor_id', sourceActorIds)
|
||||||
.returning('stash_id');
|
.returning('stash_id');
|
||||||
|
|
||||||
/* TODO: add revision entries
|
const baseTargetActor = getBaseActor(targetActor);
|
||||||
console.log('SOURCES', targetActor, sourceActors);
|
|
||||||
|
|
||||||
throw new Error('ABORT');
|
const targetDeltas = sourceActors.map((sourceActor) => ({
|
||||||
*/
|
key: 'mergeSource',
|
||||||
|
actorId: sourceActor.id,
|
||||||
|
actorName: sourceActor.name,
|
||||||
|
profileIds: sourceProfiles
|
||||||
|
.filter((sourceProfile) => sourceProfile.actor_id === sourceActor.id)
|
||||||
|
.map((sourceProfile) => sourceProfile.id),
|
||||||
|
}));
|
||||||
|
|
||||||
|
await trx('actors_revisions')
|
||||||
|
.insert({
|
||||||
|
actor_id: targetActor.id,
|
||||||
|
user_id: reqUser.id,
|
||||||
|
base: baseTargetActor,
|
||||||
|
deltas: JSON.stringify(targetDeltas),
|
||||||
|
hash: mj.hash({
|
||||||
|
base: baseTargetActor,
|
||||||
|
deltas: targetDeltas,
|
||||||
|
}),
|
||||||
|
reviewed_by: reqUser.id,
|
||||||
|
reviewed_at: knex.fn.now(),
|
||||||
|
applied_at: knex.fn.now(),
|
||||||
|
approved: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await trx('actors_revisions')
|
||||||
|
.insert(sourceActors.map((sourceActor) => {
|
||||||
|
const baseSourceActor = getBaseActor(sourceActor);
|
||||||
|
|
||||||
|
const deltas = [{
|
||||||
|
key: 'mergeTarget',
|
||||||
|
actorId: targetActorId,
|
||||||
|
actorName: targetActor.name,
|
||||||
|
profileIds: sourceProfiles
|
||||||
|
.filter((sourceProfile) => sourceProfile.actor_id === sourceActor.id)
|
||||||
|
.map((sourceProfile) => sourceProfile.id),
|
||||||
|
}];
|
||||||
|
|
||||||
|
return {
|
||||||
|
actor_id: sourceActor.id,
|
||||||
|
user_id: reqUser.id,
|
||||||
|
base: baseSourceActor,
|
||||||
|
deltas: JSON.stringify(deltas),
|
||||||
|
hash: mj.hash({
|
||||||
|
base: baseSourceActor,
|
||||||
|
deltas: targetDeltas,
|
||||||
|
}),
|
||||||
|
reviewed_by: reqUser.id,
|
||||||
|
reviewed_at: knex.fn.now(),
|
||||||
|
applied_at: knex.fn.now(),
|
||||||
|
approved: true,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
await trx.commit();
|
await trx.commit();
|
||||||
}
|
}
|
||||||
@@ -1151,205 +1413,6 @@ function curateSocials(socials) {
|
|||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBaseActor(actor) {
|
|
||||||
return Object.fromEntries(Object.entries(actor).map(([key, values]) => {
|
|
||||||
if ([
|
|
||||||
'scenes',
|
|
||||||
'likes',
|
|
||||||
'stashes',
|
|
||||||
'profiles',
|
|
||||||
].includes(key)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([
|
|
||||||
'socials',
|
|
||||||
].includes(key)) {
|
|
||||||
return [key, values];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values?.id) {
|
|
||||||
return [key, values.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values?.metric) {
|
|
||||||
return [key, values.metric];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(values)) {
|
|
||||||
return [key, values.map((value) => value?.hash || value?.id || value)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [key, values];
|
|
||||||
}).filter(Boolean));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDeltas(edits, baseActor, options) {
|
|
||||||
return Promise.all(Object.entries(edits).map(async ([key, value]) => {
|
|
||||||
if (baseActor[key] === value || typeof value === 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'originCountry' && edits.originPlace) {
|
|
||||||
// place overrides country
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'residenceCountry' && edits.residencePlace) {
|
|
||||||
// place overrides country
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['originPlace', 'residencePlace'].includes(key)) {
|
|
||||||
if (!value && !baseActor[key]) {
|
|
||||||
// don't pollute deltas if value is already unset
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return [
|
|
||||||
// { key: key.includes('origin') ? 'originCountry' : 'residenceCountry', value: null },
|
|
||||||
{ key: key.includes('origin') ? 'originState' : 'residenceState', value: null },
|
|
||||||
{ key: key.includes('origin') ? 'originCity' : 'residenceCity', value: null },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedLocation = await resolvePlace(value, {
|
|
||||||
knex,
|
|
||||||
redis,
|
|
||||||
logger,
|
|
||||||
slugify,
|
|
||||||
unprint,
|
|
||||||
}, {
|
|
||||||
userAgent: 'contact via https://traxxx.me/',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!resolvedLocation) {
|
|
||||||
throw new Error(`Failed to resolve ${key} ${value}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const countryKey = key.includes('origin') ? 'originCountry' : 'residenceCountry';
|
|
||||||
|
|
||||||
if (!resolvedLocation.country) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: countryKey,
|
|
||||||
value: resolvedLocation.country,
|
|
||||||
comment: edits[countryKey] && edits[countryKey] !== resolvedLocation.country
|
|
||||||
? `${countryKey} overridden by resolved ${key}`
|
|
||||||
: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: key.includes('origin') ? 'originState' : 'residenceState',
|
|
||||||
value: resolvedLocation.state || null, // explicitly unset to prevent outcomes like Los Angeles, Greenland
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: key.includes('origin') ? 'originCity' : 'residenceCity',
|
|
||||||
value: resolvedLocation.city || null,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'socials') {
|
|
||||||
const convertedSocials = curateSocials(value);
|
|
||||||
|
|
||||||
const convertedUrls = value
|
|
||||||
.filter((social) => social.url && !convertedSocials.some((convertedSocial) => convertedSocial.url === social.url))
|
|
||||||
.map((social) => social.url);
|
|
||||||
|
|
||||||
const conversionComment = convertedUrls.length > 0
|
|
||||||
? `curated URLs ${convertedUrls.join(', ')} as social handles`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
value: convertedSocials,
|
|
||||||
comment: conversionComment,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['cup', 'bust', 'waist', 'hip'].includes(key)) {
|
|
||||||
const convertedValue = convertFigure(key, value, options.figureUnits);
|
|
||||||
|
|
||||||
const conversionComment = !value || convertedValue === value
|
|
||||||
? null
|
|
||||||
: `${key} converted from ${value} ${options.figureUnits?.toUpperCase() || 'US'} to ${convertedValue} US`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
value: convertedValue,
|
|
||||||
comment: conversionComment,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['height'].includes(key)) {
|
|
||||||
const convertedValue = convertHeight(value, options.sizeUnits);
|
|
||||||
|
|
||||||
if (baseActor[key] === convertedValue) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const conversionComment = !value || convertedValue === value
|
|
||||||
? null
|
|
||||||
: `${key} converted from ${value[0]} in ${value[1]} ft to ${convertedValue} cm`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
value: convertedValue,
|
|
||||||
comment: conversionComment,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['weight'].includes(key)) {
|
|
||||||
const convertedValue = convertWeight(value, options.sizeUnits);
|
|
||||||
|
|
||||||
if (baseActor[key] === convertedValue) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const conversionComment = !value || convertedValue === value
|
|
||||||
? null
|
|
||||||
: `${key} converted from ${value} lbs to ${convertedValue} kg`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
value: convertedValue,
|
|
||||||
comment: conversionComment,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['penisLength', 'penisGirth'].includes(key) && options.penisUnits === 'imperial') {
|
|
||||||
const convertedValue = Math.round(convert(value, 'inches').to('cm'));
|
|
||||||
|
|
||||||
if (baseActor[key] === convertedValue) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
value: convertedValue,
|
|
||||||
comment: `${key} converted from ${value} inches to ${convertedValue} cm`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
const valueSet = new Set(value);
|
|
||||||
const baseSet = new Set(baseActor[key]);
|
|
||||||
|
|
||||||
if (valueSet.size === baseSet.size && baseActor[key].every((id) => valueSet.has(id))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { key, value: Array.from(valueSet) };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { key, value };
|
|
||||||
})).then((rawDeltas) => rawDeltas.flat().filter(Boolean));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createActorRevision(actorId, {
|
export async function createActorRevision(actorId, {
|
||||||
edits,
|
edits,
|
||||||
comment,
|
comment,
|
||||||
|
|||||||
Reference in New Issue
Block a user