Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ln/twitch embed #468

Merged
merged 10 commits into from
Oct 11, 2021
2 changes: 2 additions & 0 deletions Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ header @html Content-Security-Policy "
https://giphy.com
https://open.spotify.com
https://w.soundcloud.com
https://player.twitch.tv
https://clips.twitch.tv
pay.testwyre.com
pay.sendwyre.com
https://iframe.videodelivery.net;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@
</div>
</div>
</div>
<div class="flex-fill px-15px br-10px" [ngClass]="{ 'pt-10px': parentPost }" *ngIf="showEmbedURL">
<div class="flex-fill px-15px br-10px fs-13px" [ngClass]="{ 'pt-10px': parentPost }" *ngIf="showEmbedURL">
<input
class="br-3px"
type="url"
[(ngModel)]="embedURL"
(ngModelChange)="setEmbedURL()"
placeholder="Embed Youtube, Vimeo, TikTok, Giphy, Spotify or SoundCloud"
placeholder="Embed Youtube, Vimeo, TikTok, Giphy, Spotify, SoundCloud, or Twitch"
/>
</div>
<div class="flex-fill px-15px br-10px" [ngClass]="{ 'pt-10px': parentPost }" *ngIf="showImageLink">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export class FeedCreatePostComponent implements OnInit {
this.postVideoSrc = null;
this.embedURL = "";
this.constructedEmbedURL = "";
this.showEmbedURL = false;
this.changeRef.detectChanges();

// Refresh the post page.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,25 @@ describe("EmbedUrlParserService", () => {
return `https://w.soundcloud.com/player/?url=${url}?hide_related=true&show_comments=false`;
});

const twitchVideoID = "1234567890";
const twitchChannelID = "chann3lId";
const twitchClipID = "SomeClipId-123iuMA32-m0";
const twitchCollectionID = "M123k88lskjqi";

const validTwitchURLs = [
`https://www.twitch.tv/${twitchChannelID}`,
`https://www.twitch.tv/videos/${twitchVideoID}`,
`https://www.twitch.tv/collections/${twitchCollectionID}`,
`https://www.twitch.tv/${twitchChannelID}/clip/${twitchClipID}`,
];

const validTwitchEmbedURLs = [
`https://player.twitch.tv/?channel=${twitchChannelID}`,
`https://player.twitch.tv/?video=${twitchVideoID}`,
`https://player.twitch.tv/?collection=${twitchCollectionID}`,
`https://clips.twitch.tv/embed?clip=${twitchClipID}`,
];

const invalidURLs = [
"https://google.com",
"facebook.com<script></script>",
Expand All @@ -109,6 +128,10 @@ describe("EmbedUrlParserService", () => {
`https://giphy.com/gifs/abc-def-${giphyID}-;<script></script>`,
`https://open.notspotify.com/embed/track/${spotifyID}-?;<script/></script>`,
`https://w.soundcloud.com/player/<script>?url=maliciousscript</script>?hide_related=true&show_comments=false`,
`https://player.twitch.tv/?video=aaa${twitchVideoID}`,
`https://player.twitch.tv/<script>maliciousscript</script>?channel=${twitchChannelID}`,
`https://clips.twitch.tv<script>evilstuff</script>/embed?clip=${twitchClipID}`,
`https://player.twitch.tv/?collections=!@#&*!)@#${twitchCollectionID}`,
];

it("parses youtube URLs from user input correctly and only validates embed urls", () => {
Expand Down Expand Up @@ -228,6 +251,40 @@ describe("EmbedUrlParserService", () => {
}
});

it("parses Twitch URLs from user input correctly and only validates embed urls", () => {
for (let i = 0; i < validTwitchURLs.length; i++) {
const link = validTwitchURLs[i];
const embedLink = validTwitchEmbedURLs[i];
expect(EmbedUrlParserService.isTwitchLink(link)).toBeTruthy();
const embedURL = EmbedUrlParserService.constructTwitchEmbedURL(new URL(link));
expect(embedURL).toEqual(embedLink);
expect(EmbedUrlParserService.isValidEmbedURL(embedURL)).toBeTruthy();
expect(EmbedUrlParserService.isValidTwitchEmbedURL(embedURL)).toBeTruthy();
expect(EmbedUrlParserService.isValidTwitchEmbedURLWithParent(embedURL)).toBeFalsy();
expect(EmbedUrlParserService.isValidEmbedURL(link)).toBeFalsy();

EmbedUrlParserService.getEmbedURL(backendApiService, globalVarsService, link).subscribe((res) => {
expect(res).toEqual(embedLink + `&autoplay=false&parent=${location.hostname}`);
expect(EmbedUrlParserService.isValidEmbedURL(res)).toBeTruthy();
expect(EmbedUrlParserService.isValidTwitchEmbedURLWithParent(res)).toBeTruthy();
expect(EmbedUrlParserService.isValidTwitchEmbedURL(res)).toBeFalsy();
});

expect(EmbedUrlParserService.isTwitchLink(embedLink));
expect(EmbedUrlParserService.isValidEmbedURL(embedLink));
const constructedEmbedUrl = EmbedUrlParserService.constructTwitchEmbedURL(new URL(embedLink));
expect(EmbedUrlParserService.isValidTwitchEmbedURL(constructedEmbedUrl)).toBeTruthy();
expect(EmbedUrlParserService.isValidTwitchEmbedURLWithParent(constructedEmbedUrl)).toBeFalsy();

EmbedUrlParserService.getEmbedURL(backendApiService, globalVarsService, embedLink).subscribe((res) => {
expect(res).toEqual(embedLink + `&autoplay=false&parent=${location.hostname}`);
expect(EmbedUrlParserService.isValidEmbedURL(res)).toBeTruthy();
expect(EmbedUrlParserService.isValidTwitchEmbedURLWithParent(res)).toBeTruthy();
expect(EmbedUrlParserService.isValidTwitchEmbedURL(res)).toBeFalsy();
});
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this...a...frontend test??? 😱


it("invalid URLs return falsy values", async () => {
for (const link of invalidURLs) {
expect(EmbedUrlParserService.isValidEmbedURL(link)).toBeFalsy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,51 @@ export class EmbedUrlParserService {
: "";
}

static twitchParser(url: string): string | boolean {
const regExp = /^.*((player\.|clips\.)?twitch\.tv)\/(videos\/(\d{8,12})|\?video=(\d{8,12})|\?channel=([A-Za-z0-9_]{1,30})|collections\/([A-Za-z0-9]{10,20})|\?collection=([A-Za-z0-9]{10,20}(&video=\d{8,12})?)|embed\?clip=([A-Za-z0-9_-]{1,80})|([A-Za-z0-9_]{1,30}(\/clip\/([A-Za-z0-9_-]{1,80}))?)).*/;
const match = url.match(regExp);
if (match && match[3]) {
// https://www.twitch.tv/videos/1234567890
if (match[3].startsWith("videos") && match[4]) {
return `player.twitch.tv/?video=${match[4]}`;
}
// https://player.twitch.tv/?video=1234567890&parent=www.example.com
if (match[3].startsWith("?video=") && match[5]) {
return `player.twitch.tv/?video=${match[5]}`;
}
// https://player.twitch.tv/?channel=xxxyyy123&parent=www.example.com
if (match[3].startsWith("?channel=") && match[6]) {
return `player.twitch.tv/?channel=${match[6]}`;
}
// https://www.twitch.tv/xxxyyy123
if (match[3] && match[11] && match[3] === match[11] && !match[12] && !match[13]) {
return `player.twitch.tv/?channel=${match[11]}`;
}
// https://www.twitch.tv/xxyy_1234m/clip/AbCD123JMn-rrMMSj1239G7
if (match[12] && match[13]) {
return `clips.twitch.tv/embed?clip=${match[13]}`;
}
// https://clips.twitch.tv/embed?clip=AbCD123JMn-rrMMSj1239G7&parent=www.example.com
if (match[10]) {
return `clips.twitch.tv/embed?clip=${match[10]}`;
}
// https://www.twitch.tv/collections/11jaabbcc2yM989x?filter=collections
if (match[7]) {
return `player.twitch.tv/?collection=${match[7]}`;
}
// https://player.twitch.tv/?collection=11jaabbcc2yM989x&video=1234567890&parent=www.example.com
if (match[8]) {
return `player.twitch.tv/?collection=${match[8]}`;
}
}
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is one hell of a regex... seems legit as far as I can tell though, given all the character sets are restricted to basic characters. Nice work!


static constructTwitchEmbedURL(url: URL): string {
const twitchParsed = this.twitchParser(url.toString());
return twitchParsed ? `https://${twitchParsed}` : "";
}

static extractTikTokVideoID(fullTikTokURL: string): string | boolean {
const regExp = /^.*((tiktok\.com\/)(v\/)|(@[A-Za-z0-9_-]{2,24}\/video\/)|(embed\/v2\/))(\d{0,30}).*/;
const match = fullTikTokURL.match(regExp);
Expand Down Expand Up @@ -157,6 +202,11 @@ export class EmbedUrlParserService {
if (this.isSoundCloudFromURL(url)) {
return of(this.constructSoundCloudEmbedURL(url));
}
if (this.isTwitchFromURL(url)) {
return of(this.constructTwitchEmbedURL(url)).pipe(
map((embedURL) => (embedURL ? embedURL + `&autoplay=false&parent=${location.hostname}` : ""))
);
}
return of("");
}

Expand Down Expand Up @@ -244,6 +294,20 @@ export class EmbedUrlParserService {
return pattern.test(url.hostname);
}

static isTwitchLink(link: string): boolean {
try {
const url = new URL(link);
return this.isTwitchFromURL(url);
} catch (e) {
return false;
}
}

static isTwitchFromURL(url: URL): boolean {
const pattern = /\btwitch\.tv$/;
return pattern.test(url.hostname);
}

static isValidVimeoEmbedURL(link: string): boolean {
const regExp = /(https:\/\/player\.vimeo\.com\/video\/(\d{0,15}))$/;
return !!link.match(regExp);
Expand Down Expand Up @@ -274,6 +338,18 @@ export class EmbedUrlParserService {
return !!link.match(regExp);
}

static isValidTwitchEmbedURL(link: string): boolean {
const regExp = /(https:\/\/(player|clips)\.twitch\.tv\/(\?channel=[A-Za-z0-9_]{1,30}|\?video=\d{8,12}|embed\?clip=[A-Za-z0-9_-]{1,80}|\?collection=[A-Za-z0-9]{10,20}(&video=\d{8,12})?))$/;
return !!link.match(regExp);
}

static isValidTwitchEmbedURLWithParent(link: string): boolean {
const regExp = new RegExp(
`https:\/\/(player|clips)\.twitch\.tv\/(\\?channel\=[A-Za-z0-9_]{1,30}|\\?video=\\d{8,12}|embed\\?clip=[A-Za-z0-9_-]{1,80}|\\?collection=[A-Za-z0-9]{10,20}(\&video=\\d{8,12})\?)\&autoplay=false\&parent=${location.hostname}$`
);
return !!link.match(regExp);
}

static isValidEmbedURL(link: string): boolean {
if (link) {
return (
Expand All @@ -282,7 +358,9 @@ export class EmbedUrlParserService {
this.isValidTiktokEmbedURL(link) ||
this.isValidGiphyEmbedURL(link) ||
this.isValidSpotifyEmbedURL(link) ||
this.isValidSoundCloudEmbedURL(link)
this.isValidSoundCloudEmbedURL(link) ||
this.isValidTwitchEmbedURL(link) ||
this.isValidTwitchEmbedURLWithParent(link)
);
}
return false;
Expand Down