diff --git a/packages/extension/build/manifest.ts b/packages/extension/build/manifest.ts index f879111..0e529a2 100644 --- a/packages/extension/build/manifest.ts +++ b/packages/extension/build/manifest.ts @@ -85,6 +85,7 @@ export default function manifest (): Plugin { 'https://*.discord.com/*', 'https://*.github.com/*', 'https://*.modrinth.com/*', + 'https://*.osu.ppy.sh/*', 'https://*.twitch.tv/*', 'https://*.twitter.com/*', 'https://*.x.com/*', diff --git a/packages/extension/src/modules/osu/main.ts b/packages/extension/src/modules/osu/main.ts new file mode 100644 index 0000000..0125bd8 --- /dev/null +++ b/packages/extension/src/modules/osu/main.ts @@ -0,0 +1,123 @@ +/* + * Copyright (c) Cynthia Rey et al., All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import { css, h } from '../../utils/dom' +import { fetchPronouns } from '../../utils/fetch' +import { formatPronouns } from '../../utils/pronouns' + +export const name = 'osu!' +export const color = '#F372AD' +export const match = /^https:\/\/osu\.ppy\.sh/ +export { default as Icon } from 'simple-icons/icons/osu.svg' + +async function handleChatMessage (node: HTMLElement) { + const sender = node.querySelector('.chat-message-group__sender') + + const userId = sender?.querySelector('a')?.dataset.userId + + if (!userId) return + + const pronouns = await fetchPronouns('osu', userId) + + if (!pronouns || !pronouns.sets.en) return + + const el = h('span', { class: 'u-ellipsis-overflow' }, formatPronouns(pronouns.sets.en)) + + sender.appendChild(el) +} + +async function handleMutation (mutations: MutationRecord[]) { + for (const { addedNodes } of mutations) { + for (const node of addedNodes) { + if (node instanceof HTMLElement) { + if (node.classList.contains('chat-message-group')) { + handleChatMessage(node) + continue + } + + if (node.classList.contains('user-card--card') && !('userId' in node.dataset)) { + handleUserPopout(node) + continue + } + + if (node.classList.contains('profile-info__info')) { + handleUserProfile(node) + continue + } + } + } + } +} + +async function handleUserPopout (node: HTMLElement) { + const userId = node.querySelector('.user-card__username')?.href.split('/').pop() + + if (!userId) return + + const pronouns = await fetchPronouns('osu', userId) + + if (!pronouns || !pronouns.sets.en) return + + const usernameAnchor = node.querySelector('.user-card__username') + + if (!usernameAnchor) return + + const el = h('span', { class: 'user-card__pronouns', style: css({ marginLeft: '5px' }) }, `- ${formatPronouns(pronouns.sets.en)}`) + + usernameAnchor.parentElement!.insertBefore(el, usernameAnchor.nextSibling) +} + +async function handleUserProfile (node: HTMLElement) { + const userId = window.location.pathname.split('/').pop() + + if (!userId) return + + const profileInfo = node.querySelector('.profile-info__flag') + + if (!profileInfo) return + + const pronouns = await fetchPronouns('osu', userId) + + if (!pronouns || !pronouns.sets.en) return + + const el = h('span', { class: 'profile-info__pronouns profile-info__flag-text' }, `- ${formatPronouns(pronouns.sets.en)}`) + + profileInfo.appendChild(el) +} + +export function inject () { + document.querySelectorAll('.chat-message-group').forEach((node) => handleChatMessage(node)) + + if (document.querySelector('.profile-info__info')) { + handleUserProfile(document.querySelector('.profile-info__info')!) + } + + const observer = new MutationObserver(handleMutation) + + observer.observe(document, { childList: true, subtree: true }) +} diff --git a/packages/website/.env.example b/packages/website/.env.example index 1314882..2945611 100644 --- a/packages/website/.env.example +++ b/packages/website/.env.example @@ -31,3 +31,6 @@ OAUTH_TWITTER_SECRET= OAUTH_MICROSOFT_CLIENT= OAUTH_MICROSOFT_SECRET= + +OAUTH_OSU_CLIENT= +OAUTH_OSU_SECRET= diff --git a/packages/website/src/components/platforms/osu.ts b/packages/website/src/components/platforms/osu.ts new file mode 100644 index 0000000..73a4a02 --- /dev/null +++ b/packages/website/src/components/platforms/osu.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) Cynthia Rey et al., All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export const name = 'osu!' +export const color = '#F372AD' +export { default as icon } from 'simple-icons/icons/osu.svg?raw' diff --git a/packages/website/src/server/oauth/platforms/osu.ts b/packages/website/src/server/oauth/platforms/osu.ts new file mode 100644 index 0000000..2877e69 --- /dev/null +++ b/packages/website/src/server/oauth/platforms/osu.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) Cynthia Rey et al., All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import type { ExternalAccount } from '@server/database/database.ts' +import type { FlashMessage } from '@server/flash.ts' + +export const oauthVersion = 2 +export const clientId = import.meta.env.OAUTH_OSU_CLIENT +export const clientSecret = import.meta.env.OAUTH_OSU_SECRET + +export const authorizationUrl = 'https://osu.ppy.sh/oauth/authorize' +export const tokenUrl = 'https://osu.ppy.sh/oauth/token' +export const scopes = [ 'identify' ] + +export async function getSelf (token: string): Promise { + const res = await fetch('https://osu.ppy.sh/api/v2/me', { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + 'User-Agent': 'PronounDB Authentication Agent/2.0 (+https://pronoundb.org)', + }, + }) + + if (!res.ok) return null + const data = await res.json() + + return { + platform: 'osu', + accountId: data.id, + accountName: data.username, + } +}