From 8d8be8fb16f2d641bdbdfd9aa84178266b64c8b9 Mon Sep 17 00:00:00 2001 From: beefchimi Date: Tue, 1 Mar 2022 14:49:17 -0500 Subject: [PATCH] :sparkles: [Discord] Broaden support for URL variations --- src/capture.ts | 10 ++ src/networks/discord.ts | 21 +++- src/networks/tests/discord.test.ts | 120 +++++++++++++++---- src/socialite.ts | 14 ++- src/tests/fixtures/discord-urls.ts | 186 +++++++++++++++++++++++++++++ src/tests/fixtures/index.ts | 2 + src/types/index.ts | 2 +- src/types/social-profile.ts | 4 + src/utilities/index.ts | 6 +- src/utilities/tests/fixtures.ts | 41 +++++++ src/utilities/tests/url.test.ts | 38 +++++- src/utilities/url.ts | 26 +++- 12 files changed, 434 insertions(+), 36 deletions(-) create mode 100644 src/tests/fixtures/discord-urls.ts diff --git a/src/capture.ts b/src/capture.ts index d607b01..fa2521b 100644 --- a/src/capture.ts +++ b/src/capture.ts @@ -39,3 +39,13 @@ export const defaultUserMatcher = { subdomain: /[^.]+/, path: /[^/]+/, }; + +// TODO: This should probably be re-located elsewhere +// https://github.com/beefchimi/socialite/issues/35 +export const discordPreferredUrls = { + users: `https://discordapp.com/users/${profileReplacement.user}`, + channels: `https://discord.com/channels/${profileReplacement.user}`, + vanity: `https://discord.gg/${profileReplacement.user}`, + // TODO: This result should not be supported + default: `https://discord.com/${profileReplacement.user}`, +}; diff --git a/src/networks/discord.ts b/src/networks/discord.ts index 72356e5..7848d1f 100644 --- a/src/networks/discord.ts +++ b/src/networks/discord.ts @@ -1,11 +1,26 @@ -import {profileReplacement} from '../capture'; +import {discordPreferredUrls} from '../capture'; import type {SocialiteNetwork} from '../types'; +// Discord is difficult to solve given Socialite's current design. +// There are essentially 3 different urls to support: +// 1. User profiles (discordapp.com/users/*) +// 2. Server/channel urls (discord.com/channels/{serverid}/{channelid}) +// 3. Official vanity urls (discord.gg/*) +// Since we are not yet validating against a Top-level domain (.gg), +// any `discord` url validates as true and captures the `path`. +// This degrades the confidence provided by `users` or `channels`. + +// TODO: Solve this problem by improving `preferredUrl` and parsing criteria. +// https://github.com/beefchimi/socialite/issues/35 export const discord: SocialiteNetwork = { id: 'discord', - preferredUrl: `https://discordapp.com/users/${profileReplacement.user}`, + preferredUrl: discordPreferredUrls.users, matcher: { domain: /discord/, - user: /^(?:\/users\/)([^/]+)/, + user: /^\/(?:users\/|channels\/)?([^/]+)/, + // TODO: If we want to support capturing EVERYTHING after + // the first `/` (necessary for capturing the `channelid`), + // then we would need to use the following: + // user: /^\/(?:users\/|channels\/)?(.+)/, }, }; diff --git a/src/networks/tests/discord.test.ts b/src/networks/tests/discord.test.ts index 3706a90..69903a8 100644 --- a/src/networks/tests/discord.test.ts +++ b/src/networks/tests/discord.test.ts @@ -1,38 +1,114 @@ import {Socialite} from '../../socialite'; import type {SocialiteProfile} from '../../types'; -import {allSocialiteNetworks, mockGenericUser} from '../../tests/fixtures'; +import { + allSocialiteNetworks, + discordValidUrls, + mockGenericUser, +} from '../../tests/fixtures'; import {discord} from '../discord'; describe('Social networks > discord', () => { const mockSocialite = new Socialite(allSocialiteNetworks); - const mockCommonUrl = `https://www.discordapp.com/users/${mockGenericUser}`; - it('returns expected `id` and `user` from common url', () => { - const {id, user} = mockSocialite.parseProfile( - mockCommonUrl, - ) as SocialiteProfile; + // TODO: We want to resolve these in the future + // https://github.com/beefchimi/socialite/issues/35 + describe('bogus', () => { + it('mistakenly returns the first path match for any `discord` url', () => { + const mockPageSlug = 'about-us-page'; + const mockBogusUrl = `https://www.discord.com/${mockPageSlug}`; - expect(id).toBe(discord.id); - expect(user).toBe(mockGenericUser); + const {id, user} = mockSocialite.parseProfile( + mockBogusUrl, + ) as SocialiteProfile; + + expect(id).toBe(discord.id); + expect(user).toBe(mockPageSlug); + }); + }); + + describe('users', () => { + const mockUsersUrl = `https://www.discordapp.com/users/${mockGenericUser}`; + + it('returns `id` and `user`', () => { + const {id, user} = mockSocialite.parseProfile( + mockUsersUrl, + ) as SocialiteProfile; + + expect(id).toBe(discord.id); + expect(user).toBe(mockGenericUser); + }); + + it('omits any trailing path after the first `user` match', () => { + const mockUsersTrailingUrl = `${mockUsersUrl}/trail-123`; + + const {id, user} = mockSocialite.parseProfile( + mockUsersTrailingUrl, + ) as SocialiteProfile; + + expect(id).toBe(discord.id); + expect(user).toBe(mockGenericUser); + }); }); - it('returns expected `id` and `user` from url with trailing path', () => { - const mockUncommonUrl = `${mockCommonUrl}/trail-123`; - const {id, user} = mockSocialite.parseProfile( - mockUncommonUrl, - ) as SocialiteProfile; + describe('channels', () => { + const mockChannelsUrl = `https://www.discord.com/channels/${mockGenericUser}`; + + it('returns `id` and `user`', () => { + const {id, user} = mockSocialite.parseProfile( + mockChannelsUrl, + ) as SocialiteProfile; + + expect(id).toBe(discord.id); + expect(user).toBe(mockGenericUser); + }); + + it('omits any trailing path after the first `user` match', () => { + const mockChannelsTrailingUrl = `${mockChannelsUrl}/trail-123`; + + const {id, user} = mockSocialite.parseProfile( + mockChannelsTrailingUrl, + ) as SocialiteProfile; + + expect(id).toBe(discord.id); + expect(user).toBe(mockGenericUser); + }); + }); + + describe('vanity', () => { + const mockVanityUrl = `https://discord.gg/${mockGenericUser}`; + + it('returns `id` and `user`', () => { + const {id, user} = mockSocialite.parseProfile( + mockVanityUrl, + ) as SocialiteProfile; + + expect(id).toBe(discord.id); + expect(user).toBe(mockGenericUser); + }); + + it('omits any trailing path after the first `user` match', () => { + const mockVanityTrailingUrl = `${mockVanityUrl}/trail-123`; + + const {id, user} = mockSocialite.parseProfile( + mockVanityTrailingUrl, + ) as SocialiteProfile; - expect(id).toBe(discord.id); - expect(user).toBe(mockGenericUser); + expect(id).toBe(discord.id); + expect(user).toBe(mockGenericUser); + }); }); - it('returns `id` with no `user` when provided an unrecognized leading path', () => { - const mockUnsupportedUrl = `https://discordapp.com/foo/${mockGenericUser}`; - const match = mockSocialite.parseProfile( - mockUnsupportedUrl, - ) as SocialiteProfile; + describe('all variations', () => { + it('returns `id` and `user`', () => { + discordValidUrls.forEach(({originalUrl, preferredUrl, user}) => { + const match = mockSocialite.parseProfile( + originalUrl, + ) as SocialiteProfile; - expect(match.id).toBe(discord.id); - expect(match.user).toBeUndefined(); + expect(match.id).toBe(discord.id); + expect(match.preferredUrl).toBe(preferredUrl); + expect(match.user).toBe(user); + }); + }); }); }); diff --git a/src/socialite.ts b/src/socialite.ts index 48e9721..9757e0a 100644 --- a/src/socialite.ts +++ b/src/socialite.ts @@ -4,6 +4,7 @@ import {defaultUserMatcher, schemeRegExp} from './capture'; import {MatchUserSource} from './types'; import type { BasicUrl, + DiscordProfile, NetworkId, NetworkMap, ParsedUrlGroups, @@ -15,6 +16,7 @@ import type { } from './types'; import { filterNetworkProperties, + getDiscordPreferredUrl, getUrlGroups, getUrlWithSubstitutions, } from './utilities'; @@ -155,11 +157,13 @@ export class Socialite { return minResult; } - const preferredUrl = getUrlWithSubstitutions( - targetNetwork.preferredUrl, - user, - prefix, - ); + // TODO: Resolve this special condition + // https://github.com/beefchimi/socialite/issues/35 + const preferredUrl = + targetNetwork.id === 'discord' + ? getDiscordPreferredUrl({...minResult, user} as DiscordProfile) + : getUrlWithSubstitutions(targetNetwork.preferredUrl, user, prefix); + const appUrl = targetNetwork.appUrl ? getUrlWithSubstitutions(targetNetwork.appUrl, user, prefix) : undefined; diff --git a/src/tests/fixtures/discord-urls.ts b/src/tests/fixtures/discord-urls.ts new file mode 100644 index 0000000..0f7a7eb --- /dev/null +++ b/src/tests/fixtures/discord-urls.ts @@ -0,0 +1,186 @@ +export const discordValidUrls = [ + { + originalUrl: 'https://discord.com/users/username', + preferredUrl: 'https://discordapp.com/users/username', + user: 'username', + }, + { + originalUrl: 'http://discordapp.com/users/0123456789', + preferredUrl: 'https://discordapp.com/users/0123456789', + user: '0123456789', + }, + { + originalUrl: 'https://discord.com/channels/@me', + preferredUrl: 'https://discord.com/channels/@me', + user: '@me', + }, + { + originalUrl: 'http://discord.com/channels/892738558316662855', + preferredUrl: 'https://discord.com/channels/892738558316662855', + user: '892738558316662855', + }, + { + originalUrl: + 'https://discord.com/channels/892738558316662855/892738558996148236', + preferredUrl: 'https://discord.com/channels/892738558316662855', + user: '892738558316662855', + }, + { + originalUrl: 'http://discordapp.com/channels/serverid/channelid/messageid', + preferredUrl: 'https://discord.com/channels/serverid', + user: 'serverid', + }, + { + originalUrl: 'https://discord.gg/vanity-url', + preferredUrl: 'https://discord.gg/vanity-url', + user: 'vanity-url', + }, + { + originalUrl: 'http://discord.gg/vanity-url/0123456789', + preferredUrl: 'https://discord.gg/vanity-url', + user: 'vanity-url', + }, + { + originalUrl: 'https://discordapp.gg/0123456789', + preferredUrl: 'https://discord.gg/0123456789', + user: '0123456789', + }, + { + originalUrl: 'http://www.discord.com/users/username', + preferredUrl: 'https://discordapp.com/users/username', + user: 'username', + }, + { + originalUrl: 'https://www.discordapp.com/users/0123456789', + preferredUrl: 'https://discordapp.com/users/0123456789', + user: '0123456789', + }, + { + originalUrl: 'http://www.discord.com/channels/@me', + preferredUrl: 'https://discord.com/channels/@me', + user: '@me', + }, + { + originalUrl: 'https://www.discord.com/channels/892738558316662855', + preferredUrl: 'https://discord.com/channels/892738558316662855', + user: '892738558316662855', + }, + { + originalUrl: + 'http://www.discord.com/channels/892738558316662855/892738558996148236', + preferredUrl: 'https://discord.com/channels/892738558316662855', + user: '892738558316662855', + }, + { + originalUrl: + 'https://www.discordapp.com/channels/serverid/channelid/messageid', + preferredUrl: 'https://discord.com/channels/serverid', + user: 'serverid', + }, + { + originalUrl: 'http://www.discord.gg/vanity-url', + preferredUrl: 'https://discord.gg/vanity-url', + user: 'vanity-url', + }, + { + originalUrl: 'https://www.discord.gg/vanity-url/0123456789', + preferredUrl: 'https://discord.gg/vanity-url', + user: 'vanity-url', + }, + { + originalUrl: 'http://www.discordapp.gg/0123456789', + preferredUrl: 'https://discord.gg/0123456789', + user: '0123456789', + }, + { + originalUrl: 'www.discord.com/users/username', + preferredUrl: 'https://discordapp.com/users/username', + user: 'username', + }, + { + originalUrl: 'sub.discordapp.com/users/0123456789', + preferredUrl: 'https://discordapp.com/users/0123456789', + user: '0123456789', + }, + { + originalUrl: 'www.discord.com/channels/@me', + preferredUrl: 'https://discord.com/channels/@me', + user: '@me', + }, + { + originalUrl: 'sub1.sub2.discord.com/channels/892738558316662855', + preferredUrl: 'https://discord.com/channels/892738558316662855', + user: '892738558316662855', + }, + { + originalUrl: + 'www.discord.com/channels/892738558316662855/892738558996148236', + preferredUrl: 'https://discord.com/channels/892738558316662855', + user: '892738558316662855', + }, + { + originalUrl: 'sub.discordapp.com/channels/serverid/channelid/messageid', + preferredUrl: 'https://discord.com/channels/serverid', + user: 'serverid', + }, + { + originalUrl: 'www.discord.gg/vanity-url', + preferredUrl: 'https://discord.gg/vanity-url', + user: 'vanity-url', + }, + { + originalUrl: 'sub1.sub2.discord.gg/vanity-url/0123456789', + preferredUrl: 'https://discord.gg/vanity-url', + user: 'vanity-url', + }, + { + originalUrl: 'www.discordapp.gg/0123456789', + preferredUrl: 'https://discord.gg/0123456789', + user: '0123456789', + }, + { + originalUrl: 'discord.com/users/username', + preferredUrl: 'https://discordapp.com/users/username', + user: 'username', + }, + { + originalUrl: 'discordapp.com/users/0123456789', + preferredUrl: 'https://discordapp.com/users/0123456789', + user: '0123456789', + }, + { + originalUrl: 'discord.com/channels/@me', + preferredUrl: 'https://discord.com/channels/@me', + user: '@me', + }, + { + originalUrl: 'discord.com/channels/892738558316662855', + preferredUrl: 'https://discord.com/channels/892738558316662855', + user: '892738558316662855', + }, + { + originalUrl: 'discord.com/channels/892738558316662855/892738558996148236', + preferredUrl: 'https://discord.com/channels/892738558316662855', + user: '892738558316662855', + }, + { + originalUrl: 'discordapp.com/channels/serverid/channelid/messageid', + preferredUrl: 'https://discord.com/channels/serverid', + user: 'serverid', + }, + { + originalUrl: 'discord.gg/vanity-url', + preferredUrl: 'https://discord.gg/vanity-url', + user: 'vanity-url', + }, + { + originalUrl: 'discord.gg/vanity-url/0123456789', + preferredUrl: 'https://discord.gg/vanity-url', + user: 'vanity-url', + }, + { + originalUrl: 'discordapp.gg/0123456789', + preferredUrl: 'https://discord.gg/0123456789', + user: '0123456789', + }, +]; diff --git a/src/tests/fixtures/index.ts b/src/tests/fixtures/index.ts index b6222c8..f42fc89 100644 --- a/src/tests/fixtures/index.ts +++ b/src/tests/fixtures/index.ts @@ -1,5 +1,7 @@ export {allSocialiteNetworks, mockCustomNetworks} from './networks'; +export {discordValidUrls} from './discord-urls'; + export {invalidProfileUrls} from './invalid-profiles'; export { mockGenericUser, diff --git a/src/types/index.ts b/src/types/index.ts index e6faf6f..aaca82b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -16,4 +16,4 @@ export type { SocialiteNetworkProperties, } from './social-network'; -export type {SocialiteProfile} from './social-profile'; +export type {DiscordProfile, SocialiteProfile} from './social-profile'; diff --git a/src/types/social-profile.ts b/src/types/social-profile.ts index fe85f89..880e17a 100644 --- a/src/types/social-profile.ts +++ b/src/types/social-profile.ts @@ -10,3 +10,7 @@ export interface SocialiteProfile { user?: UserName; prefix?: UserPrefix; } + +export interface DiscordProfile extends SocialiteProfile { + id: 'discord'; +} diff --git a/src/utilities/index.ts b/src/utilities/index.ts index f5d67eb..0cb1f8a 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -1,4 +1,8 @@ export {filterNullishValuesFromObject} from './general'; export {filterNetworkProperties} from './network'; export {mergeRegExp} from './regexp'; -export {getUrlGroups, getUrlWithSubstitutions} from './url'; +export { + getUrlGroups, + getUrlWithSubstitutions, + getDiscordPreferredUrl, +} from './url'; diff --git a/src/utilities/tests/fixtures.ts b/src/utilities/tests/fixtures.ts index d6e0943..6d63f69 100644 --- a/src/utilities/tests/fixtures.ts +++ b/src/utilities/tests/fixtures.ts @@ -1,4 +1,5 @@ import {profileReplacement} from '../../capture'; +import type {DiscordProfile} from '../../types'; export const mockObject = { one: 1, @@ -26,3 +27,43 @@ export const mockFullUrl = export const mockPartialUrl = 'http://website.ca?query=param'; export const mockReplacementUrl = `https://domain.com/${profileReplacement.prefix}${profileReplacement.user}`; + +export const mockDiscordUsersProfile: DiscordProfile = { + id: 'discord', + urlGroups: { + domain: 'discordapp', + tldomain: '.com', + scheme: 'https://', + subdomain: 'www.', + path: '/users/username', + }, + originalUrl: 'https://www.discordapp.com/users/username', + preferredUrl: 'https://discordapp.com/users/username', + user: 'username', +}; + +export const mockDiscordChannelsProfile: DiscordProfile = { + id: 'discord', + urlGroups: { + domain: 'discord', + tldomain: '.com', + scheme: 'http', + subdomain: 'www.', + path: '/channels/foo', + }, + originalUrl: 'http://www.discord.com/channels/foo', + preferredUrl: 'https://discord.com/channels/foo', + user: 'foo', +}; + +export const mockDiscordVanityProfile: DiscordProfile = { + id: 'discord', + urlGroups: { + domain: 'discord', + tldomain: '.gg', + path: '/bar123', + }, + originalUrl: 'discord.gg/bar123', + preferredUrl: 'https://discord.gg/bar123', + user: 'bar123', +}; diff --git a/src/utilities/tests/url.test.ts b/src/utilities/tests/url.test.ts index 5c28ea1..613a59d 100644 --- a/src/utilities/tests/url.test.ts +++ b/src/utilities/tests/url.test.ts @@ -1,5 +1,16 @@ -import {getUrlGroups, getUrlWithSubstitutions} from '../url'; -import {mockFullUrl, mockPartialUrl, mockReplacementUrl} from './fixtures'; +import { + getUrlGroups, + getUrlWithSubstitutions, + getDiscordPreferredUrl, +} from '../url'; +import { + mockFullUrl, + mockPartialUrl, + mockReplacementUrl, + mockDiscordUsersProfile, + mockDiscordChannelsProfile, + mockDiscordVanityProfile, +} from './fixtures'; describe('Url utilities', () => { describe('getUrlGroups()', () => { @@ -74,4 +85,27 @@ describe('Url utilities', () => { expect(result).toBe(`https://domain.com/${mockPrefix}${mockUser}`); }); }); + + describe('getDiscordPreferredUrl()', () => { + it('returns `preferredUrl` for `users`', () => { + const result = getDiscordPreferredUrl(mockDiscordUsersProfile); + expect(result).toBe( + `https://discordapp.com/users/${mockDiscordUsersProfile.user}`, + ); + }); + + it('returns `preferredUrl` for `channels`', () => { + const result = getDiscordPreferredUrl(mockDiscordChannelsProfile); + expect(result).toBe( + `https://discord.com/channels/${mockDiscordChannelsProfile.user}`, + ); + }); + + it('returns `preferredUrl` for `vanity`', () => { + const result = getDiscordPreferredUrl(mockDiscordVanityProfile); + expect(result).toBe( + `https://discord.gg/${mockDiscordVanityProfile.user}`, + ); + }); + }); }); diff --git a/src/utilities/url.ts b/src/utilities/url.ts index 1592d9b..0a42d38 100644 --- a/src/utilities/url.ts +++ b/src/utilities/url.ts @@ -1,5 +1,10 @@ -import {profileReplacement, urlRegExp} from '../capture'; -import type {BasicUrl, UrlGroupSubset, ParsedUrlGroups} from '../types'; +import {discordPreferredUrls, profileReplacement, urlRegExp} from '../capture'; +import type { + BasicUrl, + DiscordProfile, + UrlGroupSubset, + ParsedUrlGroups, +} from '../types'; import {filterNullishValuesFromObject} from './general'; function updateGroupsWithSubdomain(groups: UrlGroupSubset): UrlGroupSubset { @@ -42,3 +47,20 @@ export function getUrlWithSubstitutions(url: BasicUrl, user = '', prefix = '') { .replace(profileReplacement.user, user) .replace(profileReplacement.prefix, prefix); } + +export function getDiscordPreferredUrl({urlGroups, user}: DiscordProfile) { + if (urlGroups.path?.startsWith('/users/')) { + return getUrlWithSubstitutions(discordPreferredUrls.users, user); + } + + if (urlGroups.path?.startsWith('/channels/')) { + return getUrlWithSubstitutions(discordPreferredUrls.channels, user); + } + + if (urlGroups.tldomain === '.gg') { + return getUrlWithSubstitutions(discordPreferredUrls.vanity, user); + } + + // Currently, there are no other supported URLs (such as a `appUrl`). + return getUrlWithSubstitutions(discordPreferredUrls.default, user); +}