mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 14:05:48 +09:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1054dacd62 | ||
|
|
c51800a8d6 |
No files matched your search
@@ -20,6 +20,7 @@ import {HttpStatus, MimeType} from '@fluxer/constants/src/HttpConstants';
|
||||
import {DESKTOP_RUNTIME_DISCOVERY_UNREACHABLE_ERROR_NAME} from '@fluxer/desktop_ipc/src/LocalAppRuntimeContract';
|
||||
import {
|
||||
buildInstanceDiscoveryCandidates,
|
||||
InstanceDiscoveryOriginMismatchError,
|
||||
parseInstanceDiscoveryDocument,
|
||||
requireDiscoveryDeclaresItsOwnOrigin,
|
||||
} from '@fluxer/instance_bootstrap/src/Discovery';
|
||||
@@ -79,6 +80,15 @@ class DesktopRuntimeDiscoveryFailedError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
class DesktopRuntimeDiscoveryDeclaredElsewhereError extends DesktopRuntimeDiscoveryFailedError {
|
||||
public readonly declaredApiEndpoint: string;
|
||||
|
||||
public constructor(apiEndpoint: string, attempts: ReadonlyArray<DiscoveryAttempt>, declaredApiEndpoint: string) {
|
||||
super(apiEndpoint, attempts);
|
||||
this.declaredApiEndpoint = declaredApiEndpoint;
|
||||
}
|
||||
}
|
||||
|
||||
class DesktopRuntimeDiscoveryUnreachableError extends DesktopRuntimeDiscoveryFailedError {
|
||||
public constructor(apiEndpoint: string, attempts: ReadonlyArray<DiscoveryAttempt>) {
|
||||
super(apiEndpoint, attempts);
|
||||
@@ -98,9 +108,28 @@ export async function resolveDesktopRuntimePlan({
|
||||
input,
|
||||
signal,
|
||||
}: ResolveDesktopRuntimeConfigInput): Promise<LocalAppRuntimePlan> {
|
||||
try {
|
||||
return await resolveDesktopRuntimePlanAt(input, signal);
|
||||
} catch (error) {
|
||||
if (!(error instanceof DesktopRuntimeDiscoveryDeclaredElsewhereError)) {
|
||||
throw error;
|
||||
}
|
||||
log.info('Instance discovery declared another API origin, resolving that instance', {
|
||||
input,
|
||||
declaredApiEndpoint: error.declaredApiEndpoint,
|
||||
});
|
||||
return resolveDesktopRuntimePlanAt(error.declaredApiEndpoint, signal, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveDesktopRuntimePlanAt(
|
||||
input: string,
|
||||
signal: AbortSignal | null,
|
||||
followed = false,
|
||||
): Promise<LocalAppRuntimePlan> {
|
||||
const apiEndpoint = requireDesktopRuntimeAPIEndpoint(input);
|
||||
const anchorOrigin = requireDesktopHTTPOrigin(new URL(apiEndpoint).origin);
|
||||
const resolved = await fetchRuntimePlan(apiEndpoint, anchorOrigin, signal);
|
||||
const resolved = await fetchRuntimePlan(apiEndpoint, anchorOrigin, signal, followed);
|
||||
let anchorRequirement: DesktopAddressRequirement;
|
||||
try {
|
||||
anchorRequirement = await getDesktopOutboundHTTP().registerAnchoredOrigins({
|
||||
@@ -147,9 +176,11 @@ async function fetchRuntimePlan(
|
||||
apiEndpoint: string,
|
||||
anchorOrigin: string,
|
||||
signal: AbortSignal | null,
|
||||
followed: boolean,
|
||||
): Promise<ResolvedRuntimePlan> {
|
||||
const candidates = buildInstanceDiscoveryCandidates({input: apiEndpoint});
|
||||
const attempts: Array<DiscoveryAttempt> = [];
|
||||
let declaredApiEndpoint: string | null = null;
|
||||
let instanceUnreachable = candidates.length > 0;
|
||||
for (const url of candidates) {
|
||||
let document: unknown;
|
||||
@@ -165,10 +196,16 @@ async function fetchRuntimePlan(
|
||||
return {plan, served: {url, document}, rememberedAnchorRequirement: null, attempts};
|
||||
} catch (error) {
|
||||
instanceUnreachable = false;
|
||||
if (error instanceof InstanceDiscoveryOriginMismatchError) {
|
||||
declaredApiEndpoint ??= error.declared;
|
||||
}
|
||||
attempts.push({url, reason: error instanceof Error ? error.message : String(error)});
|
||||
}
|
||||
}
|
||||
if (!instanceUnreachable) {
|
||||
if (declaredApiEndpoint !== null && !followed) {
|
||||
throw new DesktopRuntimeDiscoveryDeclaredElsewhereError(apiEndpoint, attempts, declaredApiEndpoint);
|
||||
}
|
||||
throw new DesktopRuntimeDiscoveryFailedError(apiEndpoint, attempts);
|
||||
}
|
||||
const lastServed = await readLastServedDiscovery(apiEndpoint);
|
||||
|
||||
@@ -103,11 +103,45 @@ describe('desktop discovery for an account stored on the migrated official domai
|
||||
});
|
||||
}
|
||||
|
||||
test('another instance that serves a foreign API is still refused', async () => {
|
||||
network.served.set('https://chat.example.test', officialDocument('https://web.fluxer.app'));
|
||||
test('an alias domain that declares another API resolves the instance it declares', async () => {
|
||||
network.served.set('https://domain2.example.test', officialDocument('https://domain1.example.test'));
|
||||
network.served.set('https://domain1.example.test', officialDocument('https://domain1.example.test'));
|
||||
|
||||
await assert.rejects(resolveDesktopRuntimePlan({input: 'https://chat.example.test/api', signal: null}), {
|
||||
const plan = await resolveDesktopRuntimePlan({input: 'https://domain2.example.test/api', signal: null});
|
||||
|
||||
assert.equal(plan.instanceKey, 'https://domain1.example.test/api');
|
||||
assert.equal(plan.endpoints.apiEndpoint, 'https://domain1.example.test/api');
|
||||
assert.equal(network.anchored.length, 1);
|
||||
assert.equal(network.anchored[0].anchorOrigin, 'https://domain1.example.test');
|
||||
assert.deepEqual(
|
||||
network.fetched
|
||||
.filter((request) => new URL(request.url).origin === 'https://domain1.example.test')
|
||||
.map((request) => request.expectedOrigin),
|
||||
['https://domain1.example.test'],
|
||||
);
|
||||
});
|
||||
|
||||
test('the declared instance must declare itself, so a chain stops after one hop', async () => {
|
||||
network.served.set('https://a.example.test', officialDocument('https://b.example.test'));
|
||||
network.served.set('https://b.example.test', officialDocument('https://c.example.test'));
|
||||
network.served.set('https://c.example.test', officialDocument('https://c.example.test'));
|
||||
|
||||
await assert.rejects(resolveDesktopRuntimePlan({input: 'https://a.example.test/api', signal: null}), {
|
||||
name: 'DesktopRuntimeDiscoveryFailedError',
|
||||
});
|
||||
assert.equal(
|
||||
network.fetched.some((request) => new URL(request.url).origin === 'https://c.example.test'),
|
||||
false,
|
||||
);
|
||||
assert.equal(network.anchored.length, 0);
|
||||
});
|
||||
|
||||
test('a declared instance that serves no discovery is refused', async () => {
|
||||
network.served.set('https://chat.example.test', officialDocument('https://gone.example.test'));
|
||||
|
||||
await assert.rejects(resolveDesktopRuntimePlan({input: 'https://chat.example.test/api', signal: null}), {
|
||||
name: /DesktopRuntimeDiscovery/,
|
||||
});
|
||||
assert.equal(network.anchored.length, 0);
|
||||
});
|
||||
});
|
||||
@@ -98,6 +98,17 @@ describe('renderTrayIcon', () => {
|
||||
assert.deepEqual(pixel(bitmap, 32, 6, 16), [0xff, 0xff, 0xff, 0xff]);
|
||||
});
|
||||
|
||||
test('keeps the Linux monochrome pixmap within 256 pixels', () => {
|
||||
const result = renderTrayIcon(FakeNativeImage.fromBitmap(256), 'white', false, 'linux');
|
||||
const [representation] = result.representations.values();
|
||||
assert.equal(representation.width, 256);
|
||||
assert.equal(representation.height, 256);
|
||||
const bitmap = result.toBitmap();
|
||||
assert.deepEqual(pixel(bitmap, 256, 128, 50), [0xff, 0xff, 0xff, 0xff]);
|
||||
assert.deepEqual(pixel(bitmap, 256, 128, 128), [0, 0, 0, 0]);
|
||||
assert.deepEqual(pixel(bitmap, 256, 20, 128), [0, 0, 0, 0]);
|
||||
});
|
||||
|
||||
test('skips representations whose bitmap does not match their size', () => {
|
||||
const base = FakeNativeImage.fromBitmap(16);
|
||||
base.toBitmap = () => Buffer.alloc(3);
|
||||
|
||||
@@ -18,6 +18,7 @@ const MIN_GLYPH_CONTRAST = 32;
|
||||
const GLYPH_CUTOUT_START = 0.1;
|
||||
const GLYPH_CUTOUT_END = 0.5;
|
||||
const LINUX_MONOCHROME_CANVAS_RATIO = 22 / 16;
|
||||
const LINUX_MAX_PIXMAP_SIZE = 256;
|
||||
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
const DOT_BGR: Bgr = [0x00, 0x00, 0xdd];
|
||||
const TEMPLATE_DOT_BGR: Bgr = [0x00, 0x00, 0x00];
|
||||
@@ -149,6 +150,36 @@ function cropTrayGlyph(buffer: Buffer, width: number, height: number): TrayBitma
|
||||
return {buffer: target, width: size, height: size};
|
||||
}
|
||||
|
||||
function downscaleTrayBitmap(bitmap: TrayBitmap, size: number): TrayBitmap {
|
||||
if (bitmap.width <= size) return bitmap;
|
||||
const scale = bitmap.width / size;
|
||||
const buffer = Buffer.alloc(size * size * 4);
|
||||
const sums = [0, 0, 0, 0];
|
||||
for (let y = 0; y < size; y++) {
|
||||
const top = y * scale;
|
||||
const bottom = top + scale;
|
||||
for (let x = 0; x < size; x++) {
|
||||
const left = x * scale;
|
||||
const right = left + scale;
|
||||
sums.fill(0);
|
||||
for (let sy = Math.floor(top); sy < Math.min(bitmap.height, Math.ceil(bottom)); sy++) {
|
||||
const weightY = Math.min(bottom, sy + 1) - Math.max(top, sy);
|
||||
for (let sx = Math.floor(left); sx < Math.min(bitmap.width, Math.ceil(right)); sx++) {
|
||||
const weight = weightY * (Math.min(right, sx + 1) - Math.max(left, sx));
|
||||
const offset = (sy * bitmap.width + sx) * 4;
|
||||
for (let channel = 0; channel < 4; channel++) {
|
||||
sums[channel] += bitmap.buffer[offset + channel] * weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let channel = 0; channel < 4; channel++) {
|
||||
buffer[(y * size + x) * 4 + channel] = Math.round(sums[channel] / (scale * scale));
|
||||
}
|
||||
}
|
||||
}
|
||||
return {buffer, width: size, height: size};
|
||||
}
|
||||
|
||||
function addMonochromeTrayMargins(bitmap: TrayBitmap, size: number): TrayBitmap {
|
||||
const inset = Math.floor((size - bitmap.width) / 2);
|
||||
const buffer = Buffer.alloc(size * size * 4);
|
||||
@@ -210,7 +241,13 @@ export function renderTrayIcon(
|
||||
appearance === 'template' ? TEMPLATE_DOT_BGR : DOT_BGR,
|
||||
);
|
||||
}
|
||||
if (linuxMonochrome) bitmap = addMonochromeTrayMargins(bitmap, Math.ceil(logicalCanvasSize * scaleFactor));
|
||||
if (linuxMonochrome) {
|
||||
const canvasSize = Math.min(Math.ceil(logicalCanvasSize * scaleFactor), LINUX_MAX_PIXMAP_SIZE);
|
||||
bitmap = addMonochromeTrayMargins(
|
||||
downscaleTrayBitmap(bitmap, Math.floor(canvasSize / LINUX_MONOCHROME_CANVAS_RATIO)),
|
||||
canvasSize,
|
||||
);
|
||||
}
|
||||
result.addRepresentation({scaleFactor, ...bitmap});
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
|
||||
Reference in new issue
Block a user