diff --git a/package.json b/package.json index 0bb453f..7b07412 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "start": "cd src/site && node server.js", "assistant": "cd src/site && node assistant.js", + "build-lang": "cd src/lang/utils && node build_base.js", "test": "tap" }, "keywords": [], diff --git a/src/lang/base.js b/src/lang/base.js new file mode 100644 index 0000000..6fd2e37 --- /dev/null +++ b/src/lang/base.js @@ -0,0 +1,46 @@ +// This file was automatically generated and its contents will be overwritten later. + +const data = { + "go_to_profile": "MISSING STRING: go_to_profile", + "go_to_post": "MISSING STRING: go_to_post", + "go_username_or_url": "MISSING STRING: go_username_or_url", + "go_shortcode_or_url": "MISSING STRING: go_shortcode_or_url", + "go_button": "MISSING STRING: go_button", + "about_bibliogram_header": "MISSING STRING: about_bibliogram_header", + "pug_about_bibliogram_content": locals => "MISSING TEMPLATE: pug_about_bibliogram_content", + "about_this_instance_header": "MISSING STRING: about_this_instance_header", + "onion_site_available": "MISSING STRING: onion_site_available", + "t_settings": "MISSING STRING: t_settings", + "t_privacy_policy": "MISSING STRING: t_privacy_policy", + "has_not_written_privacy_policy": "MISSING STRING: has_not_written_privacy_policy", + "instance_not_blocked": "MISSING STRING: instance_not_blocked", + "instance_partially_blocked": "MISSING STRING: instance_partially_blocked", + "instance_blocked": "MISSING STRING: instance_blocked", + "rss_enabled": "MISSING STRING: rss_enabled", + "rss_disabled": "MISSING STRING: rss_disabled", + "external_links_header": "MISSING STRING: external_links_header", + "source_link": "MISSING STRING: source_link", + "matrix_link": "MISSING STRING: matrix_link", + "instances_link": "MISSING STRING: instances_link", + "contact_link": "MISSING STRING: contact_link", + "t_featured_profiles": "MISSING STRING: t_featured_profiles", + "featured_profiles_whats_this": "MISSING STRING: featured_profiles_whats_this", + "html_featured_profiles_disclaimer": "MISSING STRING: html_featured_profiles_disclaimer", + "verified_badge_alt": "MISSING STRING: verified_badge_alt", + "verified_badge_title": "MISSING STRING: verified_badge_title", + "post_counter_label": "MISSING STRING: post_counter_label", + "outgoing_follows_counter_label": "MISSING STRING: outgoing_follows_counter_label", + "incoming_follows_counter_label": "MISSING STRING: incoming_follows_counter_label", + "t_home": "MISSING STRING: t_home", + "tab_timeline": "MISSING STRING: tab_timeline", + "tab_igtv": "MISSING STRING: tab_igtv", + "next_page_button": "MISSING STRING: next_page_button", + "next_page_button_loading": "MISSING STRING: next_page_button_loading", + "profile_is_private_notice": "MISSING STRING: profile_is_private_notice", + "no_posts_notice": "MISSING STRING: no_posts_notice", + "no_more_posts_notice": "MISSING STRING: no_more_posts_notice", + "fn_page_divider": () => "MISSING FUNCTION: fn_page_divider", + "pug_post_timestamp": locals => "MISSING TEMPLATE: pug_post_timestamp" +} + +module.exports = data diff --git a/src/lang/en-us.js b/src/lang/en-us.js new file mode 100644 index 0000000..b670abc --- /dev/null +++ b/src/lang/en-us.js @@ -0,0 +1,3 @@ +const data = {...require("./en")} + +module.exports = data diff --git a/src/lang/en.js b/src/lang/en.js new file mode 100644 index 0000000..c93075d --- /dev/null +++ b/src/lang/en.js @@ -0,0 +1,71 @@ +const compile = require("pug").compile +const data = {...require("./base")} + +/** + * @param {string} text + */ +function pug(text) { + let lines = text.split("\n") + while (lines[0] === "") lines.shift() + const indentLevel = lines[0].match(/^\t*/)[0].length + lines = lines.map(l => l.replace(new RegExp(`^\\t{0,${indentLevel}}`), "")) + return compile(lines.join("\n")) +} + +;(() => { + data.go_to_profile = "Go to profile" + data.go_to_post = "Go to post" + data.go_username_or_url = "Username or URL" + data.go_shortcode_or_url = "Shortcode or URL" + data.go_button = "Go" + data.about_bibliogram_header = "About Bibliogram" + data.pug_about_bibliogram_content = pug(` + p. + Bibliogram is a website that takes data from Instagram's public profile views and puts it into + a friendlier page that loads faster, gives downloadable images, eliminates ads, + generates RSS feeds, and doesn't urge you to sign up. #[a(href=(link_to_featured_profiles ? "#featured-profiles" : "/u/instagram")).example-link See an example.] + p. + Bibliogram does #[em not] allow you to anonymously post, like, comment, follow, or view private profiles. + It does not preserve deleted posts. + `) + data.about_this_instance_header = "About this instance" + data.onion_site_available = "Onion site available" + data.t_settings = "Settings" + data.t_privacy_policy = "Privacy policy" + data.has_not_written_privacy_policy = "Owner has not written a privacy policy" + data.instance_not_blocked = "Instance is not blocked" + data.instance_partially_blocked = "Instance is partially blocked" + data.instance_blocked = "Instance is blocked" + data.rss_disabled = "RSS feeds are disabled" + data.rss_enabled = "RSS feeds are enabled" + data.external_links_header = "External links" + data.source_link = "Code on sourcehut" + data.matrix_link = "Discussion room on Matrix" + data.instances_link = "Other Bibliogram instances" + data.contact_link = "Contact the developer" + data.t_featured_profiles = "Featured profiles" + data.featured_profiles_whats_this = "What's this?" + data.html_featured_profiles_disclaimer = pug(` + p The owner of this website personally thinks that these profiles are interesting. + p These are not endorsements from the Bibliogram project. + `)() + data.verified_badge_title = "Verified" + data.verified_badge_alt = "Verified." + data.post_counter_label = "posts" + data.outgoing_follows_counter_label = "following" + data.incoming_follows_counter_label = "followed by" + data.t_home = "Home" + data.tab_timeline = "Timeline" + data.tab_igtv = "IGTV" + data.next_page_button = "Next page" + data.next_page_button_loading = "Loading..." + data.profile_is_private_notice = "Profile is private." + data.no_posts_notice = "No posts." + data.no_more_posts_notice = "No more posts." + data.fn_page_divider = number => `Page ${number}` + data.pug_post_timestamp = pug(` + | Posted on #[time(datetime=post.date.toISOString() data-local-date)= post.getDisplayDate()]. + `) +})() + +module.exports = data diff --git a/src/lang/index.js b/src/lang/index.js new file mode 100644 index 0000000..2be7393 --- /dev/null +++ b/src/lang/index.js @@ -0,0 +1,39 @@ +const base = require("./base") + +class Lang { + constructor() { + /** @type {Map} */ + this.backing = new Map() + + this.backing.set("base", require("./base")) + + for (const code of ["en", "en-us"]) { + // Assign lang + const data = require(`./${code}`) + this.backing.set(code, data) + // Check properties + for (const key of Object.keys(base)) { + if (!data[key] || data[key] === base[key]) { + console.log(`[!] [${code}] ${key} was not replaced`) + } + } + } + } + + /** + * @param {string} code + */ + get(code) { + if (this.backing.has(code)) { + // console.log(`[.] Getting language code ${code}`) + return this.backing.get(code) + } else { + console.log(`[!] WARNING: tried to get missing language code ${code}`) + return this.backing.get("base") + } + } +} + +const lang = new Lang() + +module.exports = lang diff --git a/src/lang/utils/base.template.js b/src/lang/utils/base.template.js new file mode 100644 index 0000000..570f4a8 --- /dev/null +++ b/src/lang/utils/base.template.js @@ -0,0 +1,7 @@ +// This file is a template. + +const data = { + // CONTENT +} + +module.exports = data diff --git a/src/lang/utils/base.txt b/src/lang/utils/base.txt new file mode 100644 index 0000000..2bfc5d0 --- /dev/null +++ b/src/lang/utils/base.txt @@ -0,0 +1,51 @@ +# Front page + +# Top section +go_to_profile +go_to_post +go_username_or_url +go_shortcode_or_url +go_button + +# Bottom section +about_bibliogram_header +pug_about_bibliogram_content +about_this_instance_header +onion_site_available +t_settings +t_privacy_policy +has_not_written_privacy_policy +instance_not_blocked +instance_partially_blocked +instance_blocked +rss_enabled +rss_disabled +external_links_header +source_link +matrix_link +instances_link +contact_link +t_featured_profiles +featured_profiles_whats_this +html_featured_profiles_disclaimer + +# User page + +verified_badge_alt +verified_badge_title +post_counter_label +outgoing_follows_counter_label +incoming_follows_counter_label +t_home +tab_timeline +tab_igtv +next_page_button +next_page_button_loading +profile_is_private_notice +no_posts_notice +no_more_posts_notice +fn_page_divider + +# Post page + +pug_post_timestamp diff --git a/src/lang/utils/build_base.js b/src/lang/utils/build_base.js new file mode 100644 index 0000000..b335eec --- /dev/null +++ b/src/lang/utils/build_base.js @@ -0,0 +1,28 @@ +const fs = require("fs").promises +const pj = require("path").join + +;(async () => { + const contents = await fs.readFile(pj(__dirname, "base.txt"), "utf8") + const lines = contents.split("\n") + let template = await fs.readFile(pj(__dirname, "base.template.js"), "utf8") + + template = template + .replace("// This file is a template.", "// This file was automatically generated and its contents will be overwritten later.") + .replace("// CONTENT", lines + .filter(l => l && !l.startsWith("#")) + .map(l => { + if (l.startsWith("pug_")) { + return `"${l}": locals => "MISSING TEMPLATE: ${l}"` + } else if (l.startsWith("fn_")) { + return `"${l}": () => "MISSING FUNCTION: ${l}"` + } else { + return `"${l}": "MISSING STRING: ${l}"` + } + }) + .join(",\n\t") + ) + + await fs.writeFile(pj(__dirname, "../base.js"), template, "utf8") + + console.log("base.js written.") +})() diff --git a/src/lib/constants.js b/src/lib/constants.js index 5eee7a3..92a06b8 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -107,6 +107,7 @@ let constants = { }, default_user_settings: { + language: "en", rewrite_youtube: "invidio.us", rewrite_twitter: "nitter.net" }, diff --git a/src/site/api/routes.js b/src/site/api/routes.js index de4d662..5a945bb 100644 --- a/src/site/api/routes.js +++ b/src/site/api/routes.js @@ -1,4 +1,5 @@ const constants = require("../../lib/constants") +const lang = require("../../lang") const switcher = require("../../lib/utils/torswitcher") const {fetchUser, getOrFetchShortcode, userRequestCache, history, assistantSwitcher} = require("../../lib/collectors") const {render, redirect, getStaticURL} = require("pinski/plugins") @@ -112,7 +113,8 @@ module.exports = [ username, expiresMinutes: userRequestCache.getTtl("user/"+username, 1000*60), getStaticURL, - settings + settings, + lang }) } } else if (error === constants.symbols.extractor_results.AGE_RESTRICTED) { @@ -179,7 +181,7 @@ module.exports = [ contentType: "application/json", content: { title: getPageTitle(post), - html: pugCache.get("pug/fragments/post.pug").web({post, settings, getStaticURL}) + html: pugCache.get("pug/fragments/post.pug").web({lang, post, settings, getStaticURL}) } } }).catch(error => { diff --git a/src/site/html/static/js/elemjs/elemjs.js b/src/site/html/static/js/elemjs/elemjs.js index 079e69d..ba38c15 100644 --- a/src/site/html/static/js/elemjs/elemjs.js +++ b/src/site/html/static/js/elemjs/elemjs.js @@ -57,7 +57,7 @@ class ElemJS { event(name, callback) { this.element.addEventListener(name, event => callback(event)) } - child(toAdd, position) { + child(toAdd, position = undefined) { if (typeof(toAdd) == "object") { toAdd.parent = this; if (typeof(position) == "number" && position >= 0) { diff --git a/src/site/html/static/js/pagination.js b/src/site/html/static/js/pagination.js index 2a523b1..1f26052 100644 --- a/src/site/html/static/js/pagination.js +++ b/src/site/html/static/js/pagination.js @@ -75,7 +75,7 @@ class NextPage extends FreezeWidth { fetch() { if (this.fetching) return this.fetching = true - this.freeze("Loading...") + this.freeze(this.element.getAttribute("data-loading-text")) const type = this.element.getAttribute("data-type") return fetch(`/fragment/user/${this.element.getAttribute("data-username")}/${this.nextPageNumber}?type=${type}`).then(res => res.text()).then(text => { diff --git a/src/site/pug/fragments/post.pug b/src/site/pug/fragments/post.pug index 2fbb353..79f4a96 100644 --- a/src/site/pug/fragments/post.pug +++ b/src/site/pug/fragments/post.pug @@ -1,5 +1,7 @@ //- Needs post +- const ll = lang.get(settings.language) + include ../includes/post.pug +post(post, true) diff --git a/src/site/pug/fragments/timeline_page.pug b/src/site/pug/fragments/timeline_page.pug index da2f6da..478a411 100644 --- a/src/site/pug/fragments/timeline_page.pug +++ b/src/site/pug/fragments/timeline_page.pug @@ -1,5 +1,7 @@ //- Needs user, selectedTimeline, url, type +- const ll = lang.get(settings.language) + include ../includes/timeline_page.pug include ../includes/next_page_button.pug diff --git a/src/site/pug/home.pug b/src/site/pug/home.pug index 51baccf..a733546 100644 --- a/src/site/pug/home.pug +++ b/src/site/pug/home.pug @@ -1,5 +1,7 @@ //- Needs rssEnabled, allUnblocked, torAvailable, hasPrivacyPolicy, onionLocation +- const ll = lang.get(settings.language) + doctype html html head @@ -13,57 +15,53 @@ html .go-sections-container .go-sections section - h2.title Go to profile + h2.title= ll.go_to_profile form(method="get" action="/u").pair-entry - input(type="text" name="u" placeholder="Username or URL").text - input(type="submit" value="Go").button + input(type="text" name="u" placeholder=ll.go_username_or_url).text + input(type="submit" value=ll.go_button).button section - h2.title Go to post + h2.title= ll.go_to_post form(method="get" action="/p").pair-entry - input(type="text" name="p" placeholder="Shortcode or URL").text - input(type="submit" value="Go").button + input(type="text" name="p" placeholder=ll.go_shortcode_or_url).text + input(type="submit" value=ll.go_button).button .about-container section.about - h2 About Bibliogram - p. - Bibliogram is a website that takes data from Instagram's public profile views and puts it into - a friendlier page that loads faster, gives downloadable images, eliminates ads, - generates RSS feeds, and doesn't urge you to sign up. #[a(href=(constants.featured_profiles.length ? "#featured-profiles" : "/u/instagram")).example-link See an example.] - p. - Bibliogram does #[em not] allow you to anonymously post, like, comment, follow, or view private profiles. - It does not preserve deleted posts. - - h2 About this instance + h2= ll.about_bibliogram_header + != ll.pug_about_bibliogram_content(constants.featured_profiles.length) + h2= ll.about_this_instance_header ul if onionLocation - li: a(href=onionLocation) Onion site available - li: a(href=settingsReferrer) Settings + li: a(href=onionLocation)= ll.onion_site_available + li: a(href=settingsReferrer)= ll.t_settings if hasPrivacyPolicy - li: a(href="/privacy") Privacy policy + li: a(href="/privacy")= ll.t_privacy_policy else - li Owner has not written a privacy policy + li= ll.has_not_written_privacy_policy if allUnblocked - li Instance is not blocked + li= ll.instance_not_blocked else - li: a(href="https://git.sr.ht/~cadence/bibliogram-docs/tree/master/docs/Instagram%20rate%20limits.md#tldr-what-does-it-mean-if-an-instance-is-blocked") Instance is partially blocked - li RSS feeds are #{rssEnabled ? "enabled" : "disabled"} + li: a(href="https://git.sr.ht/~cadence/bibliogram-docs/tree/master/docs/Instagram%20rate%20limits.md#tldr-what-does-it-mean-if-an-instance-is-blocked")= ll.instance_partially_blocked + if rssEnabled + li= ll.rss_enabled + else + li= ll.rss_disabled - h2 External links + h2= ll.external_links_header ul - const links = [ - ["https://sr.ht/~cadence/bibliogram/", "Code on sourcehut", "noopener noreferrer"], - ["https://matrix.to/#/#bibliogram:matrix.org", "Discussion room on Matrix", "noopener noreferrer"], - ["https://git.sr.ht/~cadence/bibliogram-docs/tree/master/docs/Instances.md", "Other Bibliogram instances", "noopener noreferrer"], - ["https://cadence.moe/about/contact", "Contact the developer", "noopener noreferrer"] + ["https://sr.ht/~cadence/bibliogram/", ll.source_link, "noopener noreferrer"], + ["https://matrix.to/#/#bibliogram:matrix.org", ll.matrix_link, "noopener noreferrer"], + ["https://git.sr.ht/~cadence/bibliogram-docs/tree/master/docs/Instances.md", ll.instances_link, "noopener noreferrer"], + ["https://cadence.moe/about/contact", ll.contact_link, "noopener noreferrer"] ] each entry in links li: a(href!=entry[0] target="_blank" rel=entry[2])= entry[1] if constants.featured_profiles.length .featured-profiles#featured-profiles - h2.featured-profiles-header Featured profiles + h2.featured-profiles-header= ll.featured_profiles_header table.featured-profile-table tbody @@ -73,7 +71,5 @@ html td= profile.description details - summary What's this? - .details-content - p The owner of this website personally thinks that these profiles are interesting. - p These are not endorsements from the Bibliogram project. + summary= ll.featured_profiles_whats_this + .details-content= ll.html_featured_profiles_disclaimer diff --git a/src/site/pug/includes/next_page_button.pug b/src/site/pug/includes/next_page_button.pug index d38a9b6..732a0ad 100644 --- a/src/site/pug/includes/next_page_button.pug +++ b/src/site/pug/includes/next_page_button.pug @@ -4,8 +4,14 @@ mixin next_page_button(user, selectedTimeline, url, type) - const nu = new URL(url) nu.searchParams.set("page", selectedTimeline.pages.length+1) - a(href=`${nu.search}#page-${selectedTimeline.pages.length+1}` data-page=(selectedTimeline.pages.length+1) data-username=(user.data.username) data-type=type)#next-page.next-page Next page + a( + href=`${nu.search}#page-${selectedTimeline.pages.length+1}` + data-page=(selectedTimeline.pages.length+1) + data-username=(user.data.username) + data-type=type + data-loading-text=ll.next_page_button_loading + )#next-page.next-page= ll.next_page_button else div div.page-number.no-more-pages - span.number No more posts. + span.number= ll.no_more_posts_notice diff --git a/src/site/pug/includes/post.pug b/src/site/pug/includes/post.pug index b12ed73..69e3acf 100644 --- a/src/site/pug/includes/post.pug +++ b/src/site/pug/includes/post.pug @@ -39,7 +39,7 @@ mixin post(post, headerWithNavigation) if caption p.description= caption p.description - span Posted on #[time(datetime=post.date.toISOString() data-local-date)= post.getDisplayDate()]. + span!= ll.pug_post_timestamp({post}) section.images-gallery for entry in post.children diff --git a/src/site/pug/includes/timeline_page.pug b/src/site/pug/includes/timeline_page.pug index ea96bfe..7cdf97d 100644 --- a/src/site/pug/includes/timeline_page.pug +++ b/src/site/pug/includes/timeline_page.pug @@ -5,7 +5,7 @@ mixin timeline_page(page, pageIndex) - const pageNumber = pageIndex + 1 if pageNumber > 1 header.page-number(id=`page-${pageNumber}`) - span.number Page #{pageNumber} + span.number= ll.fn_page_divider(pageNumber) .timeline-inner(class=`${settings.timeline_columns}-columns`) - const suggestedSize = 260 //- from css :( diff --git a/src/site/pug/post.pug b/src/site/pug/post.pug index ff3b34d..9b44244 100644 --- a/src/site/pug/post.pug +++ b/src/site/pug/post.pug @@ -1,5 +1,7 @@ //- Needs website_origin, title, post, settings +- const ll = lang.get(settings.language) + include includes/post doctype html diff --git a/src/site/pug/settings.pug b/src/site/pug/settings.pug index 817f763..4931a7d 100644 --- a/src/site/pug/settings.pug +++ b/src/site/pug/settings.pug @@ -45,7 +45,7 @@ html h1 Settings +fieldset("Features") - +select("language", "Language", true, [ + +select("language", "Language", false, [ {value: "en", text: "English (International)"}, {value: "en-us", text: "English (US)"} ]) diff --git a/src/site/pug/user.pug b/src/site/pug/user.pug index 00821fe..d5fba4e 100644 --- a/src/site/pug/user.pug +++ b/src/site/pug/user.pug @@ -5,13 +5,14 @@ include includes/next_page_button.pug include includes/display_structured include includes/feed_link +- const ll = lang.get(settings.language) - const numberFormat = new Intl.NumberFormat().format mixin selector-button(text, selectorType, urlSuffix) a(href=(type !== selectorType && `/u/${user.data.username}${urlSuffix}`) class=(type === selectorType && "active")).selector= text mixin verified-badge(classes) - img.verified-badge(class=classes src=getStaticURL("html", "/static/img/verified.svg") width=19 height=19 alt="Verified." title="Verified") + img.verified-badge(class=classes src=getStaticURL("html", "/static/img/verified.svg") width=19 height=19 alt=ll.verified_badge_alt title=ll.verified_badge_title) doctype html html @@ -42,7 +43,7 @@ html a(href="/").nav-icon-link img(src="/static/img/logo-circle-min.svg" alt="Bibliogram").logo a(href=settingsReferrer).nav-icon-link - img(src="/static/img/settings.svg" alt="Settings").settings + img(src="/static/img/settings.svg" alt=ll.t_settings).settings .main-divider header.profile-overview .profile-sticky @@ -71,12 +72,18 @@ html p.website a(href=userURL)= userURL if user.posts != undefined - div.profile-counter #[span(data-numberformat=user.posts).count #{numberFormat(user.posts)}] posts + div.profile-counter + | #[span(data-numberformat=user.posts).count #{numberFormat(user.posts)} ] + = ll.post_counter_label if followerCountsAvailable if user.following != undefined - div.profile-counter #[span(data-numberformat=user.following).count #{numberFormat(user.following)}] following + div.profile-counter + | #[span(data-numberformat=user.following).count #{numberFormat(user.following)} ] + = ll.outgoing_follows_counter_label if user.followedBy != undefined - div.profile-counter #[span(data-numberformat=user.followedBy).count #{numberFormat(user.followedBy)}] followed by + div.profile-counter + | #[span(data-numberformat=user.followedBy).count #{numberFormat(user.followedBy)} ] + = ll.incoming_follows_counter_label else div.profile-counter.not-available Followers not available. .links @@ -86,15 +93,15 @@ html a(rel="noreferrer noopener" href=`https://www.instagram.com/${user.data.username}` target="_blank") instagram.com section.bibliogram-meta .links - a(href="/") Home - a(href=settingsReferrer) Settings + a(href="/")= ll.t_home + a(href=settingsReferrer)= ll.t_settings - const hasPosts = !user.data.is_private && selectedTimeline.pages.length && selectedTimeline.pages[0].length .timeline-section .selector-container - +selector-button("Timeline", "timeline", "") + +selector-button(ll.tab_timeline, "timeline", "") if user.data.has_channel !== false - +selector-button("IGTV", "igtv", "/channel") + +selector-button(ll.tab_igtv, "igtv", "/channel") main(class=hasPosts ? "" : "no-posts")#timeline.timeline if hasPosts @@ -106,6 +113,6 @@ html div.page-number span.number if user.data.is_private - | Profile is private. + = ll.profile_is_private_notice else - | No posts. + = ll.no_posts_notice diff --git a/src/site/server.js b/src/site/server.js index 2e58e58..9648516 100644 --- a/src/site/server.js +++ b/src/site/server.js @@ -1,6 +1,7 @@ const {Pinski} = require("pinski") const {subdirs} = require("node-dir") const constants = require("../lib/constants") +const lang = require("../lang") const passthrough = require("./passthrough") @@ -64,7 +65,7 @@ subdirs("pug", async (err, dirs) => { const plugins = require("pinski/plugins") plugins.setInstance(pinski) - Object.assign(pinski.pugDefaultLocals, {constants}) + Object.assign(pinski.pugDefaultLocals, {constants, lang}) Object.assign(passthrough, pinski.getExports()) console.log("[.] Server started")