Merge fba4fb9945
into d16db7f311
This commit is contained in:
commit
3d7f9465d3
|
@ -0,0 +1,55 @@
|
||||||
|
import { strictEqual, rejects } from 'node:assert';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { createAccount, resolveRemoteUser, sleep, type LoginUser } from '../../utils.js';
|
||||||
|
|
||||||
|
describe('API ap/show', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User resolution', () => {
|
||||||
|
test('resolve by canonical user URL (https://b.test/users/:id)', async () => {
|
||||||
|
const res = await alice.client.request('ap/show', { uri: `https://b.test/users/${bob.id}` });
|
||||||
|
strictEqual(res.type, 'User');
|
||||||
|
strictEqual(res.object.uri, `https://b.test/users/${bob.id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolve by user profile URL (https://b.test/@bob)', async () => {
|
||||||
|
const res = await alice.client.request('ap/show', { uri: `https://b.test/@${bob.username}` });
|
||||||
|
strictEqual(res.type, 'User');
|
||||||
|
strictEqual(res.object.uri, `https://b.test/users/${bob.id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolve local user by local profile url', async () => {
|
||||||
|
const res = await alice.client.request('ap/show', { uri: `https://a.test/@${alice.username}` });
|
||||||
|
strictEqual(res.type, 'User');
|
||||||
|
strictEqual(res.object.id, alice.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolve remote user by local profile URL (https://a.test/@bob@b.test)', async () => {
|
||||||
|
const res = await alice.client.request('ap/show', { uri: `https://a.test/@${bob.username}@b.test` });
|
||||||
|
strictEqual(res.type, 'User');
|
||||||
|
strictEqual(res.object.uri, `https://b.test/users/${bob.id}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Note resolution', () => {
|
||||||
|
test('resolve by note URL (https://b.test/notes/:id)', async () => {
|
||||||
|
const note = (await bob.client.request('notes/create', { text: 'hello from Bob' })).createdNote;
|
||||||
|
// 伝搬待ち
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const res = await alice.client.request('ap/show', { uri: `https://b.test/notes/${note.id}` });
|
||||||
|
strictEqual(res.type, 'Note');
|
||||||
|
strictEqual(res.object.uri, `https://b.test/notes/${note.id}`);
|
||||||
|
// 投稿者が a.test 側で解決済みの Bob になること
|
||||||
|
strictEqual(res.object.user.username, bob.username);
|
||||||
|
strictEqual(res.object.user.host, 'b.test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -516,7 +516,7 @@ describe('User', () => {
|
||||||
await rejects(
|
await rejects(
|
||||||
async () => await resolveRemoteUser('a.test', alice.id, bob),
|
async () => await resolveRemoteUser('a.test', alice.id, bob),
|
||||||
(err: any) => {
|
(err: any) => {
|
||||||
strictEqual(err.code, 'INTERNAL_ERROR');
|
strictEqual(err.code, 'SOMETHING_HAPPENED_IN_FETCHING_URI');
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -551,7 +551,7 @@ describe('User', () => {
|
||||||
await rejects(
|
await rejects(
|
||||||
async () => await resolveRemoteUser('a.test', alice.id, bob),
|
async () => await resolveRemoteUser('a.test', alice.id, bob),
|
||||||
(err: any) => {
|
(err: any) => {
|
||||||
strictEqual(err.code, 'INTERNAL_ERROR');
|
strictEqual(err.code, 'SOMETHING_HAPPENED_IN_FETCHING_URI');
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,9 +23,13 @@ export type Acct = {
|
||||||
|
|
||||||
declare namespace acct {
|
declare namespace acct {
|
||||||
export {
|
export {
|
||||||
|
correctAcct,
|
||||||
parse,
|
parse,
|
||||||
|
parseUrl,
|
||||||
|
parseAcctOrUrl,
|
||||||
toString_2 as toString,
|
toString_2 as toString,
|
||||||
Acct
|
Acct,
|
||||||
|
UrlIsNotAcctLikeError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export { acct }
|
export { acct }
|
||||||
|
@ -1229,6 +1233,9 @@ type ClipsUpdateRequest = operations['clips___update']['requestBody']['content']
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type ClipsUpdateResponse = operations['clips___update']['responses']['200']['content']['application/json'];
|
type ClipsUpdateResponse = operations['clips___update']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
function correctAcct(acct: Acct, localHostname?: string): Acct;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type DateString = string;
|
type DateString = string;
|
||||||
|
|
||||||
|
@ -3261,7 +3268,13 @@ type PagesUnlikeRequest = operations['pages___unlike']['requestBody']['content']
|
||||||
type PagesUpdateRequest = operations['pages___update']['requestBody']['content']['application/json'];
|
type PagesUpdateRequest = operations['pages___update']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
function parse(_acct: string): Acct;
|
function parse(acct: string): Acct;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
function parseAcctOrUrl(acct: string, localHostname?: string): Acct;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
function parseUrl(str: string): Acct;
|
||||||
|
|
||||||
// Warning: (ae-forgotten-export) The symbol "Values" needs to be exported by the entry point index.d.ts
|
// Warning: (ae-forgotten-export) The symbol "Values" needs to be exported by the entry point index.d.ts
|
||||||
//
|
//
|
||||||
|
@ -3628,6 +3641,11 @@ type TestResponse = operations['test']['responses']['200']['content']['applicati
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
function toString_2(acct: Acct): string;
|
function toString_2(acct: Acct): string;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
class UrlIsNotAcctLikeError extends Error {
|
||||||
|
constructor();
|
||||||
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type User = components['schemas']['User'];
|
type User = components['schemas']['User'];
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,77 @@ export type Acct = {
|
||||||
host: string | null;
|
host: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parse(_acct: string): Acct {
|
export function correctAcct(acct: Acct, localHostname?: string): Acct {
|
||||||
let acct = _acct;
|
const result = { ...acct };
|
||||||
if (acct.startsWith('@')) acct = acct.substring(1);
|
if (!acct.host) {
|
||||||
const split = acct.split('@', 2);
|
result.host = null;
|
||||||
|
} else if (localHostname && acct.host === localHostname) {
|
||||||
|
result.host = null;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parse(acct: string): Acct {
|
||||||
|
let acctWithNoPrefix = acct;
|
||||||
|
|
||||||
|
if (acct.startsWith('@')) {
|
||||||
|
acctWithNoPrefix = acct.substring(1);
|
||||||
|
} else if (acct.startsWith('acct:')) {
|
||||||
|
acctWithNoPrefix = acct.substring(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
const split = acctWithNoPrefix.split('@', 2);
|
||||||
|
|
||||||
return { username: split[0], host: split[1] || null };
|
return { username: split[0], host: split[1] || null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UrlIsNotAcctLikeError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('This url is not acct like.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only supports `https?://example.com/@username` or `https?://example.com/@username@other.example.com`
|
||||||
|
*/
|
||||||
|
export function parseUrl(str: string): Acct {
|
||||||
|
const url = new URL(str);
|
||||||
|
|
||||||
|
if (url.hash.length > 0) throw new UrlIsNotAcctLikeError();
|
||||||
|
if (url.search.length > 0) throw new UrlIsNotAcctLikeError();
|
||||||
|
|
||||||
|
const splited = url.pathname.split('/');
|
||||||
|
const path = splited[1];
|
||||||
|
|
||||||
|
if (!path) throw new UrlIsNotAcctLikeError();
|
||||||
|
if (!path.startsWith('@')) throw new UrlIsNotAcctLikeError();
|
||||||
|
if (path.length <= 1) throw new UrlIsNotAcctLikeError();
|
||||||
|
|
||||||
|
const split = path.split('@', 3); // ['', 'username', 'other.example.com']
|
||||||
|
|
||||||
|
return { username: split[1], host: split[2] || url.hostname };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a string and returns an Acct object.
|
||||||
|
* @param acct String to parse
|
||||||
|
* The string should be in one of the following formats:
|
||||||
|
* * At-mark style: `@username@example.com`
|
||||||
|
* * Starts with `acct:`: `acct:username@example.com`
|
||||||
|
* * URL style: `https://example.com/@username`, `https://self.example.com/@username@other.example.com`
|
||||||
|
* @param localHostname If provided and matches the host found in acct, the returned `host` will be set to `null`.
|
||||||
|
* @returns Acct object
|
||||||
|
*/
|
||||||
|
export function parseAcctOrUrl(acct: string, localHostname?: string): Acct {
|
||||||
|
if (acct.startsWith('https://') || acct.startsWith('http://')) {
|
||||||
|
// url style
|
||||||
|
return correctAcct(parseUrl(acct), localHostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
// acct style
|
||||||
|
return correctAcct(parse(acct), localHostname);
|
||||||
|
}
|
||||||
|
|
||||||
export function toString(acct: Acct): string {
|
export function toString(acct: Acct): string {
|
||||||
return acct.host == null ? acct.username : `${acct.username}@${acct.host}`;
|
return acct.host == null ? acct.username : `${acct.username}@${acct.host}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13554,6 +13554,7 @@ export interface operations {
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
'application/json': {
|
||||||
uri: string;
|
uri: string;
|
||||||
|
onlyUriFetch?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import * as acct from '../src/acct.js';
|
||||||
|
|
||||||
|
function testParseAcct(fn: (acct: string) => acct.Acct) {
|
||||||
|
it('parses plain username', () => {
|
||||||
|
const res = fn('alice');
|
||||||
|
expect(res).toEqual({ username: 'alice', host: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses at-mark style without host', () => {
|
||||||
|
const res = fn('@alice');
|
||||||
|
expect(res).toEqual({ username: 'alice', host: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses at-mark style with host', () => {
|
||||||
|
const res = fn('@alice@example.com');
|
||||||
|
expect(res).toEqual({ username: 'alice', host: 'example.com' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses acct: style', () => {
|
||||||
|
const res = fn('acct:alice@example.com');
|
||||||
|
expect(res).toEqual({ username: 'alice', host: 'example.com' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parse Mr.http', () => {
|
||||||
|
const res = fn('http');
|
||||||
|
expect(res).toEqual({ username: 'http', host: null });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testParseUrl(fn: (acct: string) => acct.Acct) {
|
||||||
|
it('parses url style https with same host -> host kept when localHostname not provided', () => {
|
||||||
|
const res = fn('https://example.com/@alice');
|
||||||
|
expect(res).toEqual({ username: 'alice', host: 'example.com' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses url style with remote host contained in path', () => {
|
||||||
|
const res = fn('https://self.example.com/@alice@other.example.com');
|
||||||
|
expect(res).toEqual({ username: 'alice', host: 'other.example.com' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on non-acct-like url path (root)', () => {
|
||||||
|
expect(() => fn('https://example.com')).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on non-acct-like url path (users/alice)', () => {
|
||||||
|
expect(() => fn('https://example.com/users/alice')).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws ended with @', () => {
|
||||||
|
expect(() => fn('https://example.com/@')).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses url ended with /', () => {
|
||||||
|
const res = fn('https://example.com/@alice/');
|
||||||
|
expect(res).toEqual({ username: 'alice', host: 'example.com' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws url have @username path but ended with sub directory', () => {
|
||||||
|
expect(() => fn('https://example.com/@alice/subdir')).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws url with search params', () => {
|
||||||
|
expect(() => fn('https://example.com/@alice?foo=bar')).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws url with hash', () => {
|
||||||
|
expect(() => fn('https://example.com/@alice#fragment')).toThrowError();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('acct.parse', () => {
|
||||||
|
testParseAcct(acct.parse);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('acct.parseUrl', () => {
|
||||||
|
testParseUrl(acct.parseUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('acct.parseAcctOrUrl', () => {
|
||||||
|
testParseAcct(acct.parseAcctOrUrl);
|
||||||
|
testParseUrl(acct.parseAcctOrUrl);
|
||||||
|
|
||||||
|
it('parse url with localHostname', () => {
|
||||||
|
const res = acct.parseAcctOrUrl('https://example.com/@alice', 'example.com');
|
||||||
|
expect(res).toEqual({ username: 'alice', host: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parse @username with localHostname', () => {
|
||||||
|
const res = acct.parseAcctOrUrl('@alice', 'example.com');
|
||||||
|
expect(res).toEqual({ username: 'alice', host: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('acct.correctAcct', () => {
|
||||||
|
it('returns host=null when acct.host is null', () => {
|
||||||
|
const input: acct.Acct = { username: 'alice', host: null };
|
||||||
|
const out = acct.correctAcct(input);
|
||||||
|
expect(out).toEqual({ username: 'alice', host: null });
|
||||||
|
expect(out).not.toBe(input); // immutability
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps host when localHostname not provided', () => {
|
||||||
|
const input: acct.Acct = { username: 'bob', host: 'example.com' };
|
||||||
|
const out = acct.correctAcct(input);
|
||||||
|
expect(out).toEqual({ username: 'bob', host: 'example.com' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nulls host when it matches localHostname', () => {
|
||||||
|
const input: acct.Acct = { username: 'carol', host: 'example.com' };
|
||||||
|
const out = acct.correctAcct(input, 'example.com');
|
||||||
|
expect(out).toEqual({ username: 'carol', host: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps host when it differs from localHostname', () => {
|
||||||
|
const input: acct.Acct = { username: 'dave', host: 'other.example.com' };
|
||||||
|
const out = acct.correctAcct(input, 'example.com');
|
||||||
|
expect(out).toEqual({ username: 'dave', host: 'other.example.com' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('acct.toString', () => {
|
||||||
|
it('returns username when host is null', () => {
|
||||||
|
expect(acct.toString({ username: 'alice', host: null })).toBe('alice');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns username@host when host exists', () => {
|
||||||
|
expect(acct.toString({ username: 'alice', host: 'example.com' })).toBe('alice@example.com');
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue