Skip to content

Commit

Permalink
Ln/twitch embed (#468)
Browse files Browse the repository at this point in the history
* hold current progress for twitch embeds

* add twitch to caddyfile

* add more cases to regex to handle collections and clips for twitch, add tests, close embed url input after submitting post

* fix caddyfile

* prevent autoplay on twitch embeds

* add Twitch to placeholder text for the embed input box, decrease font-size
  • Loading branch information
lazynina committed Oct 11, 2021
1 parent e77be21 commit 56136b3
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 3 deletions.
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
4 changes: 2 additions & 2 deletions src/app/feed/feed-create-post/feed-create-post.component.html
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();
});
}
});

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;
}

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

0 comments on commit 56136b3

Please sign in to comment.