diff --git a/packages/content-handler/src/index.ts b/packages/content-handler/src/index.ts index 3dbe4c9d6..cddbccde9 100644 --- a/packages/content-handler/src/index.ts +++ b/packages/content-handler/src/index.ts @@ -39,6 +39,7 @@ import { WeixinQqHandler } from './websites/weixin-qq-handler' import { WikipediaHandler } from './websites/wikipedia-handler' import { YoutubeHandler } from './websites/youtube-handler' import { ZhihuHandler } from './websites/zhihu-handler' +import { TikTokHandler } from './websites/tiktok-handler' const validateUrlString = (url: string): boolean => { const u = new URL(url) @@ -83,6 +84,7 @@ const contentHandlers: ContentHandler[] = [ new WeixinQqHandler(), new ZhihuHandler(), new TwitterHandler(), + new TikTokHandler(), ] const newsletterHandlers: ContentHandler[] = [ diff --git a/packages/content-handler/src/websites/tiktok-handler.ts b/packages/content-handler/src/websites/tiktok-handler.ts new file mode 100644 index 000000000..385c294bb --- /dev/null +++ b/packages/content-handler/src/websites/tiktok-handler.ts @@ -0,0 +1,100 @@ +import { ContentHandler, PreHandleResult } from '../content-handler' +import axios from 'axios' +import _ from 'underscore' + +const getRedirectUrl = async (url: string): Promise => { + try { + const response = await axios.get(url, { + maxRedirects: 0, + validateStatus: (status) => status === 302, + }) + return response.headers.location as string + } catch (error: any) { + if (error.response && error.response.headers.location) { + return error.response.headers.location + } + console.error('No redirect or network error occurred:', error.message) + return null + } +} + +const escapeTitle = (title: string) => { + return _.escape(title) +} + +export class TikTokHandler extends ContentHandler { + constructor() { + super() + this.name = 'TikTok' + } + + shouldPreHandle(url: string): boolean { + const u = new URL(url) + return u.hostname.endsWith('tiktok.com') + } + + async preHandle(url: string): Promise { + let fetchUrl = url + const u = new URL(url) + if ( + u.hostname.startsWith('vm.tiktok.com') || + u.hostname.startsWith('vt.tiktok.com') + ) { + // Fetch the full URL + const redirectedUrl = await getRedirectUrl(url) + if (!redirectedUrl) { + throw new Error('Could not fetch redirect URL for: ' + url) + } + fetchUrl = redirectedUrl + } + + const oembedUrl = + `https://www.tiktok.com/oembed?format=json&url=` + + encodeURIComponent(fetchUrl) + const oembed = (await axios.get(oembedUrl.toString())).data as { + title: string + width: number + height: number + html: string + thumbnail_url: string + author_name: string + author_url: string + } + console.log('oembed results: ', oembed) + // escape html entities in title + const title = oembed.title + const escapedTitle = escapeTitle(title) + const ratio = oembed.width / oembed.height + const thumbnail = oembed.thumbnail_url + const height = 350 + const width = height * ratio + const authorName = _.escape(oembed.author_name) + //

${escapedTitle}

+ const content = ` + + TikTok page + + + + + + + + + +
+ +
+ + ` + + console.log('content, title', title, content) + + return { content, title } + } +} diff --git a/packages/readabilityjs/Readability.js b/packages/readabilityjs/Readability.js index 55a49b647..44b698bc6 100644 --- a/packages/readabilityjs/Readability.js +++ b/packages/readabilityjs/Readability.js @@ -263,7 +263,7 @@ Readability.prototype = { // These are the classes that we want to keep. CLASSES_TO_PRESERVE: [ - "page", "twitter-tweet", "tweet-placeholder", "instagram-placeholder", "morning-brew-markets", "prism-code" + "page", "twitter-tweet", "tweet-placeholder", "instagram-placeholder", "morning-brew-markets", "prism-code", "tiktok-embed" ], // Classes of placeholder elements that can be empty but shouldn't be removed diff --git a/packages/web/additional.d.ts b/packages/web/additional.d.ts index ffacb54e9..eeffd8b9f 100644 --- a/packages/web/additional.d.ts +++ b/packages/web/additional.d.ts @@ -16,6 +16,7 @@ declare global { AndroidWebKitMessenger?: AndroidWebKitMessenger themeKey?: string twttr?: EmbedTweetWidget + tiktokEmbed?: EmbedTiktokWidget } } @@ -50,3 +51,9 @@ export interface EmbedTweetWidget { } [key: string]: string | { createTweet: unknown } } + +export interface EmbedTiktokWidget { + lib: { + render: (tiktoksOnPage) => void + } +} diff --git a/packages/web/components/templates/article/Article.tsx b/packages/web/components/templates/article/Article.tsx index d010988ba..f9ea77e40 100644 --- a/packages/web/components/templates/article/Article.tsx +++ b/packages/web/components/templates/article/Article.tsx @@ -67,9 +67,8 @@ export function Article(props: ArticleProps): JSX.Element { const [lightboxOpen, setLightboxOpen] = useState(false) const [imageSrcs, setImageSrcs] = useState([]) const [lightboxIndex, setlightBoxIndex] = useState(0) - const [linkHoverData, setlinkHoverData] = useState< - LinkHoverData | undefined - >() + const [linkHoverData, setlinkHoverData] = + useState() useEffect(() => { ;(async () => { @@ -247,6 +246,36 @@ export function Article(props: ArticleProps): JSX.Element { } }, []) + useEffect(() => { + const tikTokPlaceholders = Array.from( + document.getElementsByClassName('tiktok-embed') + ) + + if (tikTokPlaceholders.length > 0) { + ;(async () => { + const tkScriptUrl = 'https://www.tiktok.com/embed.js' + const tkScriptWindowFieldName = 'tiktok' + const tkScriptName = tkScriptWindowFieldName + + await new Promise((resolve, reject) => { + if (!loadjs.isDefined(tkScriptName)) { + loadjs(tkScriptUrl, tkScriptName) + } + loadjs.ready(tkScriptName, { + success: () => { + if (window.tiktokEmbed) { + window.tiktokEmbed.lib.render(tikTokPlaceholders) + } + + resolve(true) + }, + error: () => reject(new Error('Could not load TikTok handler')), + }) + }) + })() + } + }, []) + useEffect(() => { // Get all images with initial sizes, if they are small // make sure they get displayed small diff --git a/packages/web/next.config.js b/packages/web/next.config.js index f7a772d85..5e6314214 100644 --- a/packages/web/next.config.js +++ b/packages/web/next.config.js @@ -1,14 +1,14 @@ const ContentSecurityPolicy = ` default-src 'self'; base-uri 'self'; - connect-src 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://proxy-prod.omnivore-image-cache.app https://accounts.google.com https://proxy-demo.omnivore-image-cache.app https://storage.googleapis.com https://widget.intercom.io https://api-iam.intercom.io https://static.intercomassets.com https://downloads.intercomcdn.com https://platform.twitter.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io wss://nexus-europe-websocket.intercom.io wss://nexus-australia-websocket.intercom.io https://uploads.intercomcdn.com https://tools.applemediaservices.com; + connect-src 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://proxy-prod.omnivore-image-cache.app https://accounts.google.com https://proxy-demo.omnivore-image-cache.app https://storage.googleapis.com https://widget.intercom.io https://api-iam.intercom.io https://static.intercomassets.com https://downloads.intercomcdn.com https://platform.twitter.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io wss://nexus-europe-websocket.intercom.io wss://nexus-australia-websocket.intercom.io https://uploads.intercomcdn.com https://tools.applemediaservices.com wss://www.tiktok.com; font-src 'self' data: https://cdn.jsdelivr.net https://js.intercomcdn.com https://fonts.intercomcdn.com; form-action 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://getpocket.com/auth/authorize https://intercom.help https://api-iam.intercom.io https://api-iam.eu.intercom.io https://api-iam.au.intercom.io https://www.notion.so https://api.notion.com; frame-ancestors 'none'; - frame-src 'self' https://accounts.google.com https://platform.twitter.com https://www.youtube.com https://www.youtube-nocookie.com https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/ https://www.recaptcha.net; + frame-src 'self' https://accounts.google.com https://platform.twitter.com https://www.youtube.com https://www.youtube-nocookie.com https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/ https://www.recaptcha.net https://www.tiktok.com; manifest-src 'self'; - script-src 'self' 'unsafe-inline' 'unsafe-eval' accounts.google.com https://widget.intercom.io https://js.intercomcdn.com https://platform.twitter.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net https://www.gstatic.cn/; - style-src 'self' 'unsafe-inline' https://accounts.google.com https://cdnjs.cloudflare.com; + script-src 'self' 'unsafe-inline' 'unsafe-eval' accounts.google.com https://widget.intercom.io https://js.intercomcdn.com https://platform.twitter.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net https://www.gstatic.cn/ https://*.neutral.ttwstatic.com https://www.tiktok.com/embed.js; + style-src 'self' 'unsafe-inline' https://accounts.google.com https://cdnjs.cloudflare.com https://*.neutral.ttwstatic.com; img-src 'self' blob: data: https:; worker-src 'self' blob:; media-src https://js.intercomcdn.com; diff --git a/packages/web/public/static/scripts/tiktok-embed.js b/packages/web/public/static/scripts/tiktok-embed.js new file mode 100644 index 000000000..7f3f55d35 --- /dev/null +++ b/packages/web/public/static/scripts/tiktok-embed.js @@ -0,0 +1 @@ +!function(){var t={723:function(t,n,r){"use strict";function e(t){!i.length&&o(),i[i.length]=t}t.exports=e;var o,i=[],u=0;function c(){for(;u1024){for(var n=0,r=i.length-u;n2?arguments[2]:void 0,s=Math.min((void 0===f?u:o(f,u))-a,u-c),l=1;for(a0;)a in r?r[c]=r[a]:delete r[c],c+=l,a+=l;return r}},6852:function(t,n,r){"use strict";var e=r("508"),o=r("2337"),i=r("875");t.exports=function(t){for(var n=e(this),r=i(n.length),u=arguments.length,c=o(u>1?arguments[1]:void 0,r),a=u>2?arguments[2]:void 0,f=void 0===a?r:o(a,r);f>c;)n[c++]=t;return n}},9315:function(t,n,r){var e=r("2110"),o=r("875"),i=r("2337");t.exports=function(t){return function(n,r,u){var c,a=e(n),f=o(a.length),s=i(u,f);if(t&&r!=r){for(;f>s;)if((c=a[s++])!=c)return!0}else for(;f>s;s++)if((t||s in a)&&a[s]===r)return t||s||0;return!t&&-1}}},50:function(t,n,r){var e=r("741"),o=r("9797"),i=r("508"),u=r("875"),c=r("6886");t.exports=function(t,n){var r=1==t,a=2==t,f=3==t,s=4==t,l=6==t,h=5==t||l,p=n||c;return function(n,c,v){for(var d,y,g=i(n),m=o(g),_=e(c,v,3),b=u(m.length),w=0,x=r?p(n,b):a?p(n,0):void 0;b>w;w++)if((h||w in m)&&(y=_(d=m[w],w,g),t)){if(r)x[w]=y;else if(y)switch(t){case 3:return!0;case 5:return d;case 6:return w;case 2:x.push(d)}else if(s)return!1}return l?-1:f||s?s:x}}},7628:function(t,n,r){var e=r("4963"),o=r("508"),i=r("9797"),u=r("875");t.exports=function(t,n,r,c,a){e(n);var f=o(t),s=i(f),l=u(f.length),h=a?l-1:0,p=a?-1:1;if(r<2)for(;;){if(h in s){c=s[h],h+=p;break}if(h+=p,a?h<0:l<=h)throw TypeError("Reduce of empty array with no initial value")}for(;a?h>=0:l>h;h+=p)h in s&&(c=n(c,s[h],h,f));return c}},2736:function(t,n,r){var e=r("5286"),o=r("4302"),i=r("6314")("species");t.exports=function(t){var n;return o(t)&&("function"==typeof(n=t.constructor)&&(n===Array||o(n.prototype))&&(n=void 0),e(n)&&null===(n=n[i])&&(n=void 0)),void 0===n?Array:n}},6886:function(t,n,r){var e=r("2736");t.exports=function(t,n){return new(e(t))(n)}},1488:function(t,n,r){var e=r("2032"),o=r("6314")("toStringTag"),i="Arguments"==e(function(){return arguments}()),u=function(t,n){try{return t[n]}catch(t){}};t.exports=function(t){var n,r,c;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(r=u(n=Object(t),o))?r:i?e(n):"Object"==(c=e(n))&&"function"==typeof n.callee?"Arguments":c}},2032:function(t){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},5645:function(t){var n=t.exports={version:"2.5.7"};"number"==typeof __e&&(__e=n)},2811:function(t,n,r){"use strict";var e=r("9275"),o=r("681");t.exports=function(t,n,r){n in t?e.f(t,n,o(0,r)):t[n]=r}},741:function(t,n,r){var e=r("4963");t.exports=function(t,n,r){if(e(t),void 0===n)return t;switch(r){case 1:return function(r){return t.call(n,r)};case 2:return function(r,e){return t.call(n,r,e)};case 3:return function(r,e,o){return t.call(n,r,e,o)}}return function(){return t.apply(n,arguments)}}},1355:function(t){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},7057:function(t,n,r){t.exports=!r("4253")(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},2457:function(t,n,r){var e=r("5286"),o=r("3816").document,i=e(o)&&e(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},4430:function(t){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},2985:function(t,n,r){var e=r("3816"),o=r("5645"),i=r("7728"),u=r("7234"),c=r("741"),a="prototype",f=function(t,n,r){var s,l,h,p,v=t&f.F,d=t&f.G,y=t&f.S,g=t&f.P,m=t&f.B,_=d?e:y?e[n]||(e[n]={}):(e[n]||{})[a],b=d?o:o[n]||(o[n]={}),w=b[a]||(b[a]={});for(s in d&&(r=n),r)h=((l=!v&&_&&void 0!==_[s])?_:r)[s],p=m&&l?c(h,e):g&&"function"==typeof h?c(Function.call,h):h,_&&u(_,s,h,t&f.U),b[s]!=h&&i(b,s,p),g&&w[s]!=h&&(w[s]=h)};e.core=o,f.F=1,f.G=2,f.S=4,f.P=8,f.B=16,f.W=32,f.U=64,f.R=128,t.exports=f},4253:function(t){t.exports=function(t){try{return!!t()}catch(t){return!0}}},3816:function(t){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},9181:function(t){var n={}.hasOwnProperty;t.exports=function(t,r){return n.call(t,r)}},7728:function(t,n,r){var e=r("9275"),o=r("681");t.exports=r("7057")?function(t,n,r){return e.f(t,n,o(1,r))}:function(t,n,r){return t[n]=r,t}},639:function(t,n,r){var e=r("3816").document;t.exports=e&&e.documentElement},1734:function(t,n,r){t.exports=!r("7057")&&!r("4253")(function(){return 7!=Object.defineProperty(r("2457")("div"),"a",{get:function(){return 7}}).a})},9797:function(t,n,r){var e=r("2032");t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==e(t)?t.split(""):Object(t)}},6555:function(t,n,r){var e=r("2803"),o=r("6314")("iterator"),i=Array.prototype;t.exports=function(t){return void 0!==t&&(e.Array===t||i[o]===t)}},4302:function(t,n,r){var e=r("2032");t.exports=Array.isArray||function(t){return"Array"==e(t)}},5286:function(t){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},8851:function(t,n,r){var e=r("7007");t.exports=function(t,n,r,o){try{return o?n(e(r)[0],r[1]):n(r)}catch(n){var i=t.return;throw void 0!==i&&e(i.call(t)),n}}},9988:function(t,n,r){"use strict";var e=r("2503"),o=r("681"),i=r("2943"),u={};r("7728")(u,r("6314")("iterator"),function(){return this}),t.exports=function(t,n,r){t.prototype=e(u,{next:o(1,r)}),i(t,n+" Iterator")}},2923:function(t,n,r){"use strict";var e=r("4461"),o=r("2985"),i=r("7234"),u=r("7728"),c=r("2803"),a=r("9988"),f=r("2943"),s=r("468"),l=r("6314")("iterator"),h=!([].keys&&"next"in[].keys()),p="keys",v="values",d=function(){return this};t.exports=function(t,n,r,y,g,m,_){a(r,n,y);var b,w,x,A=function(t){if(!h&&t in S)return S[t];switch(t){case p:case v:break}return function(){return new r(this,t)}},E=n+" Iterator",T=g==v,k=!1,S=t.prototype,O=S[l]||S["@@iterator"]||g&&S[g],I=O||A(g),L=g?T?A("entries"):I:void 0,P="Array"==n&&S.entries||O;if(P&&(x=s(P.call(new t)))!==Object.prototype&&x.next&&(f(x,E,!0),!e&&"function"!=typeof x[l]&&u(x,l,d)),T&&O&&O.name!==v&&(k=!0,I=function(){return O.call(this)}),(!e||_)&&(h||k||!S[l])&&u(S,l,I),c[n]=I,c[E]=d,g){if(b={values:T?I:A(v),keys:m?I:A(p),entries:L},_)for(w in b)!(w in S)&&i(S,w,b[w]);else o(o.P+o.F*(h||k),n,b)}return b}},7462:function(t,n,r){var e=r("6314")("iterator"),o=!1;try{var i=[7][e]();i.return=function(){o=!0},Array.from(i,function(){throw 2})}catch(t){}t.exports=function(t,n){if(!n&&!o)return!1;var r=!1;try{var i=[7],u=i[e]();u.next=function(){return{done:r=!0}},i[e]=function(){return u},t(i)}catch(t){}return r}},5436:function(t){t.exports=function(t,n){return{value:n,done:!!t}}},2803:function(t){t.exports={}},4461:function(t){t.exports=!1},2503:function(t,n,r){var e=r("7007"),o=r("5588"),i=r("4430"),u=r("9335")("IE_PROTO"),c=function(){},a="prototype",f=function(){var t,n=r("2457")("iframe"),e=i.length;for(n.style.display="none",r("639").appendChild(n),n.src="javascript:",(t=n.contentWindow.document).open(),t.write("