Skip to content

Commit 40aff9f

Browse files
violet-shCherry
andauthored
Allow fetching by texture IDs (#42)
Co-authored-by: James Ross <james@jross.me>
1 parent 1e7c01d commit 40aff9f

File tree

3 files changed

+76
-11
lines changed

3 files changed

+76
-11
lines changed

src/worker/request.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export enum RequestedKind {
1616
export enum IdentityKind {
1717
Uuid,
1818
Username,
19+
TextureID,
1920
}
2021

2122
export enum TextureKind {
@@ -126,6 +127,8 @@ export function interpretRequest(request: Request): CraftheadRequest | null {
126127
} else if (identity.length === 36) {
127128
identity = identity.replaceAll('-', '');
128129
identityType = IdentityKind.Uuid;
130+
} else if (identity.length === 64) {
131+
identityType = IdentityKind.TextureID;
129132
} else {
130133
return null;
131134
}

src/worker/services/mojang/service.ts

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export default class MojangRequestService {
4747
* @param gatherer any promise gatherer
4848
*/
4949
async normalizeRequest(request: CraftheadRequest, gatherer: PromiseGatherer): Promise<CraftheadRequest> {
50-
if (request.identityType === IdentityKind.Uuid) {
50+
if (request.identityType === IdentityKind.Uuid || request.identityType === IdentityKind.TextureID) {
5151
return request;
5252
}
5353

@@ -67,22 +67,17 @@ export default class MojangRequestService {
6767
* Fetches a texture directly from the Mojang servers. Assumes the request has been normalized already.
6868
*/
6969
private async retrieveTextureDirect(request: CraftheadRequest, gatherer: PromiseGatherer, kind: TextureKind): Promise<Response> {
70+
if (request.identityType === IdentityKind.TextureID) {
71+
const textureResponse = await MojangRequestService.fetchTextureFromId(request.identity);
72+
return MojangRequestService.constructTextureResponse(textureResponse, request);
73+
}
7074
const rawUuid = fromHex(request.identity);
7175
if (uuidVersion(rawUuid) === 4) {
7276
const lookup = await this.mojangApi.fetchProfile(request.identity, gatherer);
7377
if (lookup.result) {
7478
const textureResponse = await MojangRequestService.fetchTextureFromProfile(lookup.result, kind);
7579
if (textureResponse) {
76-
const buff = await textureResponse.texture.arrayBuffer();
77-
if (buff && buff.byteLength > 0) {
78-
return new Response(buff, {
79-
status: 200,
80-
headers: {
81-
'X-Crafthead-Profile-Cache-Hit': lookup.source,
82-
'X-Crafthead-Skin-Model': request.model || textureResponse.model || 'default',
83-
},
84-
});
85-
}
80+
return MojangRequestService.constructTextureResponse(textureResponse, request, lookup.source);
8681
}
8782
return new Response(STEVE_SKIN, {
8883
status: 404,
@@ -110,6 +105,26 @@ export default class MojangRequestService {
110105
});
111106
}
112107

108+
private static async constructTextureResponse(textureResponse: TextureResponse, request: CraftheadRequest, source?: string): Promise<Response> {
109+
const buff = await textureResponse.texture.arrayBuffer();
110+
if (buff && buff.byteLength > 0) {
111+
return new Response(buff, {
112+
status: 200,
113+
headers: {
114+
'X-Crafthead-Profile-Cache-Hit': source || 'miss',
115+
'X-Crafthead-Skin-Model': request.model || textureResponse.model || 'default',
116+
},
117+
});
118+
}
119+
return new Response(STEVE_SKIN, {
120+
status: 404,
121+
headers: {
122+
'X-Crafthead-Profile-Cache-Hit': 'not-found',
123+
'X-Crafthead-Skin-Model': 'default',
124+
},
125+
});
126+
}
127+
113128
async retrieveSkin(request: CraftheadRequest, gatherer: PromiseGatherer): Promise<Response> {
114129
if (request.identity === 'char' || request.identity === 'MHF_Steve') {
115130
// These are special-cased by Minotar.
@@ -174,6 +189,29 @@ export default class MojangRequestService {
174189
return undefined;
175190
}
176191

192+
private static async fetchTextureFromId(id: string): Promise<TextureResponse> {
193+
const url = `https://textures.minecraft.net/texture/${id}`;
194+
return this.fetchTextureFromUrl(url);
195+
}
196+
197+
private static async fetchTextureFromUrl(textureUrl: string): Promise<TextureResponse> {
198+
const textureResponse = await fetch(textureUrl, {
199+
cf: {
200+
cacheEverything: true,
201+
cacheTtl: 86400,
202+
},
203+
headers: {
204+
'User-Agent': 'Crafthead (+https://crafthead.net)',
205+
},
206+
});
207+
if (!textureResponse.ok) {
208+
throw new Error(`Unable to retrieve texture from Mojang, http status ${textureResponse.status}`);
209+
}
210+
211+
//console.log('Successfully retrieved texture');
212+
return { texture: textureResponse, model: 'default' };
213+
}
214+
177215
async fetchProfile(request: CraftheadRequest, gatherer: PromiseGatherer): Promise<CacheComputeResult<MojangProfile | null>> {
178216
const normalized = await this.normalizeRequest(request, gatherer);
179217
if (!normalized.identity || uuidVersion(fromHex(normalized.identity)) === 3) {

test/worker.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,30 @@ describe('worker', () => {
142142
expect(await response.headers.get('content-type')).toContain('image/png');
143143
});
144144

145+
it('responds with image for avatar on texture ID', async () => {
146+
const request = new IncomingRequest('http://crafthead.net/avatar/9d2e80355eed693e3f0485893ef04ff6a507f3aab33f2bedb48cef56e30f67d0');
147+
const ctx = createExecutionContext();
148+
const response = await worker.fetch(request, env, ctx);
149+
await waitOnExecutionContext(ctx);
150+
expect(await response.headers.get('content-type')).toContain('image/png');
151+
});
152+
153+
it('responds with a matching avatar image by UUID, username, and texture ID', async () => {
154+
const request1 = new IncomingRequest('http://crafthead.net/avatar/ef6134805b6244e4a4467fbe85d65513');
155+
const request2 = new IncomingRequest('http://crafthead.net/avatar/CherryJimbo');
156+
const request3 = new IncomingRequest('http://crafthead.net/avatar/9d2e80355eed693e3f0485893ef04ff6a507f3aab33f2bedb48cef56e30f67d0');
157+
const ctx = createExecutionContext();
158+
const response1 = await worker.fetch(request1, env, ctx);
159+
const response2 = await worker.fetch(request2, env, ctx);
160+
const response3 = await worker.fetch(request3, env, ctx);
161+
await waitOnExecutionContext(ctx);
162+
const image1 = await response1.arrayBuffer();
163+
const image2 = await response2.arrayBuffer();
164+
const image3 = await response3.arrayBuffer();
165+
expect(image1).toStrictEqual(image2);
166+
expect(image2).toStrictEqual(image3);
167+
});
168+
145169
type ProfileResponse = {
146170
id: string;
147171
name: string;

0 commit comments

Comments
 (0)