mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-04-28 17:46:56 +00:00
filter url
properties by mediaType
This commit is contained in:
parent
d8d94b65a0
commit
910b83c531
4 changed files with 235 additions and 34 deletions
|
@ -45,25 +45,36 @@ export class ApUtilityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches a list of URLs or Links for the first one matching a given target URL's host authority.
|
* Finds the "best" URL for a given AP object.
|
||||||
* Returns null if none match.
|
* The list of URLs is first filtered via findSameAuthorityUrl, then further filtered based on mediaType, and finally sorted to select the best one.
|
||||||
* @param targetUrl URL with the target host authority
|
* @throws {IdentifiableError} if object does not have an ID
|
||||||
* @param searchUrls URL, Link, or array to search for matching URLs
|
* @returns the best URL, or null if none were found
|
||||||
*/
|
*/
|
||||||
public findSameAuthorityUrl(targetUrl: string, searchUrls: string | IObject | undefined | (string | IObject)[]): string | null {
|
public findBestObjectUrl(object: IObject): string | null {
|
||||||
|
const targetUrl = getApId(object);
|
||||||
const targetAuthority = this.utilityService.punyHostPSLDomain(targetUrl);
|
const targetAuthority = this.utilityService.punyHostPSLDomain(targetUrl);
|
||||||
|
|
||||||
const match = toArray(searchUrls)
|
const rawUrls = toArray(object.url);
|
||||||
.map(raw => getOneApHrefNullable(raw))
|
const acceptableUrls = rawUrls
|
||||||
.find(url => {
|
.map(raw => ({
|
||||||
|
url: getOneApHrefNullable(raw),
|
||||||
|
type: typeof(raw) === 'object'
|
||||||
|
? raw.mediaType?.toLowerCase()
|
||||||
|
: undefined,
|
||||||
|
}))
|
||||||
|
.filter(({ url, type }) => {
|
||||||
if (!url) return false;
|
if (!url) return false;
|
||||||
if (!this.checkHttps(url)) return false;
|
if (!this.checkHttps(url)) return false;
|
||||||
|
if (!isAcceptableUrlType(type)) return false;
|
||||||
|
|
||||||
const urlAuthority = this.utilityService.punyHostPSLDomain(url);
|
const urlAuthority = this.utilityService.punyHostPSLDomain(url);
|
||||||
return urlAuthority === targetAuthority;
|
return urlAuthority === targetAuthority;
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
return rankUrlType(a.type) - rankUrlType(b.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
return match ?? null;
|
return acceptableUrls[0]?.url ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -78,3 +89,20 @@ export class ApUtilityService {
|
||||||
return url.startsWith('https://') || (url.startsWith('http://') && isNonProd);
|
return url.startsWith('https://') || (url.startsWith('http://') && isNonProd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAcceptableUrlType(type: string | undefined): boolean {
|
||||||
|
if (!type) return true;
|
||||||
|
if (type.startsWith('text/')) return true;
|
||||||
|
if (type.startsWith('application/ld+json')) return true;
|
||||||
|
if (type.startsWith('application/activity+json')) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rankUrlType(type: string | undefined): number {
|
||||||
|
if (!type) return 2;
|
||||||
|
if (type === 'text/html') return 0;
|
||||||
|
if (type.startsWith('text/')) return 1;
|
||||||
|
if (type.startsWith('application/ld+json')) return 3;
|
||||||
|
if (type.startsWith('application/activity+json')) return 4;
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
|
@ -180,7 +180,7 @@ export class ApNoteService {
|
||||||
throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${entryUri}`);
|
throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${entryUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = this.apUtilityService.findSameAuthorityUrl(note.id, note.url);
|
const url = this.apUtilityService.findBestObjectUrl(note);
|
||||||
|
|
||||||
this.logger.info(`Creating the Note: ${note.id}`);
|
this.logger.info(`Creating the Note: ${note.id}`);
|
||||||
|
|
||||||
|
@ -395,7 +395,7 @@ export class ApNoteService {
|
||||||
throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${noteUri}`);
|
throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${noteUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = this.apUtilityService.findSameAuthorityUrl(note.id, note.url);
|
const url = this.apUtilityService.findBestObjectUrl(note);
|
||||||
|
|
||||||
this.logger.info(`Creating the Note: ${note.id}`);
|
this.logger.info(`Creating the Note: ${note.id}`);
|
||||||
|
|
||||||
|
|
|
@ -351,8 +351,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
throw new UnrecoverableError(`Refusing to create person without id: ${uri}`);
|
throw new UnrecoverableError(`Refusing to create person without id: ${uri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const personId = getApId(person);
|
const url = this.apUtilityService.findBestObjectUrl(person);
|
||||||
const url = this.apUtilityService.findSameAuthorityUrl(personId, person.url);
|
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
let user: MiRemoteUser | null = null;
|
let user: MiRemoteUser | null = null;
|
||||||
|
@ -561,8 +560,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
if (person.id == null) {
|
if (person.id == null) {
|
||||||
throw new UnrecoverableError(`Refusing to update person without id: ${uri}`);
|
throw new UnrecoverableError(`Refusing to update person without id: ${uri}`);
|
||||||
}
|
}
|
||||||
const personId = getApId(person);
|
|
||||||
const url = this.apUtilityService.findSameAuthorityUrl(personId, person.url);
|
const url = this.apUtilityService.findBestObjectUrl(person);
|
||||||
|
|
||||||
const updates = {
|
const updates = {
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
|
|
|
@ -95,83 +95,257 @@ describe(ApUtilityService, () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('findSameAuthorityUrl', () => {
|
describe('findBestObjectUrl', () => {
|
||||||
it('should return null when input is undefined', () => {
|
it('should return null when input is undefined', () => {
|
||||||
const result = serviceUnderTest.findSameAuthorityUrl('https://example.com', undefined);
|
const object = {
|
||||||
|
id: 'https://example.com',
|
||||||
|
url: undefined,
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null when input is empty array', () => {
|
it('should return null when input is empty array', () => {
|
||||||
const result = serviceUnderTest.findSameAuthorityUrl('https://example.com', []);
|
const object = {
|
||||||
|
id: 'https://example.com',
|
||||||
|
url: [] as string[],
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return return url if string input matches', () => {
|
it('should return return url if string input matches', () => {
|
||||||
const result = serviceUnderTest.findSameAuthorityUrl('https://example.com/1', 'https://example.com/2');
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: 'https://example.com/2',
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
expect(result).toBe('https://example.com/2');
|
expect(result).toBe('https://example.com/2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return return url if object input matches', () => {
|
it('should return return url if object input matches', () => {
|
||||||
const input = {
|
const object = {
|
||||||
href: 'https://example.com/2',
|
id: 'https://example.com/1',
|
||||||
|
url: {
|
||||||
|
href: 'https://example.com/2',
|
||||||
|
} as IObject,
|
||||||
} as IObject;
|
} as IObject;
|
||||||
|
|
||||||
const result = serviceUnderTest.findSameAuthorityUrl('https://example.com/1', input);
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
expect(result).toBe('https://example.com/2');
|
expect(result).toBe('https://example.com/2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return return url if string[] input matches', () => {
|
it('should return return url if string[] input matches', () => {
|
||||||
const result = serviceUnderTest.findSameAuthorityUrl('https://example.com/1', ['https://example.com/2']);
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: ['https://example.com/2'],
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
expect(result).toBe('https://example.com/2');
|
expect(result).toBe('https://example.com/2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return return url if object[] input matches', () => {
|
it('should return return url if object[] input matches', () => {
|
||||||
const input = {
|
const object = {
|
||||||
href: 'https://example.com/2',
|
id: 'https://example.com/1',
|
||||||
|
url: [{
|
||||||
|
href: 'https://example.com/2',
|
||||||
|
} as IObject],
|
||||||
} as IObject;
|
} as IObject;
|
||||||
|
|
||||||
const result = serviceUnderTest.findSameAuthorityUrl('https://example.com/1', [input]);
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
expect(result).toBe('https://example.com/2');
|
expect(result).toBe('https://example.com/2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip invalid entries', () => {
|
it('should skip invalid entries', () => {
|
||||||
const result = serviceUnderTest.findSameAuthorityUrl('https://example.com/1', [{} as IObject, 'https://example.com/2']);
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: [{} as IObject, 'https://example.com/2'],
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
expect(result).toBe('https://example.com/2');
|
expect(result).toBe('https://example.com/2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return first match', () => {
|
it('should allow empty mediaType', () => {
|
||||||
const result = serviceUnderTest.findSameAuthorityUrl('https://example.com/1', ['https://example.com/2', 'https://example.com/3']);
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: {
|
||||||
|
href: 'https://example.com/2',
|
||||||
|
} as IObject,
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow text/html mediaType', () => {
|
||||||
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: {
|
||||||
|
href: 'https://example.com/2',
|
||||||
|
mediaType: 'text/html',
|
||||||
|
} as IObject,
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow other text/ mediaTypes', () => {
|
||||||
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: {
|
||||||
|
href: 'https://example.com/2',
|
||||||
|
mediaType: 'text/imaginary',
|
||||||
|
} as IObject,
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow application/ld+json mediaType', () => {
|
||||||
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: {
|
||||||
|
href: 'https://example.com/2',
|
||||||
|
mediaType: 'application/ld+json;profile=https://www.w3.org/ns/activitystreams',
|
||||||
|
} as IObject,
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow application/activity+json mediaType', () => {
|
||||||
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: {
|
||||||
|
href: 'https://example.com/2',
|
||||||
|
mediaType: 'application/activity+json',
|
||||||
|
} as IObject,
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject other mediaTypes', () => {
|
||||||
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: [
|
||||||
|
{
|
||||||
|
href: 'https://example.com/2',
|
||||||
|
mediaType: 'application/json',
|
||||||
|
} as IObject,
|
||||||
|
{
|
||||||
|
href: 'https://example.com/3',
|
||||||
|
mediaType: 'image/jpeg',
|
||||||
|
} as IObject,
|
||||||
|
],
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return best match', () => {
|
||||||
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: [
|
||||||
|
'https://example.com/2',
|
||||||
|
{
|
||||||
|
href: 'https://example.com/3',
|
||||||
|
} as IObject,
|
||||||
|
{
|
||||||
|
href: 'https://example.com/4',
|
||||||
|
mediaType: 'text/html',
|
||||||
|
} as IObject,
|
||||||
|
{
|
||||||
|
href: 'https://example.com/5',
|
||||||
|
mediaType: 'text/plain',
|
||||||
|
} as IObject,
|
||||||
|
{
|
||||||
|
href: 'https://example.com/6',
|
||||||
|
mediaType: 'application/ld+json',
|
||||||
|
} as IObject,
|
||||||
|
{
|
||||||
|
href: 'https://example.com/7',
|
||||||
|
mediaType: 'application/activity+json',
|
||||||
|
} as IObject,
|
||||||
|
{
|
||||||
|
href: 'https://example.com/8',
|
||||||
|
mediaType: 'image/jpeg',
|
||||||
|
} as IObject,
|
||||||
|
],
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return first match in case of ties', () => {
|
||||||
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: ['https://example.com/2', 'https://example.com/3'],
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
expect(result).toBe('https://example.com/2');
|
expect(result).toBe('https://example.com/2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip invalid scheme', () => {
|
it('should skip invalid scheme', () => {
|
||||||
const result = serviceUnderTest.findSameAuthorityUrl('https://example.com/1', ['file://example.com/1', 'https://example.com/2']);
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: ['file://example.com/1', 'https://example.com/2'],
|
||||||
|
} as IObject;
|
||||||
|
|
||||||
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
|
|
||||||
expect(result).toBe('https://example.com/2');
|
expect(result).toBe('https://example.com/2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip HTTP in production', () => {
|
it('should skip HTTP in production', () => {
|
||||||
|
// noinspection HttpUrlsUsage
|
||||||
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: ['http://example.com/1', 'https://example.com/2'],
|
||||||
|
} as IObject;
|
||||||
env.NODE_ENV = 'production';
|
env.NODE_ENV = 'production';
|
||||||
|
|
||||||
// noinspection HttpUrlsUsage
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
const result = serviceUnderTest.findSameAuthorityUrl('https://example.com/1', ['http://example.com/1', 'https://example.com/2']);
|
|
||||||
|
|
||||||
expect(result).toBe('https://example.com/2');
|
expect(result).toBe('https://example.com/2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow HTTP in non-prod', () => {
|
it('should allow HTTP in non-prod', () => {
|
||||||
|
// noinspection HttpUrlsUsage
|
||||||
|
const object = {
|
||||||
|
id: 'https://example.com/1',
|
||||||
|
url: ['http://example.com/1', 'https://example.com/2'],
|
||||||
|
} as IObject;
|
||||||
env.NODE_ENV = 'test';
|
env.NODE_ENV = 'test';
|
||||||
|
|
||||||
// noinspection HttpUrlsUsage
|
const result = serviceUnderTest.findBestObjectUrl(object);
|
||||||
const result = serviceUnderTest.findSameAuthorityUrl('https://example.com/1', ['http://example.com/1', 'https://example.com/2']);
|
|
||||||
|
|
||||||
// noinspection HttpUrlsUsage
|
// noinspection HttpUrlsUsage
|
||||||
expect(result).toBe('http://example.com/1');
|
expect(result).toBe('http://example.com/1');
|
||||||
|
|
Loading…
Add table
Reference in a new issue