Skip to content
This repository was archived by the owner on Jul 28, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import dotenv from 'dotenv'
dotenv.config();
import { patchIdentity } from "./providers/discord";
import { createWidget, patchIdentity } from "./providers/discord";
import { fetchOsuProfile } from "./providers/osu";
import { toIdentity } from "./types/osuProfile";

Expand Down
301 changes: 297 additions & 4 deletions src/providers/discord.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,313 @@
import axios from "axios";

const widgetData = {
display_name: "osu! Stats",
surfaces: {
widget_bottom: {
layout: "widget_bottom_stats",
components: {
stat_5: {
fields: {
value: {
value_type: "data",
presentation_type: "text",
value: "osu_top_play",
},
label: {
value_type: "custom_string",
presentation_type: "text",
value: "Top Play pp",
},
},
},
stat_6: {
fields: {
value: {
value_type: "data",
presentation_type: "text",
value: "osu_playtime",
},
label: {
value_type: "custom_string",
presentation_type: "text",
value: "Playtime",
},
},
},
stat_3: {
fields: {
value: {
value_type: "data",
presentation_type: "text",
value: "osu_country_placement",
},
label: {
value_type: "custom_string",
presentation_type: "text",
value: "Country Placement",
},
},
},
stat_2: {
fields: {
value: {
value_type: "data",
presentation_type: "text",
value: "osu_global_placement",
},
label: {
value_type: "custom_string",
presentation_type: "text",
value: "Global Placement",
},
},
},
stat_4: {
fields: {
value: {
value_type: "data",
presentation_type: "text",
value: "osu_overall_pp",
},
label: {
value_type: "custom_string",
presentation_type: "text",
value: "Overall pp",
},
},
},
stat_1: {
fields: {
value: {
value_type: "data",
presentation_type: "text",
value: "osu_country",
},
icon: {
value_type: "data",
presentation_type: "image",
value: "osu_flag",
},
label: {
value_type: "custom_string",
presentation_type: "text",
value: "Country",
},
},
},
},
},
add_widget_preview: {
layout: "add_widget_preview_contained",
components: {
contained_image: {
fields: {
image: {
value_type: "data",
presentation_type: "image",
value: "osu_profile",
},
},
},
},
},
widget_top: {
layout: "widget_top_contained",
components: {
subtitle_2: {
fields: {
text: {
value_type: "data",
presentation_type: "text",
value: "osu_is_supporter",
},
label: {
value_type: "custom_string",
presentation_type: "text",
value: "Supporter",
},
},
},
title: {
fields: {
text: {
value_type: "data",
presentation_type: "text",
value: "osu_name",
},
},
},
subtitle_1: {
fields: {
text: {
presentation_type: "text",
value_type: "data",
value: "osu_fav_map",
fallback: {
value_type: "custom_string",
presentation_type: "text",
value: "None",
},
},
label: {
value_type: "custom_string",
presentation_type: "text",
value: "Favorite Map",
},
},
},
subtitle_3: {
fields: {
text: {
value_type: "data",
presentation_type: "number",
value: "osu_playcount",
},
label: {
value_type: "custom_string",
presentation_type: "text",
value: "Playcount",
},
},
},
contained_image: {
fields: {
image: {
value_type: "data",
presentation_type: "image",
value: "osu_profile",
},
},
},
},
},
mini_profile: {
layout: "mini_profile_contained_stat",
components: {
stat: {
fields: {
text: {
value_type: "data",
presentation_type: "text",
value: "osu_name",
},
label: {
value_type: "data",
presentation_type: "text",
value: "osu_global_placement",
},
},
},
contained_image: {
fields: {
image: {
value_type: "data",
presentation_type: "image",
value: "osu_profile",
},
},
},
},
},
activity_accessory: {
layout: "activity_accessory_stat",
components: {
stat: {
fields: {
text: {
value_type: "custom_string",
presentation_type: "text",
value: "Check out my stats",
},
},
},
},
},
},
};

const discordApplicationId = process.env.APP_ID!;
const discordAPI = axios.create({
baseURL: "https://discord.com/api/v10",
headers: {
"Authorization": "Bot " + process.env.DISCORD_TOKEN!
}
Authorization: "Bot " + process.env.DISCORD_TOKEN!,
},
});
Comment on lines 230 to 235

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

No timeout configured on discordAPI client.

A stalled network call here will hang indefinitely since no timeout is set, which is risky for a script expected to run to completion (e.g. in CI/deploy pipelines).

🕒 Suggested fix
 const discordAPI = axios.create({
   baseURL: "https://discord.com/api/v10",
+  timeout: 10000,
   headers: {
     Authorization: "Bot " + process.env.DISCORD_TOKEN!,
   },
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const discordAPI = axios.create({
baseURL: "https://discord.com/api/v10",
headers: {
"Authorization": "Bot " + process.env.DISCORD_TOKEN!
}
Authorization: "Bot " + process.env.DISCORD_TOKEN!,
},
});
const discordAPI = axios.create({
baseURL: "https://discord.com/api/v10",
timeout: 10000,
headers: {
Authorization: "Bot " + process.env.DISCORD_TOKEN!,
},
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/discord.ts` around lines 230 - 235, The discordAPI axios client
is missing a request timeout, so calls can hang indefinitely; add a timeout to
the axios.create configuration in discordAPI so the client fails fast instead of
blocking completion. Update the existing client setup near discordAPI to include
an appropriate timeout value alongside baseURL and headers, keeping the change
localized to the discordAPI initializer.


export async function createWidget() {
let widgetConfigId = await isExist();

try {
if (widgetConfigId) {
console.log(`Updating existing widget config: ${widgetConfigId}`);
await discordAPI.patch(
`/applications/${discordApplicationId}/widget-configs/${widgetConfigId}`,
widgetData,
);
} else {
console.log("No existing config found. Creating a new one...");
const createResponse = await discordAPI.post(
`/applications/${discordApplicationId}/widget-configs`,
widgetData,
);
widgetConfigId = createResponse.data.id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the exact JSON response shape (field names) for Discord's POST /applications/{application.id}/widget-configs and GET /applications/{application.id}/widget-configs endpoints?

💡 Result:

The endpoints POST /applications/{application.id}/widget-configs and GET /applications/{application.id}/widget-configs relate to the management of application-specific widget configurations, a feature distinct from legacy Guild Widgets [1]. According to documentation for this resource, the widget configuration object uses the following primary fields [1]: - display_name (string): The display name of the widget configuration. - surfaces (map): A collection of widget surfaces, mapped by surface type [1]. When creating a new configuration via POST, the request body accepts the display_name and surfaces fields [1]. The response for both the creation (POST) and retrieval (GET) of these configurations returns the widget configuration object, which typically includes the above fields along with a unique identifier (often config_id) [1][2]. Note: These endpoints are distinct from Guild Widget settings, which involve fields like channel_id and enabled [3][4][5][6]. The application widget config feature is specifically designed for application-level integrations [1].

Citations:


🏁 Script executed:

sed -n '220,290p' src/providers/discord.ts

Repository: DEVKaxtusik/osu-widget

Length of output: 2109


🏁 Script executed:

rg -n "widget-configs|config_id|createResponse\.data\.id|configs\[0\]\.id" src README.md .github docs

Repository: DEVKaxtusik/osu-widget

Length of output: 793


🌐 Web query:

Discord application widget-configs POST GET response schema config_id id widget-configs

💡 Result:

In the context of the Discord API, widget-configs refer to configurations for application widgets displayed on user profiles [1]. These are managed via endpoints associated with an application [1]. Widget Config Object Structure A widget config object typically contains the following fields [1]: - application_id (snowflake): The ID of the application [1]. - config_id (snowflake): The unique ID of the widget configuration [1]. - display_name (string): The user-defined name for the configuration [1]. - surfaces (map): A mapping of surface types (e.g., widget_top, widget_bottom) to their respective layouts and components [2][1]. - status (string): The current status of the configuration [1]. - resolved_assets (array): Optional resolved assets for the widget [1]. - published_at (ISO8601 timestamp): The time the configuration was published [1]. - updated_at (ISO8601 timestamp): The time the configuration was last updated [1]. API Endpoints - Create Application Widget Config: POST /applications/{application.id}/widget-configs [1]. This endpoint requires a payload specifying the display_name and surfaces [1]. - List Application Widget Configs: GET /applications/{application.id}/widget-configs [1]. - Get Developer Application Widget Configs: GET /widget-configs/developer [1]. This allows developers to retrieve configs for applications they have access to [1]. Note: These endpoints are generally considered internal or experimental and are not documented in the official public Discord Developer Portal [3][4][5]. They have been observed through community research and unofficial documentation [1][6]. Developers working with these should be aware that they are subject to change and may require specific experiments or flags to function [6].

Citations:


🌐 Web query:

"POST /applications/{application.id}/widget-configs" "GET /applications/{application.id}/widget-configs" config_id response

💡 Result:

The API pattern POST /applications/{application.id}/widget-configs and GET /applications/{application.id}/widget-configs is commonly used to manage widget configurations for an application [1]. In this architectural pattern: 1. POST /applications/{application.id}/widget-configs: This endpoint is used to create a new widget configuration [1]. Upon successful creation, the API typically returns the newly created widget configuration object in the response body [1]. This response object contains a unique identifier, often referred to as config_id, which can be used for subsequent operations such as publishing or updating the configuration [1][2]. 2. GET /applications/{application.id}/widget-configs: This endpoint is used to retrieve a list of all widget configurations associated with the specified application [1]. The response typically returns an array of widget configuration objects, each containing its respective config_id [1]. While the specific naming conventions and fields (such as config_id versus id) can vary depending on the implementation and service provider, the pattern of returning the created configuration record—including its unique identifier—within the POST response is a standard RESTful practice for resource management [1][2][3]. Developers should refer to the specific API documentation for their service to confirm whether the field is explicitly named config_id or simply id [1][4][5].

Citations:


Use config_id instead of id for widget config IDs.
createResponse.data.id and configs[0].id don’t match the widget-config response shape; if the API returns config_id, widgetConfigId stays empty, isExist() never finds an existing config, and publishWidget() can be called with undefined.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/discord.ts` at line 253, The widget config ID handling in the
Discord provider is using the wrong response field, which can leave
widgetConfigId unset and break lookups/publishing. Update the logic in the
create/fetch flow around widgetConfigId, createResponse.data, configs[0],
isExist(), and publishWidget() to read and compare config_id instead of id
wherever the widget-config API response shape is consumed.

}

await publishWidget(widgetConfigId!);
console.log(`Successfully published widget configuration: ${widgetConfigId}`);
} catch (error: any) {
console.error("Error setting up/publishing widget: ", error?.response?.data || error.message);
}
}
Comment on lines +237 to +261

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

createWidget() swallows all failures without signaling failure to the caller.

The catch block only logs and never rethrows or exits non-zero. Since this is invoked from setup.ts as a standalone setup script with no additional error handling, a total failure (bad token, network error, API rejection) will still make the process exit successfully (code 0), silently hiding the failure from any CI/deploy pipeline that checks the exit code.

🚨 Suggested fix
   } catch (error: any) {
     console.error("Error setting up/publishing widget: ", error?.response?.data || error.message);
+    throw error;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function createWidget() {
let widgetConfigId = await isExist();
try {
if (widgetConfigId) {
console.log(`Updating existing widget config: ${widgetConfigId}`);
await discordAPI.patch(
`/applications/${discordApplicationId}/widget-configs/${widgetConfigId}`,
widgetData,
);
} else {
console.log("No existing config found. Creating a new one...");
const createResponse = await discordAPI.post(
`/applications/${discordApplicationId}/widget-configs`,
widgetData,
);
widgetConfigId = createResponse.data.id;
}
await publishWidget(widgetConfigId!);
console.log(`Successfully published widget configuration: ${widgetConfigId}`);
} catch (error: any) {
console.error("Error setting up/publishing widget: ", error?.response?.data || error.message);
}
}
export async function createWidget() {
let widgetConfigId = await isExist();
try {
if (widgetConfigId) {
console.log(`Updating existing widget config: ${widgetConfigId}`);
await discordAPI.patch(
`/applications/${discordApplicationId}/widget-configs/${widgetConfigId}`,
widgetData,
);
} else {
console.log("No existing config found. Creating a new one...");
const createResponse = await discordAPI.post(
`/applications/${discordApplicationId}/widget-configs`,
widgetData,
);
widgetConfigId = createResponse.data.id;
}
await publishWidget(widgetConfigId!);
console.log(`Successfully published widget configuration: ${widgetConfigId}`);
} catch (error: any) {
console.error("Error setting up/publishing widget: ", error?.response?.data || error.message);
throw error;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/discord.ts` around lines 237 - 261, The createWidget() flow in
src/providers/discord.ts swallows setup failures by only logging inside the
catch block, so the caller never sees the error and the standalone setup script
can still exit successfully. Update createWidget() to propagate failures after
logging by rethrowing the caught error (or otherwise terminating with a non-zero
exit) so setup.ts can fail the process on bad token, network, or API errors; use
createWidget(), publishWidget(), and the existing discordAPI calls as the key
points to keep behavior intact while making failures visible.


async function isExist(): Promise<string | null> {
try {
const response = await discordAPI.get(
`/applications/${discordApplicationId}/widget-configs`,
);

const configs = response.data;

if (configs && configs.length > 0) {
return configs[0].id;
}

return null;
} catch (error: any) {
if (error.response) {
console.error(`ERROR Fetching widget configs:`, JSON.stringify(error.response.data));
}
return null;
}
}
Comment on lines +263 to +282

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

isExist() conflates "no config exists" with "fetch failed".

Any error without a .response property (network error, timeout, DNS failure) is silently swallowed with no logging at all, and every error path returns null — the same value used to mean "no widget config exists yet". This makes createWidget() unable to distinguish a real absence of config from a transient failure, risking creation of a duplicate widget config (via POST) on every retry after a network blip, instead of updating the existing one.

🛠️ Suggested fix
   } catch (error: any) {
-    if (error.response) {
-      console.error(`ERROR Fetching widget configs:`, JSON.stringify(error.response.data));
-    }
-    return null;
+    console.error(`ERROR Fetching widget configs:`, error.response ? JSON.stringify(error.response.data) : error.message);
+    throw error;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function isExist(): Promise<string | null> {
try {
const response = await discordAPI.get(
`/applications/${discordApplicationId}/widget-configs`,
);
const configs = response.data;
if (configs && configs.length > 0) {
return configs[0].id;
}
return null;
} catch (error: any) {
if (error.response) {
console.error(`ERROR Fetching widget configs:`, JSON.stringify(error.response.data));
}
return null;
}
}
async function isExist(): Promise<string | null> {
try {
const response = await discordAPI.get(
`/applications/${discordApplicationId}/widget-configs`,
);
const configs = response.data;
if (configs && configs.length > 0) {
return configs[0].id;
}
return null;
} catch (error: any) {
console.error(
`ERROR Fetching widget configs:`,
error.response ? JSON.stringify(error.response.data) : error.message,
);
throw error;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/discord.ts` around lines 263 - 282, The isExist() helper
currently returns null for both “no widget config exists” and actual fetch
failures, so createWidget() can’t tell whether to create or update. Update
isExist() to distinguish these cases by logging all caught errors (including
non-response/network failures) and either rethrowing or returning a separate
failure signal, while preserving null only for the true “no configs found” path.
Then adjust createWidget() to handle that distinct failure path instead of
treating it as an empty result.


async function publishWidget(configId: string): Promise<void> {
try {
await discordAPI.post(
`/applications/${discordApplicationId}/widget-configs/${configId}/publish`
);
} catch (error: any) {
console.error(`ERROR Publishing widget config ${configId}:`, error.message);
if (error.response) {
console.error(`ERROR Details:`, JSON.stringify(error.response.data));
}
throw error;
}
}

export async function patchIdentity(identity: any, userID: string) {
try {
await discordAPI.patch(`/applications/${discordApplicationId}/users/${userID}/identities/0/profile`, identity);
await discordAPI.patch(
`/applications/${discordApplicationId}/users/${userID}/identities/0/profile`,
identity,
);
} catch (error: any) {
console.error(`ERROR Discord API Patch failed: ${error.message}`);
if (error.response) {
console.error(`ERROR Status: ${error.response.status} | Data:`, JSON.stringify(error.response.data));
console.error(
`ERROR Status: ${error.response.status} | Data:`,
JSON.stringify(error.response.data),
);
}
throw error;
}
Expand Down
9 changes: 9 additions & 0 deletions src/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import dotenv from 'dotenv';
dotenv.config();
import { createWidget } from "./providers/discord";

async function setup() {
await createWidget();
}

setup();