├── .gitignore ├── Dockerfile ├── Dockerfile.deploy ├── LICENSE.txt ├── README.md ├── ad-network ├── ad-database.js ├── ad-network.js ├── content │ ├── ad.html.ejs │ ├── index.html │ ├── product-level-ad.html.ejs │ ├── product.html.ejs │ └── static │ │ ├── ad-network-api.js │ │ └── bidding-function.js ├── context-evaluation.js └── prototypes.js ├── config.js ├── package-lock.json ├── package.json ├── turtledove-server ├── content │ ├── static │ │ ├── ad-remove.html │ │ ├── console.html │ │ ├── css │ │ │ ├── ad-wrapper.css │ │ │ ├── normalize.css │ │ │ ├── skeleton-tabs.css │ │ │ ├── skeleton.css │ │ │ └── tooltip.css │ │ ├── images │ │ │ ├── favicon.png │ │ │ └── turtledove-question-mark.png │ │ ├── js │ │ │ ├── common.js │ │ │ ├── iframe-api.js │ │ │ ├── log.js │ │ │ ├── render-ad.js │ │ │ ├── skeleton-tabs.js │ │ │ ├── storage-keys.js │ │ │ ├── store.js │ │ │ └── user-interface.js │ │ ├── product-remove.html │ │ └── user-interface.html │ ├── turtledove-console.js │ └── turtledove.js └── turtledove-server.js └── websites ├── content ├── aboutanimals.html.ejs ├── aboutplanes.html.ejs ├── catordog-product.html.ejs ├── catordog.html.ejs ├── clothes-category.html.ejs ├── clothes-product.html.ejs ├── clothes.html.ejs ├── sportequipment-product.html.ejs ├── sportequipment.html.ejs ├── static │ ├── css │ │ └── complete.css │ ├── images │ │ ├── animals.png │ │ ├── bikes-blue.svg │ │ ├── bikes-green.svg │ │ ├── bikes-red.svg │ │ ├── bikes.svg │ │ ├── black-caps.svg │ │ ├── black-jackets.svg │ │ ├── black-scarfs.svg │ │ ├── blue-caps.svg │ │ ├── blue-jackets.svg │ │ ├── blue-scarfs.svg │ │ ├── cat.svg │ │ ├── dog.svg │ │ ├── eagle-background.png │ │ ├── favicon.png │ │ ├── green-caps.svg │ │ ├── green-jackets.svg │ │ ├── green-scarfs.svg │ │ ├── lot.svg │ │ ├── pkp.png │ │ ├── plane-background.png │ │ ├── plane-background.svg │ │ ├── plane.svg │ │ ├── purple-caps.svg │ │ ├── purple-jackets.svg │ │ ├── purple-scarfs.svg │ │ ├── red-caps.svg │ │ ├── red-jackets.svg │ │ ├── red-scarfs.svg │ │ ├── rollerblades-blue.svg │ │ ├── rollerblades-green.svg │ │ ├── rollerblades-red.svg │ │ ├── rollerblades.svg │ │ ├── scooters-blue.svg │ │ ├── scooters-green.svg │ │ ├── scooters-red.svg │ │ ├── scooters.svg │ │ ├── train.svg │ │ └── train2.png │ └── js │ │ └── retargeting.js └── trainorplane.html.ejs └── websites.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Generated tags 108 | tags 109 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | WORKDIR /usr/src/app 4 | COPY package*.json ./ 5 | 6 | RUN npm install 7 | COPY . . 8 | EXPOSE 8000 8001 8002 8003 8004 8005 8007 8008 9 | CMD npm start 10 | -------------------------------------------------------------------------------- /Dockerfile.deploy: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | WORKDIR /usr/src/app 4 | COPY package*.json ./ 5 | 6 | RUN npm install 7 | COPY . . 8 | EXPOSE 8000 8001 8002 8003 8004 8005 8007 8008 9 | CMD npm run deploy 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 RTB House 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository is archived 2 | Now, as [FLEDGE](https://github.com/WICG/turtledove/blob/main/FLEDGE.md) experiment is live, we don't need to simulate TURTLEDOVE. Additionally, with shared storage partitioning based on the toplevel site, the proposed demo does not work like expected. Because of that, there is no need for keeping this repository alive. 3 | 4 | # Goal 5 | This repository contains an implementation of TURTLEDOVE (https://github.com/WICG/turtledove). We aim to provide a solution that will be so similar to the final standard, that it could be a drop-in replacement of it. Until the proposal is implemented in browsers you can take advantage of this project to override proposed Navigator object's methods and use it the same way you would use the original TURTLEDOVE. As we can't modify browser code by itself, our implementation is based on existing technologies: _localStorage_ used by embedded _iframes_ and communicating with the main website by _postMessages_. This way all data is stored locally inside a browser and only the script serving domain has access to the private data. 6 | 7 | Alongside the core code, we implemented a few sample websites. Everything is currently available on the Internet, so you can play with our demo without worrying about its deployment. The functionality of the core scripts is not limited to a few presented pages - everyone can write such a sample, one just needs some dummy ad network, an advertiser that will put that ad network in the `readers` field, and a publisher that is 'integrated' with the very same ad network. 8 | 9 | # Usage 10 | To use our implementation just import a file `turtledove.js` and call `initTurtledove`: 11 | ``` 12 | import { initTurtledove } from "https://turtledove.pl/turtledove.js" 13 | 14 | initTurtledove({ logs: true, productLevel: true }) 15 | ``` 16 | 17 | Initializing function consumes a config object which recognizes two keys: 18 | - `logs`- if set to `true`, adds a button to open a message log that gives a bit of insight into implementation internals. 19 | - `productLevel`- if set to `true`, enables products fetching and admits product-level ads to auctions. To read more about proposal of Product-Level TURTLEDOVE check out https://github.com/jonasz/product_level_turtledove 20 | 21 | Calling `initTurtledove` overwrites functions `window.navigator.renderAds`, `window.navigator.joinAdInterestGroup` and `window.navigator.leaveAdInterestGroup`, that in the future would be provided by a browser. 22 | 23 | # Demo websites 24 | As stated before our implementation consists not only of a core but also a few websites that are using it to render some ads. 25 | All websites mentioned here are initializing TURTLEDOVE with parameter `logs` set to `true`, so on every page, there is a 26 | small question mark with turtledove on it - after clicking there the console will open on the bottom of a screen. The whole logging idea is added only for visualization purposes - it is not a part of the TURTLEDOVE standard. 27 | 28 | Besides reading logs you may find it helpful to open developer tools in the browser and peek at request flow. 29 | 30 | ## Turtledove server 31 | This is the central part of the demo, hosted on the Internet at https://turtledove.pl. It serves a _turtledove.js_ file containing functions that in the future will be browser's API. 32 | It also hosts addresses _/store_ and _/render-ad_ that are the source of iframes that are, respectively, saving ad interest groups or performing on-device auction of private ads. 33 | 34 | The last function of the turtledove server is exposing a set of files to add a console with a log of TURTLEDOVE events, and a simple user interface available on the main page http://turtledove.pl (such a panel also will be a part of the browser in the future). 35 | 36 | The important thing to remember is that the server by itself is **NOT** a part of TURTLEDOVE, which by definition is not related to any third-party servers. The whole point of the turtledove-server is just to host a few javascript files that later are performing all operations **locally in the browser**. 37 | 38 | ## Publishers 39 | 40 | TURTLEDOVE ads can be seen in two example publishers: 41 | - https://aboutplanes.pl - a publisher, that don't like ads about the other means of transport than planes 42 | - https://aboutanimals.pl - a publisher on which ad-network values more ads about animals 43 | 44 | 45 | ## Advertisers 46 | We wrote three web pages that are creating AdInterestGroups: 47 | - https://coolclothes.pl - a shop that is performing product-level retargeting. 48 | - https://sportequipment.pl - a shop which is performing naive retargeting by showing ads only for last viewed category. 49 | - https://catordog.pl - an advertiser that asks you if you like cats or dogs and afterward shows you its ads promoting 50 | either cat or dog food. If you didn't declare anything, it will show you a generic ad for its visitors. Quite the opposite, 51 | if you click on an ad and then buy food, then you will see higher valued ads for food buyers. 52 | - https://trainorplane.pl - an advertiser that just asks you about your locomotion preferences and then shows you related ads. 53 | 54 | ## Ad network 55 | This part is a dummy company that serves ads. It contains some hardcoded data to customize responses for publishers and advertisers from this sample. 56 | The interface between _turtledove.js_ and ad network have to be rigid and to allow easy implementation of new ad networks is published in a separate `turtledove-js-api` npm package. 57 | On the other hand an interface between publishers and the ad network is completely arbitrary and is not an element of TURTLEDOVE specification. Our ad network is available at address https://ad-network.pl 58 | 59 | # Sample testing scenario 60 | 1. If you already entered on any website in this demo, go to https://turtledove.pl and clear your data. 61 | 2. Enter https://aboutplanes.pl and see contextual ads (you were not added to any group yet). To get the better insight you can also open a console by clicking on a question mark with a turtledove on it in the top-right corner and read what just happened. 62 | 3. Go to the https://sportequipment.pl and check out bikes page. 63 | 4. Check both https://aboutplanes.pl and https://aboutanimals.pl. The bike-related ad will not show on a website about planes. You can go now to https://turtledove.pl and check the bidding function of the ad for bikes_viewer and context signals of auctions that were performed on both websites - that will explain why winners differ in both auctions. 64 | 5. Go to the https://sportequipment.pl and this time list rollerblades. 65 | 6. Check both https://aboutplanes.pl and https://aboutanimals.pl. Now rollerblades ad should be everywhere (excluding the best context spot on the right side of aboutplanes.pl), as it is quite valuable and not denied on any site. 66 | 7. Go to the https://turtledove.pl and check out that the bikes ad was removed when rollerblades ad appeared. 67 | 8. Go to https://catordog.pl and select the kind of pets that you like. 68 | 9. Once again see both https://aboutplanes.pl and https://aboutanimals.pl. This time animals-related ads should be shown at aboutanimals.pl, as they are the best fit for the site topic. 69 | 10. Assume you don't like an ad for cat/dog lovers. Hover over a small question mark at the top of an ad iframe, read the description, and remove this ad. Refresh a website to check if it vanished indeed. 70 | 71 | # Open issues / comments 72 | - This simulation is a continuously developed **unofficial** prototype. It is not the reference implementation, we just built that to get a better understanding of how future changes will affect the advertising business. 73 | - The base of our demo is localStorage. We use that because it's what we have access to. Maybe in the future, it will not be available but hopefully, at the same time, proper TURTLEDOVE will be. **Users who have disabled localStorage in the browser will not be able to use this demo.** 74 | - We are fetching ads exactly in the time joining AdInterestGroup. It will not be the case in TURTLEDOVE, but for our demo, it's very handy simplification. 75 | - Our iframes are just plain old-school iframes versus fancy fenced iframes that probably will be used in final implementation (see https://github.com/shivanigithub/fenced-frame) 76 | - We have no aggregated reporting. Or rather no reporting at all. It is out of scope of this demo, at least for now. 77 | - Both publisher and advertiser have to specify the same ad network. A collaboration of ad networks is a big topic and is not clarified at all in the proposal itself yet. 78 | - Context bids are forcefully fitted into the same rendering regime as behavioral ads - this was not specified in original TURTLEDOVE, but it has certainly its advantages and probably will be a subject of ongoing discussions. 79 | - Our `renderAds`, in contrast to proposed `renderInterestGroupAd`, supports multiple ad networks in a single auction. 80 | - Ad network responses differ from described in the proposal. However, its only consumer is our demo which differs significantly in the internal details from the final TURTLEDOVE anyway. 81 | - Interest group membership timeout is not implemented 82 | 83 | # Launch 84 | As stated before, all of those samples are running live under the provided addresses - however, if you'd like to, you can experiment with the code on your own and run these samples locally. You have two basic ways to do this. 85 | 86 | ## With node 14 87 | Just go to the folder with the repository and run: 88 | ``` 89 | npm install 90 | npm start 91 | ``` 92 | Note, that you'll need version 14 of node.js to be able to run this app. 93 | 94 | ## With docker 95 | If you don't want to bother with node.js, you can just build a docker image and run it: 96 | ``` 97 | docker build -t td-demo . 98 | docker run -ti -p 8000-8008:8000-8008 td-demo 99 | ``` 100 | -------------------------------------------------------------------------------- /ad-network/ad-database.js: -------------------------------------------------------------------------------- 1 | import { addresses } from '../config.js' 2 | import { SimpleAdPrototype, ProductLevelAdPrototype } from './prototypes.js' 3 | 4 | function extractHost (url) { 5 | return new URL(url).host 6 | } 7 | 8 | function generateAd (address, groupName, img, href, baseValue) { 9 | const fullName = extractHost(address) + '_' + groupName 10 | if (href.length === 0 || href[0] === '/') { 11 | href = address + href 12 | } 13 | if (img[0] === '/') { 14 | img = address + img 15 | } 16 | return new SimpleAdPrototype(fullName, img, href, baseValue) 17 | } 18 | 19 | function generateProductLevelAd (address, groupName, productsCount, baseValue) { 20 | const fullName = extractHost(address) + '_' + groupName 21 | return new ProductLevelAdPrototype(fullName, 1, productsCount, baseValue) 22 | } 23 | 24 | export const adsDb = [ 25 | generateAd(addresses.animalsAdvertiser, 'animals_visitor', '/static/images/animals.png', '', 0.1), 26 | generateAd(addresses.animalsAdvertiser, 'cats_lover', '/static/images/cat.svg', '/catfood', 0.4), 27 | generateAd(addresses.animalsAdvertiser, 'catfood_buyer', '/static/images/cat.svg', '/catfood', 2.8), 28 | generateAd(addresses.animalsAdvertiser, 'dogs_lover', '/static/images/dog.svg', '/dogfood', 0.3), 29 | generateAd(addresses.animalsAdvertiser, 'dogfood_buyer', '/static/images/dog.svg', '/dogfood', 2.7), 30 | generateAd(addresses.transportAdvertiser, 'trains_lover', '/static/images/pkp.png', 'https://pkp.pl', 0.5), 31 | generateAd(addresses.transportAdvertiser, 'planes_lover', '/static/images/lot.svg', 'https://lot.com', 0.6), 32 | generateAd(addresses.sportEquipmentAdvertiser, 'scooters_viewer', '/static/images/scooters.svg', '/scooters', 0.8), 33 | generateAd(addresses.sportEquipmentAdvertiser, 'bikes_viewer', '/static/images/bikes.svg', '/bikes', 0.9), 34 | generateAd(addresses.sportEquipmentAdvertiser, 'rollerblades_viewer', '/static/images/rollerblades.svg', '/rollerblades', 1.1), 35 | generateProductLevelAd(addresses.clothesAdvertiser, 'visitor', 3, 2) 36 | ] 37 | -------------------------------------------------------------------------------- /ad-network/ad-network.js: -------------------------------------------------------------------------------- 1 | import { generateAdNetworkAppAsync, ContextualBidResponse } from 'turtledove-js-api' 2 | import { ProductPrototype } from './prototypes.js' 3 | import express from 'express' 4 | 5 | import fs from 'fs' 6 | import path from 'path' 7 | 8 | import { extractContextSignals, getContextualAd } from './context-evaluation.js' 9 | import { ports, addresses } from '../config.js' 10 | import { adsDb } from './ad-database.js' 11 | 12 | const __dirname = path.resolve('./ad-network') 13 | 14 | /** 15 | * For given InterestGroup returns list of prepared InterestGroupAds, in the format described by TURTLEDOVE. 16 | * @param {string} interestGroupId 17 | * @returns {Promise} 18 | */ 19 | async function fetchAds (interestGroupId) { 20 | console.log(`Fetch ads for: ${interestGroupId}`) 21 | const prototypes = adsDb.filter(adPrototype => adPrototype.adTarget === interestGroupId) 22 | const bidFunctionUrl = addresses.adPartner + '/static/bidding-function.js' 23 | return await Promise.all(prototypes.map(p => p.convertToAd(interestGroupId, bidFunctionUrl))) 24 | } 25 | 26 | /** 27 | * Receives some information about the context (in the format settled between publisher and ad-network). Basing on this, 28 | * ad-network is returning contextSignals (later fed to bidding function from InterestGroupAd) and purely contextual ad. 29 | * @param {ContextualBidRequest} contextualBidRequest 30 | * @returns {Promise} 31 | */ 32 | async function fetchContextualBid (contextualBidRequest) { 33 | console.log(`Fetch contextual bid for: ${JSON.stringify(contextualBidRequest)}`) 34 | const contextSignals = extractContextSignals(contextualBidRequest) 35 | const evaluatedContextualAd = await getContextualAd(contextualBidRequest, contextSignals) 36 | return new ContextualBidResponse(contextSignals, evaluatedContextualAd?.contextualAd, evaluatedContextualAd?.bidValue) 37 | } 38 | 39 | /** 40 | * Returns a single product that can be used on product-level ads. 41 | */ 42 | async function fetchProducts (owner, productId) { 43 | console.log(`Fetch product ${productId} for: ${owner}`) 44 | const productPrototype = new ProductPrototype(owner, productId) 45 | return await productPrototype.convertToProduct() 46 | } 47 | 48 | const app = generateAdNetworkAppAsync(fetchAds, fetchContextualBid, fetchProducts) 49 | app.use('/static', express.static(`${__dirname}/content/static`)) 50 | app.get('/', (req, res) => { 51 | res.set('Content-Type', 'text/html') 52 | res.send(fs.readFileSync(`${__dirname}/content/index.html`)) 53 | }) 54 | app.listen(ports.adPartnerPort, 55 | () => console.log(`Ad network listening at http://localhost:${ports.adPartnerPort}`)) 56 | -------------------------------------------------------------------------------- /ad-network/content/ad.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ad 6 | 7 | 35 | 36 | 37 | <% if (it.href) { %> 38 | 39 | <% } %> 40 |
41 |

Ad for <%= it.adTarget %>

42 | Offer image 43 |
44 | <% if (it.href) { %> 45 |
46 | <% } %> 47 | 48 | 49 | -------------------------------------------------------------------------------- /ad-network/content/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example ad network 6 | 7 | 8 | 22 | 23 | 24 |
25 |

This is an address of an example ad network

26 |

It's a part of the TURTLEDOVE simulation that you can test in your browser. Check out more in our Github repository

27 |
28 | 29 | -------------------------------------------------------------------------------- /ad-network/content/product-level-ad.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Product-Level Ad 6 | 7 | 44 | 45 | 46 | 59 |
60 |

Ad for <%= it.adTarget %>

61 |
62 |
63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /ad-network/content/product.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 42 | 43 | 44 | 45 | ' target="_top"> 46 |
47 | 48 |
<%= it.title %>
49 |
50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /ad-network/content/static/ad-network-api.js: -------------------------------------------------------------------------------- 1 | export class AdPolicy { 2 | constructor (deniedTerms) { 3 | this.deniedTerms = deniedTerms 4 | } 5 | } 6 | 7 | export class Placement { 8 | constructor (side) { 9 | this.side = side 10 | } 11 | } 12 | 13 | export class ContextualBidRequest { 14 | constructor (topic, placement, adPolicy) { 15 | this.site = window.location.host 16 | this.topic = topic 17 | this.placement = placement 18 | this.adPolicy = adPolicy 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ad-network/content/static/bidding-function.js: -------------------------------------------------------------------------------- 1 | (contextSignals, interestGroupSignals) => { 2 | if (contextSignals?.deniedTerms?.find((t) => interestGroupSignals?.name.includes(t)) !== undefined) { 3 | return 0 4 | } 5 | const igOwnerBonusMap = contextSignals?.igOwnerBonus 6 | 7 | const baseValue = interestGroupSignals?.baseValue || 0 8 | const igBonus = igOwnerBonusMap !== undefined ? igOwnerBonusMap[interestGroupSignals?.owner] || 0 : 0 9 | return baseValue + igBonus 10 | } 11 | -------------------------------------------------------------------------------- /ad-network/context-evaluation.js: -------------------------------------------------------------------------------- 1 | import { SimpleAdPrototype } from './prototypes.js' 2 | import { addresses } from '../config.js' 3 | 4 | class ContextSignals { 5 | constructor (topic, deniedTerms, igOwnerBonus) { 6 | this.topic = topic 7 | this.deniedTerms = deniedTerms 8 | this.igOwnerBonus = igOwnerBonus 9 | } 10 | } 11 | 12 | class EvaluatedContextualAd { 13 | constructor (contextualAd, bidValue) { 14 | this.contextualAd = contextualAd 15 | this.bidValue = bidValue 16 | } 17 | } 18 | 19 | /** 20 | * Returns context signals that will be fed to bidding function during the on-device auction. Returned signals are completely free-form JSON. 21 | * @param {ContextualBidRequest} contextualBidRequest 22 | * @returns {ContextSignals} 23 | */ 24 | export function extractContextSignals (contextualBidRequest) { 25 | const igOwnerBonus = {} 26 | if (contextualBidRequest?.topic === 'animals') { 27 | igOwnerBonus[new URL(addresses.animalsAdvertiser).host] = 1 28 | } 29 | return new ContextSignals(contextualBidRequest?.topic, contextualBidRequest?.adPolicy?.deniedTerms, igOwnerBonus) 30 | } 31 | 32 | /** 33 | * Returns contextual ad based on received ContextualBidRequest and previously extracted ContextSignals 34 | * @param {ContextualBidRequest} contextualBidRequest 35 | * @param {ContextSignals} contextSignals 36 | * @returns {Promise} 37 | */ 38 | export async function getContextualAd (contextualBidRequest, contextSignals) { 39 | const id = contextSignals.topic + '-' + contextualBidRequest.placement?.side 40 | const isOnRight = contextualBidRequest.placement?.side === 'right' 41 | const isOnTransportSite = addresses.planesPublisher.includes(contextualBidRequest?.site) 42 | const bidValue = isOnRight ? (isOnTransportSite ? 2 : 0.15) : 0.05 43 | const ctxAdPrototype = new SimpleAdPrototype('context_' + contextSignals.topic, `https://picsum.photos/seed/${id}/280/180`, '', bidValue) 44 | return new EvaluatedContextualAd(await ctxAdPrototype.convertToAd(), bidValue) 45 | } 46 | -------------------------------------------------------------------------------- /ad-network/prototypes.js: -------------------------------------------------------------------------------- 1 | import eta from 'eta' 2 | import path from 'path' 3 | import { addresses } from '../config.js' 4 | import { ContextualAd, InterestGroupAd, ProductLevelInterestGroupAd, Product } from 'turtledove-js-api' 5 | 6 | const __dirname = path.resolve('./ad-network') 7 | 8 | function hsvToRgb (h, s, v) { 9 | const i = Math.floor(h * 6) 10 | const f = h * 6 - i 11 | const p = v * (1 - s) 12 | const q = v * (1 - f * s) 13 | const t = v * (1 - (1 - f) * s) 14 | let r, g, b 15 | switch (i % 6) { 16 | case 0: 17 | r = v 18 | g = t 19 | b = p 20 | break 21 | case 1: 22 | r = q 23 | g = v 24 | b = p 25 | break 26 | case 2: 27 | r = p 28 | g = v 29 | b = t 30 | break 31 | case 3: 32 | r = p 33 | g = q 34 | b = v 35 | break 36 | case 4: 37 | r = t 38 | g = p 39 | b = v 40 | break 41 | case 5: 42 | r = v 43 | g = p 44 | b = q 45 | break 46 | } 47 | return { 48 | r: Math.round(r * 255), 49 | g: Math.round(g * 255), 50 | b: Math.round(b * 255) 51 | } 52 | } 53 | 54 | function rgbToString (rgb) { 55 | return 'rgb(' + rgb.r + ', ' + rgb.g + ',' + rgb.b + ')' 56 | } 57 | 58 | class InterestGroupSignals { 59 | constructor (owner, name, baseValue) { 60 | this.owner = owner 61 | this.name = name 62 | this.baseValue = baseValue 63 | } 64 | } 65 | 66 | /** 67 | * Returns interest group signals - data that are used in the bidding function for a given ad prototype. 68 | * @param {AdPrototype} interestGroupId 69 | * @returns {InterestGroupSignals} 70 | */ 71 | function computeInterestGroupSignals (adPrototype) { 72 | const owner = adPrototype.adTarget.split('_')[0] 73 | const name = adPrototype.adTarget.substring(owner.length + 1) 74 | return new InterestGroupSignals(owner, name, adPrototype.baseValue) 75 | } 76 | 77 | class AdPrototype { 78 | constructor (id, adTarget, baseValue, adPath) { 79 | this.id = id 80 | this.adTarget = adTarget 81 | this.adPath = adPath 82 | this.baseValue = baseValue 83 | this.adPartner = addresses.adPartner 84 | } 85 | 86 | async generateAdHtml () { 87 | return await eta.renderFile(path.join(__dirname, this.adPath), this) 88 | } 89 | 90 | /** 91 | * This function is converting prototype to final Ad that will be sent to browser. 92 | */ 93 | async convertToAd (interestGroupId, bidFunctionUrl) { 94 | const html = await this.generateAdHtml() 95 | if (interestGroupId === undefined) { 96 | return new ContextualAd(this.id, html, addresses.adPartner) 97 | } 98 | const signals = computeInterestGroupSignals(this) 99 | if (this.productsCount === undefined) { 100 | return new InterestGroupAd(this.id, interestGroupId, html, signals, bidFunctionUrl, addresses.adPartner) 101 | } 102 | const productsOwner = interestGroupId.split('_')[0] 103 | return new ProductLevelInterestGroupAd(this.id, interestGroupId, html, signals, bidFunctionUrl, productsOwner, this.minProductsCount, this.productsCount, addresses.adPartner) 104 | } 105 | } 106 | 107 | export class SimpleAdPrototype extends AdPrototype { 108 | constructor (adTarget, image, href, baseValue) { 109 | super(adTarget, adTarget, baseValue, '/content/ad.html.ejs') 110 | this.image = image 111 | this.href = href 112 | const rgb = hsvToRgb(Math.random(), 0.15, 0.95) 113 | this.background = rgbToString(rgb) 114 | } 115 | } 116 | 117 | export class ProductLevelAdPrototype extends AdPrototype { 118 | constructor (adTarget, minProductsCount, productsCount, baseValue) { 119 | super(adTarget + '#PLTD', adTarget, baseValue, '/content/product-level-ad.html.ejs') 120 | const rgb = hsvToRgb(Math.random(), 0.3, 0.95) 121 | this.background = rgbToString(rgb) 122 | this.minProductsCount = minProductsCount 123 | this.productsCount = productsCount 124 | } 125 | } 126 | 127 | export class ProductPrototype { 128 | constructor (owner, productId) { 129 | this.owner = owner 130 | this.productId = productId 131 | this.title = productId.replace('-', ' ') 132 | this.adPartner = addresses.adPartner 133 | } 134 | 135 | async generateHtml () { 136 | return await eta.renderFile(path.join(__dirname, '/content/product.html.ejs'), this) 137 | } 138 | 139 | async convertToProduct () { 140 | return new Product(this.owner, this.productId, await this.generateHtml()) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | export const ports = { 2 | turtledovePort: 8008, 3 | adPartnerPort: 8007, 4 | animalsPublisherPort: 8000, 5 | planesPublisherPort: 8001, 6 | animalsAdvertiserPort: 8002, 7 | transportAdvertiserPort: 8003, 8 | sportEquipmentAdvertiserPort: 8004, 9 | clothesAdvertiserPort: 8005 10 | } 11 | 12 | const addressesVariants = { 13 | web: { 14 | turtledoveHost: 'https://turtledove.pl', 15 | adPartner: 'https://ad-network.pl', 16 | animalsPublisher: 'https://aboutanimals.pl', 17 | planesPublisher: 'https://aboutplanes.pl', 18 | animalsAdvertiser: 'https://catordog.pl', 19 | transportAdvertiser: 'https://trainorplane.pl', 20 | sportEquipmentAdvertiser: 'https://sportequipment.pl', 21 | clothesAdvertiser: 'https://coolclothes.pl' 22 | }, 23 | local: { 24 | turtledoveHost: 'http://localhost:' + ports.turtledovePort, 25 | adPartner: 'http://localhost:' + ports.adPartnerPort, 26 | animalsPublisher: 'http://localhost:' + ports.animalsPublisherPort, 27 | planesPublisher: 'http://localhost:' + ports.planesPublisherPort, 28 | animalsAdvertiser: 'http://localhost:' + ports.animalsAdvertiserPort, 29 | transportAdvertiser: 'http://localhost:' + ports.transportAdvertiserPort, 30 | sportEquipmentAdvertiser: 'http://localhost:' + ports.sportEquipmentAdvertiserPort, 31 | clothesAdvertiser: 'http://localhost:' + ports.clothesAdvertiserPort 32 | } 33 | } 34 | export const addresses = process.argv[2] === 'prod' ? addressesVariants.web : addressesVariants.local 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "turtledove-js", 3 | "description": "TURTLEDOVE implementation and examples", 4 | "main": "turtledove-server/turtledove-server.js", 5 | "type": "module", 6 | "scripts": { 7 | "start": "concurrently \"nodemon ad-network/ad-network.js\" \"nodemon turtledove-server/turtledove-server.js\" \"nodemon websites/websites.js\"", 8 | "deploy": "concurrently \"node ad-network/ad-network.js prod\" \"node turtledove-server/turtledove-server.js prod\" \"node websites/websites.js prod\"", 9 | "test": "standard" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/dervan/turtledove-js" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "body-parser": "^1.19.0", 19 | "concurrently": "^5.2.0", 20 | "cors": "^2.8.5", 21 | "eta": "^1.2.2", 22 | "express": "^4.17.1", 23 | "madge": "^3.10.0", 24 | "nodemo": "^1.0.0", 25 | "turtledove-js-api": "^1.2.0" 26 | }, 27 | "devDependencies": { 28 | "babel-eslint": "^10.1.0", 29 | "nodemon": "^2.0.4", 30 | "standard": "^14.3.4" 31 | }, 32 | "standard": { 33 | "ignore": [ 34 | "ad-network/content/static/bidding-function.js" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /turtledove-server/content/static/ad-remove.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TURTLEDOVE demo UI 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 41 |
42 |

Ad removed!

43 |

It will not show again.

44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /turtledove-server/content/static/console.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TURTLEDOVE demo logs 6 | 7 | 8 | 9 | 10 | 46 | 47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
TimeWebsiteLog
60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /turtledove-server/content/static/css/ad-wrapper.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | border: none; 4 | padding: 0; 5 | margin: 0; 6 | } 7 | 8 | iframe { 9 | border: none; 10 | } -------------------------------------------------------------------------------- /turtledove-server/content/static/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /turtledove-server/content/static/css/skeleton-tabs.css: -------------------------------------------------------------------------------- 1 | 2 | ul.tab-nav { 3 | list-style: none; 4 | border-bottom: 1px solid #bbb; 5 | padding-left: 5px; 6 | } 7 | 8 | ul.tab-nav li { 9 | display: inline; 10 | } 11 | 12 | ul.tab-nav li a.button { 13 | border-bottom-left-radius: 0; 14 | border-bottom-right-radius: 0; 15 | margin-bottom: -1px; 16 | border-bottom: none; 17 | } 18 | 19 | ul.tab-nav li a.active.button { 20 | border-bottom: 0.2em solid #ebdcff; 21 | } 22 | 23 | .tab-content .tab-pane { 24 | display: none; 25 | } 26 | 27 | .tab-content .tab-pane.active { 28 | display: block; 29 | } 30 | -------------------------------------------------------------------------------- /turtledove-server/content/static/css/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | nitialized\* 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 128 | color: #222; } 129 | 130 | 131 | /* Typography 132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 133 | h1, h2, h3, h4, h5, h6 { 134 | margin-top: 0; 135 | margin-bottom: 2rem; 136 | font-weight: 300; } 137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 143 | 144 | /* Larger than phablet */ 145 | @media (min-width: 550px) { 146 | h1 { font-size: 5.0rem; } 147 | h2 { font-size: 4.2rem; } 148 | h3 { font-size: 3.6rem; } 149 | h4 { font-size: 3.0rem; } 150 | h5 { font-size: 2.4rem; } 151 | h6 { font-size: 1.5rem; } 152 | } 153 | 154 | p { 155 | margin-top: 0; } 156 | 157 | 158 | /* Links 159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 160 | a { 161 | color: #1EAEDB; } 162 | a:hover { 163 | color: #0FA0CE; } 164 | 165 | 166 | /* Buttons 167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 168 | .button, 169 | button, 170 | input[type="submit"], 171 | input[type="reset"], 172 | input[type="button"] { 173 | display: inline-block; 174 | height: 38px; 175 | padding: 0 30px; 176 | color: #555; 177 | text-align: center; 178 | font-size: 11px; 179 | font-weight: 600; 180 | line-height: 38px; 181 | letter-spacing: .1rem; 182 | text-transform: uppercase; 183 | text-decoration: none; 184 | white-space: nowrap; 185 | background-color: transparent; 186 | border-radius: 4px; 187 | border: 1px solid #bbb; 188 | cursor: pointer; 189 | box-sizing: border-box; } 190 | .button:hover, 191 | button:hover, 192 | input[type="submit"]:hover, 193 | input[type="reset"]:hover, 194 | input[type="button"]:hover, 195 | .button:focus, 196 | button:focus, 197 | input[type="submit"]:focus, 198 | input[type="reset"]:focus, 199 | input[type="button"]:focus { 200 | color: #333; 201 | border-color: #888; 202 | outline: 0; } 203 | .button.button-primary, 204 | button.button-primary, 205 | input[type="submit"].button-primary, 206 | input[type="reset"].button-primary, 207 | input[type="button"].button-primary { 208 | color: #FFF; 209 | background-color: #33C3F0; 210 | border-color: #33C3F0; } 211 | .button.button-primary:hover, 212 | button.button-primary:hover, 213 | input[type="submit"].button-primary:hover, 214 | input[type="reset"].button-primary:hover, 215 | input[type="button"].button-primary:hover, 216 | .button.button-primary:focus, 217 | button.button-primary:focus, 218 | input[type="submit"].button-primary:focus, 219 | input[type="reset"].button-primary:focus, 220 | input[type="button"].button-primary:focus { 221 | color: #FFF; 222 | background-color: #1EAEDB; 223 | border-color: #1EAEDB; } 224 | 225 | 226 | /* Forms 227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 228 | input[type="email"], 229 | input[type="number"], 230 | input[type="search"], 231 | input[type="text"], 232 | input[type="tel"], 233 | input[type="url"], 234 | input[type="password"], 235 | textarea, 236 | select { 237 | height: 38px; 238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 239 | background-color: #fff; 240 | border: 1px solid #D1D1D1; 241 | border-radius: 4px; 242 | box-shadow: none; 243 | box-sizing: border-box; } 244 | /* Removes awkward default styles on some inputs for iOS */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea { 253 | -webkit-appearance: none; 254 | -moz-appearance: none; 255 | appearance: none; } 256 | textarea { 257 | min-height: 65px; 258 | padding-top: 6px; 259 | padding-bottom: 6px; } 260 | input[type="email"]:focus, 261 | input[type="number"]:focus, 262 | input[type="search"]:focus, 263 | input[type="text"]:focus, 264 | input[type="tel"]:focus, 265 | input[type="url"]:focus, 266 | input[type="password"]:focus, 267 | textarea:focus, 268 | select:focus { 269 | border: 1px solid #33C3F0; 270 | outline: 0; } 271 | label, 272 | legend { 273 | display: block; 274 | margin-bottom: .5rem; 275 | font-weight: 600; } 276 | fieldset { 277 | padding: 0; 278 | border-width: 0; } 279 | input[type="checkbox"], 280 | input[type="radio"] { 281 | display: inline; } 282 | label > .label-body { 283 | display: inline-block; 284 | margin-left: .5rem; 285 | font-weight: normal; } 286 | 287 | 288 | /* Lists 289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 290 | ul { 291 | list-style: circle inside; } 292 | ol { 293 | list-style: decimal inside; } 294 | ol, ul { 295 | padding-left: 0; 296 | margin-top: 0; } 297 | ul ul, 298 | ul ol, 299 | ol ol, 300 | ol ul { 301 | margin: 1.5rem 0 1.5rem 3rem; 302 | font-size: 90%; } 303 | li { 304 | margin-bottom: 1rem; } 305 | 306 | 307 | /* Code 308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 309 | code { 310 | padding: .2rem .5rem; 311 | margin: 0 .2rem; 312 | font-size: 90%; 313 | white-space: nowrap; 314 | background: #F1F1F1; 315 | border: 1px solid #E1E1E1; 316 | border-radius: 4px; } 317 | pre > code { 318 | display: block; 319 | padding: 1rem 1.5rem; 320 | white-space: pre; } 321 | 322 | 323 | /* Tables 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | th, 326 | td { 327 | padding: 12px 15px; 328 | text-align: left; 329 | border-bottom: 1px solid #E1E1E1; } 330 | 331 | 332 | /* Spacing 333 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 334 | button, 335 | .button { 336 | margin-bottom: 1rem; } 337 | input, 338 | textarea, 339 | select, 340 | fieldset { 341 | margin-bottom: 1.5rem; } 342 | pre, 343 | blockquote, 344 | dl, 345 | figure, 346 | table, 347 | p, 348 | ul, 349 | ol, 350 | form { 351 | margin-bottom: 2.5rem; } 352 | 353 | 354 | /* Utilities 355 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 356 | .u-full-width { 357 | width: 100%; 358 | box-sizing: border-box; } 359 | .u-max-full-width { 360 | max-width: 100%; 361 | box-sizing: border-box; } 362 | .u-pull-right { 363 | float: right; } 364 | .u-pull-left { 365 | float: left; } 366 | 367 | 368 | /* Misc 369 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 370 | hr { 371 | margin-top: 3rem; 372 | margin-bottom: 3.5rem; 373 | border-width: 0; 374 | border-top: 1px solid #E1E1E1; } 375 | 376 | 377 | /* Clearing 378 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 379 | 380 | /* Self Clearing Goodness */ 381 | .container:after, 382 | .row:after, 383 | .u-cf { 384 | content: ""; 385 | display: table; 386 | clear: both; } 387 | 388 | 389 | /* Media Queries 390 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 391 | /* 392 | Note: The best way to structure the use of media queries is to create the queries 393 | near the relevant code. For example, if you wanted to change the styles for buttons 394 | on small devices, paste the mobile query code up in the buttons section and style it 395 | there. 396 | */ 397 | 398 | 399 | /* Larger than mobile */ 400 | @media (min-width: 400px) {} 401 | 402 | /* Larger than phablet (also point when grid becomes active) */ 403 | @media (min-width: 550px) {} 404 | 405 | /* Larger than tablet */ 406 | @media (min-width: 750px) {} 407 | 408 | /* Larger than desktop */ 409 | @media (min-width: 1000px) {} 410 | 411 | /* Larger than Desktop HD */ 412 | @media (min-width: 1200px) {} 413 | -------------------------------------------------------------------------------- /turtledove-server/content/static/css/tooltip.css: -------------------------------------------------------------------------------- 1 | /* Tooltip container */ 2 | .tooltip { 3 | position: relative; 4 | display: inline-block; 5 | border-bottom: 1px dotted black; /* If you want dots under the hoverable text */ 6 | } 7 | 8 | .tooltip .tooltiptext a { 9 | color: #33C3F0; 10 | } 11 | 12 | .tooltip .tooltiptext > h4 { 13 | margin: 0.6em; 14 | } 15 | 16 | /* Tooltip text */ 17 | .tooltip .tooltiptext { 18 | visibility: hidden; 19 | width: 200px; 20 | background-color: black; 21 | color: #fff; 22 | text-align: center; 23 | padding: 5px; 24 | border-radius: 6px; 25 | font-size: 0.8em; 26 | 27 | /* Position the tooltip text - see examples below! */ 28 | position: absolute; 29 | z-index: 1; 30 | top: 5px; 31 | right: 105%; 32 | } 33 | 34 | /* Show the tooltip text when you mouse over the tooltip container */ 35 | .tooltip:hover .tooltiptext { 36 | visibility: visible; 37 | } -------------------------------------------------------------------------------- /turtledove-server/content/static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dervan/turtledove-js/55954a2baaca60a29c36d5d0fd9cc7d8a4366c72/turtledove-server/content/static/images/favicon.png -------------------------------------------------------------------------------- /turtledove-server/content/static/images/turtledove-question-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dervan/turtledove-js/55954a2baaca60a29c36d5d0fd9cc7d8a4366c72/turtledove-server/content/static/images/turtledove-question-mark.png -------------------------------------------------------------------------------- /turtledove-server/content/static/js/common.js: -------------------------------------------------------------------------------- 1 | import { logsCountKey, logsKey, tdVersionKey, testStorageKey } from './storage-keys.js' 2 | 3 | export const logSeparator = '<|>' 4 | const turtledoveVersion = '1.1.3' 5 | 6 | class Log { 7 | constructor (text, site) { 8 | this.val = text 9 | this.site = site 10 | this.ts = new Date().toISOString() 11 | } 12 | } 13 | 14 | /** 15 | * A class used to save logs both in localStorage and in standard console. 16 | */ 17 | export class Logger { 18 | constructor (site, loggingEnabled, lazyDump) { 19 | this.logs = [] 20 | this.saved = lazyDump === false 21 | this.site = site 22 | this.loggingEnabled = loggingEnabled 23 | } 24 | 25 | static dump (newLogs) { 26 | let savedLogs = window.localStorage.getItem(logsKey) 27 | if (savedLogs !== null) { 28 | savedLogs = savedLogs + logSeparator 29 | } else { 30 | savedLogs = '' 31 | } 32 | const newLogsString = newLogs.map(JSON.stringify).join(logSeparator) 33 | window.localStorage.setItem(logsKey, savedLogs + newLogsString) 34 | 35 | const currentLogsCount = parseInt(window.localStorage.getItem(logsCountKey)) || 0 36 | window.localStorage.setItem(logsCountKey, currentLogsCount + newLogs.length) 37 | } 38 | 39 | save () { 40 | Logger.dump(this.logs) 41 | this.logs = [] 42 | this.saved = true 43 | } 44 | 45 | log (text) { 46 | if (!this.loggingEnabled) { 47 | return 48 | } 49 | const newLog = new Log(text, this.site) 50 | console.log(text) 51 | if (this.saved) { // Save also delayed logs 52 | Logger.dump([newLog]) 53 | } else { 54 | this.logs.push(newLog) 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Checks and alerts if it detects that we cannot use localStorage. 61 | */ 62 | export function testLocalStorageAvailability () { 63 | try { 64 | window.localStorage.setItem(testStorageKey, testStorageKey) 65 | window.localStorage.removeItem(testStorageKey) 66 | } catch (e) { 67 | console.error('Browser\'s localStorage has to be enabled, but is not accessible!') 68 | window.alert('Browser\'s localStorage has to be enabled, but is not accessible!') 69 | } 70 | } 71 | 72 | export function verifyVersion () { 73 | if (window.localStorage.getItem(tdVersionKey) !== turtledoveVersion) { 74 | if (window.localStorage.getItem(logsKey) != null) { 75 | window.alert('Incompatible storage of TURTLEDOVE detected. Demo was reset to continue to work') 76 | } 77 | resetStorage() 78 | } 79 | } 80 | 81 | export function resetStorage () { 82 | window.localStorage.clear() 83 | window.localStorage.setItem(tdVersionKey, turtledoveVersion) 84 | } 85 | 86 | /** 87 | * Get a string that will be an identifier of this InterestGroup. 88 | * @param {InterestGroup} interestGroup 89 | */ 90 | export function getInterestGroupId (interestGroup) { 91 | return `${interestGroup.owner}_${interestGroup.name}` 92 | } 93 | 94 | /** 95 | * Shuffles an array. 96 | */ 97 | export function shuffle (array) { 98 | for (let i = array.length - 1; i > 0; i--) { 99 | const j = Math.floor(Math.random() * (i + 1)) 100 | const tmp = array[i] 101 | array[i] = array[j] 102 | array[j] = tmp 103 | } 104 | return array 105 | } 106 | -------------------------------------------------------------------------------- /turtledove-server/content/static/js/iframe-api.js: -------------------------------------------------------------------------------- 1 | export class StoreRequest { 2 | constructor (type, interestGroup, membershipTimeout, loggingEnabled, productLevelEnabled) { 3 | this.type = type 4 | this.interestGroup = interestGroup 5 | this.membershipTimeout = membershipTimeout 6 | this.loggingEnabled = loggingEnabled 7 | this.productLevelEnabled = productLevelEnabled 8 | } 9 | } 10 | 11 | export class RenderingRequest { 12 | constructor (contextualBidRequests, loggingEnabled, productLevelEnabled) { 13 | this.contextualBidRequests = contextualBidRequests 14 | this.loggingEnabled = loggingEnabled 15 | this.productLevelEnabled = productLevelEnabled 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /turtledove-server/content/static/js/log.js: -------------------------------------------------------------------------------- 1 | import { consoleStateKey, logsCountKey, logsKey } from './storage-keys.js' 2 | 3 | const consoleRefreshInterval = 1000 4 | let loadedLogsCount = 0 5 | 6 | function insertLogs (logs) { 7 | const logsTable = document.getElementById(logsKey) 8 | for (const logEntry of logs) { 9 | if (!logEntry) { 10 | continue 11 | } 12 | const row = logsTable.insertRow() 13 | const time = row.insertCell() 14 | time.className = 'time' 15 | time.innerText = logEntry.ts 16 | const site = row.insertCell() 17 | site.innerText = logEntry.site 18 | const logText = row.insertCell() 19 | logText.innerText = logEntry.val 20 | } 21 | const tableContainer = logsTable.parentElement.parentElement 22 | tableContainer.scrollTop = tableContainer.scrollHeight 23 | } 24 | 25 | function getRawLogs () { 26 | return window.localStorage.getItem(logsKey)?.split('<|>') || [] 27 | } 28 | 29 | function getLogsCount () { 30 | return parseInt(window.localStorage.getItem(logsCountKey)) || 0 31 | } 32 | 33 | function getConsoleState () { 34 | return JSON.parse(window.localStorage.getItem(consoleStateKey)) || {} 35 | } 36 | 37 | function storeConsoleState (state) { 38 | window.localStorage.setItem(consoleStateKey, JSON.stringify(state)) 39 | } 40 | 41 | // Try to load logs into console 42 | try { 43 | insertLogs(getRawLogs().map(JSON.parse)) 44 | loadedLogsCount = getLogsCount() 45 | } catch (err) { 46 | window.localStorage.removeItem(logsKey) 47 | window.localStorage.removeItem(logsCountKey) 48 | loadedLogsCount = 0 49 | console.log('Error in stored logs format detected. All logs were removed.') 50 | } 51 | 52 | // Handle consistent console state between sites 53 | window.onmessage = (messageEvent) => { 54 | const request = messageEvent.data 55 | if (request.type === 'load') { 56 | messageEvent.source.postMessage(getConsoleState(), messageEvent.origin) 57 | } else if (request.type === 'hide') { 58 | const state = getConsoleState() 59 | state.hidden = true 60 | storeConsoleState(state) 61 | } else if (request.type === 'show') { 62 | const state = getConsoleState() 63 | state.hidden = false 64 | storeConsoleState(state) 65 | } else if (request.type === 'store') { 66 | const state = request.value 67 | state.hidden = false 68 | storeConsoleState(state) 69 | } 70 | } 71 | 72 | // Refresh console if new logs were added 73 | setInterval(() => { 74 | const logsCount = getLogsCount() 75 | const missingLogsCount = logsCount - loadedLogsCount 76 | if (missingLogsCount > 0) { 77 | const newLogs = getRawLogs().slice(loadedLogsCount, logsCount).map(x => { 78 | try { return JSON.parse(x) } catch (err) { return null } 79 | }) 80 | insertLogs(newLogs) 81 | } 82 | loadedLogsCount = logsCount 83 | }, consoleRefreshInterval) 84 | -------------------------------------------------------------------------------- /turtledove-server/content/static/js/skeleton-tabs.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function main () { 3 | var tabButtons = [].slice.call(document.querySelectorAll('ul.tab-nav li a.button')) 4 | 5 | tabButtons.map(function (button) { 6 | button.addEventListener('click', function () { 7 | document.querySelector('li a.active.button').classList.remove('active') 8 | button.classList.add('active') 9 | 10 | document.querySelector('.tab-pane.active').classList.remove('active') 11 | document.querySelector(button.getAttribute('data-pane')).classList.add('active') 12 | }) 13 | }) 14 | } 15 | 16 | if (document.readyState !== 'loading') { 17 | main() 18 | } else { 19 | document.addEventListener('DOMContentLoaded', main) 20 | } 21 | })() 22 | -------------------------------------------------------------------------------- /turtledove-server/content/static/js/storage-keys.js: -------------------------------------------------------------------------------- 1 | export const logsKey = 'logs' 2 | export const logsCountKey = 'logsCount' 3 | export const consoleStateKey = 'consoleState' 4 | export const activePartnersKey = 'activeAdPartners' 5 | export const interestGroupsStorageKey = 'interestGroups' 6 | export const fetchedAdsStorageKeyPrefix = 'fetchedAds|' 7 | export const fetchedProductsStorageKeyPrefix = 'fetchedProducts|' 8 | export const winnersRegisterKey = 'winnerAds' 9 | export const testStorageKey = 'tdTestKey' 10 | export const tdVersionKey = 'tdVersion' 11 | export const adRemovalNotifiedKey = 'adRemovalNotified' 12 | export const productRemovalNotifiedKey = 'productRemovalNotified' 13 | -------------------------------------------------------------------------------- /turtledove-server/content/static/js/store.js: -------------------------------------------------------------------------------- 1 | import { activePartnersKey, fetchedAdsStorageKeyPrefix, fetchedProductsStorageKeyPrefix, interestGroupsStorageKey } from './storage-keys.js' 2 | 3 | import { getInterestGroupId, Logger, verifyVersion, testLocalStorageAvailability } from './common.js' 4 | 5 | /* eslint-env browser */ 6 | 7 | /** 8 | * Saves given group in window.localStorage. All interest groups are stored under the same key. 9 | * 10 | * @param {InterestGroup} interestGroup 11 | * @param {number} membershipTimeout 12 | * @param logger 13 | */ 14 | function storeInterestGroup (interestGroup, membershipTimeout, logger) { 15 | // Load current interest-groups of user 16 | const allInterestGroups = JSON.parse(window.localStorage.getItem(interestGroupsStorageKey)) || {} 17 | const groupId = getInterestGroupId(interestGroup) 18 | if (groupId in allInterestGroups) { 19 | logger.log('Already known group: ' + groupId) 20 | } else { 21 | logger.log('New interest group: ' + groupId) 22 | } 23 | if (interestGroup.timeout === undefined && membershipTimeout !== null) { 24 | interestGroup.timeout = new Date(Date.now() + membershipTimeout) 25 | } 26 | allInterestGroups[groupId] = interestGroup 27 | window.localStorage.setItem(interestGroupsStorageKey, JSON.stringify(allInterestGroups)) 28 | } 29 | 30 | /** 31 | * Saves given group in window.localStorage. All interest groups are stored under the same key. 32 | * 33 | * @param {InterestGroup} interestGroup 34 | * @param logger 35 | */ 36 | function removeInterestGroup (interestGroup, logger) { 37 | // Load current user-groups of user 38 | const allInterestGroups = JSON.parse(window.localStorage.getItem(interestGroupsStorageKey)) || {} 39 | const groupId = getInterestGroupId(interestGroup) 40 | if (groupId in allInterestGroups) { 41 | logger.log('Leaving known group: ' + getInterestGroupId(interestGroup)) 42 | delete allInterestGroups[groupId] 43 | // warning: not thread safe, check local storage for that case 44 | window.localStorage.setItem(interestGroupsStorageKey, JSON.stringify(allInterestGroups)) 45 | } else { 46 | logger.log('Trying to leave not found group: ' + getInterestGroupId(interestGroup)) 47 | } 48 | } 49 | 50 | /** 51 | * Updates a list of all ad partners that have some active ads. 52 | * 53 | * @param {string[]} addedReaders - list of readers that should be added 54 | */ 55 | function updateActivePartners (addedReaders) { 56 | const activePartners = JSON.parse(window.localStorage.getItem(activePartnersKey)) || [] 57 | const newPartners = addedReaders.filter((partner) => activePartners.indexOf(partner) === -1) 58 | const partnersToRemove = [] 59 | for (const partner of activePartners) { 60 | const partnerAds = window.localStorage.getItem(fetchedAdsStorageKeyPrefix + partner) 61 | if (partnerAds === {}) { 62 | partnersToRemove.push(partner) 63 | } 64 | } 65 | if (newPartners.length > 0 || partnersToRemove.length > 0) { 66 | const newPartnersList = activePartners.concat(newPartners).filter(p => (partnersToRemove.indexOf(p) === -1 || addedReaders.indexOf(p) !== -1)) 67 | window.localStorage.setItem(activePartnersKey, JSON.stringify(newPartnersList)) 68 | } 69 | } 70 | 71 | /** 72 | * Removes ads fetched for given interest group. 73 | * 74 | * @param {InterestGroup} interestGroup 75 | * @param logger 76 | */ 77 | function removeAds (interestGroup, logger) { 78 | const activePartners = JSON.parse(window.localStorage.getItem(activePartnersKey)) || [] 79 | const interestGroupId = getInterestGroupId(interestGroup) 80 | for (const partner of activePartners) { 81 | const partnerKey = fetchedAdsStorageKeyPrefix + partner 82 | const localAds = JSON.parse(window.localStorage.getItem(partnerKey)) || {} 83 | if (!(interestGroupId in localAds)) { 84 | continue 85 | } 86 | delete localAds[interestGroupId] 87 | logger.log(`Removed ad for group ${interestGroupId}.`) 88 | window.localStorage.setItem(partnerKey, JSON.stringify(localAds)) 89 | } 90 | } 91 | 92 | /** 93 | * Returns an function that will download bidding function source code from a specified address 94 | * and put it inside an Ad for every ad on a list. 95 | * 96 | * @param {Logger} logger 97 | */ 98 | function enrichAdsWithBiddingFunctions (logger) { 99 | return interestBasedAds => Promise.allSettled( 100 | interestBasedAds.map(ad => fetch(ad.bidFunctionSrc) 101 | .then(async (scriptResponse) => { 102 | if (!scriptResponse.ok) { 103 | logger.log(`Request for bid function of ad ${ad.id} returned ${scriptResponse.status}`) 104 | return null 105 | } 106 | ad.bidFunction = await scriptResponse.text() 107 | return ad 108 | }).catch(() => logger.log(`Cannot download bid function for ${ad.id}`)))) 109 | } 110 | 111 | /** 112 | * Returns a function that will save list of ads into localStorage. 113 | * Currently it is overwriting old ads for the same interest group! 114 | * 115 | * @param {string} reader 116 | * @param {Logger} logger 117 | */ 118 | function saveAds (reader, logger) { 119 | return adFetchResults => { 120 | const readerAdsKey = fetchedAdsStorageKeyPrefix + reader 121 | const readerAds = JSON.parse(window.localStorage.getItem(readerAdsKey)) || {} 122 | const newAds = adFetchResults 123 | .filter(adResult => adResult !== null && adResult.status === 'fulfilled' && adResult.bidFunctionSrc !== null) 124 | .map(adResult => adResult.value) 125 | for (const ad of newAds) { 126 | if (ad.id in readerAds) { 127 | logger.log(`Refreshed ad ${ad.id} from ${ad.adPartner}.`) 128 | } else { 129 | logger.log(`Fetched new ad ${ad.id} from ${ad.adPartner}.`) 130 | } 131 | readerAds[ad.id] = ad 132 | } 133 | window.localStorage.setItem(readerAdsKey, JSON.stringify(readerAds)) 134 | } 135 | } 136 | 137 | /** 138 | * Performs a call to fetch-ads. Due to demo simplicity, it is performed always after adding to a user group 139 | * (on the contrary to standard TURTLEDOVE, where this call will be asynchronous) 140 | * 141 | * @param {InterestGroup} interestGroup 142 | * @param {Logger} logger 143 | */ 144 | function fetchNewAds (interestGroup, logger) { 145 | const timeout = 1000 146 | const interestGroupId = getInterestGroupId(interestGroup) 147 | 148 | for (const reader of interestGroup.readers) { 149 | const controller = new AbortController() 150 | fetch(`${reader}/fetch-ads?interest_group=${encodeURIComponent(interestGroupId)}`, { signal: controller.signal }) 151 | .then(r => r.json()) 152 | .then(enrichAdsWithBiddingFunctions(logger)) 153 | .then(saveAds(reader, logger)) 154 | .catch(reason => logger.log(`Request to ${reader} for ${interestGroupId} failed: ${reason}`)) 155 | setTimeout(() => controller.abort(), timeout) 156 | } 157 | } 158 | 159 | /** 160 | * Returns a function that will save fetched product to localStorage. 161 | * 162 | * @param {string} owner 163 | * @param {string} productId 164 | * @param {string} reader 165 | * @param {Logger} logger 166 | */ 167 | function saveProduct (owner, reader, logger) { 168 | return (product) => { 169 | const productId = product.productId 170 | const readerProdKey = fetchedProductsStorageKeyPrefix + reader 171 | const readerProducts = JSON.parse(window.localStorage.getItem(readerProdKey)) || {} 172 | const ownerProducts = readerProducts[owner] || {} 173 | if (owner in readerProducts && productId in ownerProducts) { 174 | logger.log(`Refreshed existing product ${productId} from ${owner} requested by a partner ${reader}`) 175 | } else { 176 | logger.log(`Saved new product ${productId} from ${owner} requested by a partner ${reader}`) 177 | } 178 | ownerProducts[productId] = product 179 | readerProducts[owner] = ownerProducts 180 | window.localStorage.setItem(readerProdKey, JSON.stringify(readerProducts)) 181 | } 182 | } 183 | 184 | /** 185 | * Performs a call to fetch-products. Due to demo simplicity, it is performed always after adding to a user group 186 | * (on the contrary to standard TURTLEDOVE, where this call will be asynchronous) 187 | * 188 | * Currently, every product is identified by a pair (owner, productId) and is stored separately for every interest group reader. 189 | * This allows the reader to process an offer before it gets served. But it's just a simple design decision and should be discussed later. 190 | * 191 | * @param {InterestGroup} interestGroup 192 | * @param {Logger} logger 193 | */ 194 | function fetchNewProducts (interestGroup, logger) { 195 | const timeout = 5000 196 | if (interestGroup.products === undefined) { 197 | return 198 | } 199 | for (const reader of interestGroup.readers) { 200 | const controller = new AbortController() 201 | interestGroup.products.map(productId => 202 | fetch(`${reader}/fetch-product?owner=${encodeURIComponent(interestGroup.owner)}&product=${encodeURIComponent(productId)}`, { signal: controller.signal }) 203 | .then(r => r.json()) 204 | .then(saveProduct(interestGroup.owner, reader, logger)) 205 | .catch(reason => logger.log(`Request to ${reader} for ${productId} failed: ${reason}`))) 206 | setTimeout(() => controller.abort(), timeout) 207 | } 208 | } 209 | 210 | window.onmessage = function (messageEvent) { 211 | const storeRequest = messageEvent.data 212 | const interestGroup = storeRequest.interestGroup 213 | const productLevelEnabled = storeRequest.productLevelEnabled 214 | 215 | const logger = new Logger(messageEvent.origin, storeRequest.loggingEnabled) 216 | 217 | if (storeRequest.type === 'store') { 218 | storeInterestGroup(interestGroup, storeRequest.membershipTimeout, logger) 219 | fetchNewAds(interestGroup, logger) 220 | if (productLevelEnabled) { 221 | fetchNewProducts(interestGroup, logger) 222 | } 223 | updateActivePartners(interestGroup.readers) 224 | } else if (storeRequest.type === 'remove') { 225 | removeInterestGroup(interestGroup, logger) 226 | removeAds(interestGroup, logger) 227 | updateActivePartners([]) 228 | } 229 | logger.save() 230 | } 231 | verifyVersion() 232 | testLocalStorageAvailability() 233 | -------------------------------------------------------------------------------- /turtledove-server/content/static/js/user-interface.js: -------------------------------------------------------------------------------- 1 | import { 2 | activePartnersKey, 3 | fetchedAdsStorageKeyPrefix, 4 | fetchedProductsStorageKeyPrefix, 5 | winnersRegisterKey 6 | } from './storage-keys.js' 7 | 8 | /* eslint-env browser */ 9 | /* eslint no-unused-vars: 0 */ 10 | 11 | /** 12 | * Lists all partners that have/hod some ad stored in this browser. 13 | */ 14 | function listActivePartners () { 15 | return JSON.parse(window.localStorage.getItem(activePartnersKey)) || [] 16 | } 17 | 18 | /** 19 | * Lists all ads that won an on-device auction. 20 | */ 21 | export function listWinners () { 22 | return JSON.parse(window.localStorage.getItem(winnersRegisterKey))?.reverse() || [] 23 | } 24 | 25 | /** 26 | * Lists all retrieved ads saved in localStorage. 27 | */ 28 | export function listAds () { 29 | let ads = [] 30 | for (const adPartner of listActivePartners()) { 31 | const partnerAdsKey = fetchedAdsStorageKeyPrefix + adPartner 32 | const partnerAdsMap = JSON.parse(window.localStorage.getItem(partnerAdsKey)) || {} 33 | ads = ads.concat(Object.values(partnerAdsMap)) 34 | } 35 | return ads 36 | } 37 | 38 | /** 39 | * Lists all retrieved products saved in localStorage. 40 | */ 41 | export function listProducts () { 42 | const products = [] 43 | for (const adPartner of listActivePartners()) { 44 | const partnerProductsKey = fetchedProductsStorageKeyPrefix + adPartner 45 | const partnerProductsMap = JSON.parse(window.localStorage.getItem(partnerProductsKey)) || {} 46 | for (const ownerProducts of Object.values(partnerProductsMap)) { 47 | for (const product of Object.values(ownerProducts)) { 48 | product.adPartner = adPartner 49 | products.push(product) 50 | } 51 | } 52 | } 53 | return products 54 | } 55 | 56 | /** 57 | * Removes one previously fetched product from local storage 58 | */ 59 | export function removeProduct (adPartner, owner, productId) { 60 | const adPartnerKey = fetchedProductsStorageKeyPrefix + adPartner 61 | const products = JSON.parse(window.localStorage.getItem(adPartnerKey)) || {} 62 | if (owner in products && productId in products[owner]) { 63 | delete products[owner][productId] 64 | window.localStorage.setItem(adPartnerKey, JSON.stringify(products)) 65 | } 66 | } 67 | 68 | /** 69 | * Removes one previously fetched ad from local storage 70 | */ 71 | export function removeAd (adPartner, adId) { 72 | const adPartnerKey = fetchedAdsStorageKeyPrefix + adPartner 73 | const ads = JSON.parse(window.localStorage.getItem(adPartnerKey)) || {} 74 | if (adId in ads) { 75 | delete ads[adId] 76 | window.localStorage.setItem(adPartnerKey, JSON.stringify(ads)) 77 | } 78 | } 79 | 80 | export function fillAdsTable () { 81 | const table = document.getElementById('fetched-ads') 82 | 83 | for (const ad of listAds()) { 84 | const row = table.insertRow() 85 | const groupId = row.insertCell() 86 | groupId.innerHTML = `Group ID:
87 | ${ad.groupName}
88 | Ad partner:
89 | ${ad.adPartner}
` 90 | const adRemoveButton = document.createElement('button') 91 | adRemoveButton.textContent = 'Remove' 92 | adRemoveButton.onclick = () => { 93 | removeAd(ad.adPartner, ad.id) 94 | table.deleteRow(row.rowIndex) 95 | } 96 | groupId.appendChild(adRemoveButton) 97 | const iframe = document.createElement('iframe') 98 | iframe.srcdoc = ad.iframeContent 99 | iframe.height = '250px' 100 | iframe.width = '300px' 101 | iframe.scrolling = 'no' 102 | const adCell = row.insertCell() 103 | adCell.appendChild(iframe) 104 | const bidFunction = row.insertCell() 105 | const button = document.createElement('button') 106 | button.textContent = 'Show code' 107 | button.onclick = () => { 108 | const content = renderScript(ad.bidFunction) 109 | const myWindow = window.open('', ad.groupName + ' bid function', 'width=400,height=400,menubar=no,location=no,resizable=no,scrollbars=no,status=no') 110 | myWindow.document.write(content) 111 | } 112 | bidFunction.appendChild(button) 113 | const igSignals = row.insertCell() 114 | igSignals.innerHTML = '
' + JSON.stringify(ad.interestGroupSignals, null, 2) + '
' 115 | } 116 | } 117 | 118 | function renderScript (script) { 119 | return ` 120 | 126 | 127 |
128 | 151 | 152 | ` 153 | } 154 | 155 | export function fillProductsTable () { 156 | const productsTable = document.getElementById('products-table') 157 | for (const product of listProducts()) { 158 | const row = productsTable.insertRow() 159 | const owner = row.insertCell() 160 | const adPartner = row.insertCell() 161 | const id = row.insertCell() 162 | const iframeContainer = row.insertCell() 163 | const remove = row.insertCell() 164 | adPartner.innerText = product.adPartner 165 | owner.innerText = product.owner 166 | id.innerText = product.productId 167 | const iframe = document.createElement('iframe') 168 | iframe.srcdoc = product.iframeContent 169 | iframe.height = '200px' 170 | iframe.width = '150px' 171 | iframe.scrolling = 'no' 172 | iframeContainer.appendChild(iframe) 173 | const adRemoveButton = document.createElement('button') 174 | adRemoveButton.textContent = 'Remove' 175 | adRemoveButton.onclick = () => { 176 | removeProduct(product.adPartner, product.owner, product.productId) 177 | productsTable.deleteRow(row.rowIndex) 178 | } 179 | remove.appendChild(adRemoveButton) 180 | } 181 | } 182 | 183 | export function fillWinnersTable () { 184 | const winnersTable = document.getElementById('winners-ads') 185 | for (const winner of listWinners()) { 186 | const row = winnersTable.insertRow() 187 | const site = row.insertCell() 188 | site.innerText = winner.site 189 | const price = row.insertCell() 190 | price.innerText = winner.bidValue 191 | const ad = row.insertCell() 192 | const iframe = document.createElement('iframe') 193 | iframe.srcdoc = winner.iframeContent 194 | iframe.height = '250px' 195 | iframe.width = '300px' 196 | iframe.scrolling = 'no' 197 | ad.appendChild(iframe) 198 | if (winner.productsPayload !== null) { 199 | iframe.onload = () => { 200 | iframe.contentWindow.postMessage(winner.productsPayload, '*') 201 | } 202 | } 203 | const ctxSignals = row.insertCell() 204 | ctxSignals.innerHTML = winner.contextSignals ? '
' + JSON.stringify(winner.contextSignals, null, 2) + '
' : '' 205 | const time = row.insertCell() 206 | time.innerHTML = winner.time 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /turtledove-server/content/static/product-remove.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TURTLEDOVE demo UI 6 | 7 | 8 | 23 | 24 | 25 | 46 |
47 | Removed! 48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /turtledove-server/content/static/user-interface.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TURTLEDOVE demo UI 6 | 7 | 8 | 9 | 10 | 11 | 12 | 37 | 38 | 39 | 40 |
41 |

TURTLEDOVE demo UI

42 |

43 | This is a simple user interface of a simulation created to visualize and help to understand a TURTLEDOVE proposal - a 45 | novel approach to privacy in online advertising. To know more about how to explore our demo, check out 46 | README in our repo. 47 |

48 | 54 |
55 | 56 |
57 |

Data reset

58 |

If you want to delete all data from demo local storage, use this button:

59 | 60 |
61 | 62 | 63 |
64 |

Fetched ads

65 |

All ads fetched because of joining an interest group:

66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
Ad idAdBidding functionInterest group signals
76 |
77 | 78 | 79 |
80 |

Fetched products

81 |

This table contains all products that were fetched for some user-group. Those are valid only in Product-Level TURTLEDOVE which is a proposal described in a dedicated repository. 82 | Note that association with user-group may be not current but a product will still be cached here.

83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 |
OwnerAd networkIdIframeRemove
94 |
95 | 96 | 97 |
98 |

Shown ads

99 |

This table contains all ads that won an auction in the past, together with a price and a publisher's address.

100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 |
WebsitePriceAdContext signalsTime
111 |
112 |
113 |
114 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /turtledove-server/content/turtledove-console.js: -------------------------------------------------------------------------------- 1 | import 'https://cdn.interactjs.io/v1.9.20/auto-start/index.js' 2 | import 'https://cdn.interactjs.io/v1.9.20/actions/resize/index.js' 3 | import 'https://cdn.interactjs.io/v1.9.20/modifiers/index.js' 4 | import interact from 'https://cdn.interactjs.io/v1.9.20/interactjs/index.js' 5 | 6 | const consoleIconId = 'td-console-icon' 7 | const resizeOverlayId = 'td-console-overlay' 8 | const consoleIframeId = 'td-console' 9 | const tdDemoAddress = '<%= it.turtledoveHost %>' 10 | 11 | const consoleCss = '#td-console {\n' + 12 | ' font-family: monospace;\n' + 13 | ' position: fixed;\n' + 14 | ' bottom: 0;\n' + 15 | ' left: 0;\n' + 16 | ' background-color: rgba(0, 0.1, 0.2, 0.6);\n' + 17 | ' border-radius: 2px;\n' + 18 | ' padding: 7px;\n' + 19 | ' padding-bottom: 1px;\n' + 20 | ' border: none;\n' + 21 | ' touch-action: none;\n' + 22 | ' width: 100%;\n' + 23 | ' height: 20%;\n' + 24 | ' overflow-x: hidden;\n' + 25 | ' overflow-y: auto;\n' + 26 | ' box-sizing: border-box;\n' + 27 | '}\n' + 28 | '#td-console-icon {\n' + 29 | ' border: 1px groove black;\n' + 30 | ' position: fixed;\n' + 31 | ' top: 0;\n' + 32 | ' right: 0;\n' + 33 | ' height: 35px;\n' + 34 | ' width:auto;\n' + 35 | ' margin: 0;\n' + 36 | ' transition: border 2s ease;\n' + 37 | ' transition: background 2s ease;\n' + 38 | '}\n' + 39 | '#td-console-overlay {\n' + 40 | ' position: fixed;\n' + 41 | ' bottom: 0;\n' + 42 | ' left: 0;\n' + 43 | '}' 44 | 45 | function createOverlay () { 46 | const overlay = document.createElement('div') 47 | overlay.id = resizeOverlayId 48 | document.body.appendChild(overlay) 49 | return overlay 50 | } 51 | 52 | function updateOverlay (x, y, event) { 53 | const overlay = document.getElementById(resizeOverlayId) || createOverlay() 54 | overlay.setAttribute('data-x', x) 55 | overlay.setAttribute('data-y', y) 56 | overlay.style.width = event.rect.width + 'px' 57 | overlay.style.height = event.rect.height + 'px' 58 | overlay.style.webkitTransform = overlay.style.transform = 'translate(' + x + 'px, 0px)' 59 | } 60 | 61 | function checkSelection () { 62 | if (window.getSelection) { 63 | const sel = window.getSelection() 64 | if (sel.rangeCount) { 65 | sel.removeAllRanges() 66 | } 67 | } else if (document.selection) { 68 | if (document.selection.createRange().text > '') { 69 | document.selection.empty() 70 | } 71 | } 72 | } 73 | 74 | interact('#' + consoleIframeId) 75 | .resizable({ 76 | // resize from all edges and corners 77 | edges: { left: true, right: true, bottom: false, top: true }, 78 | 79 | listeners: { 80 | start (event) { 81 | const target = event.target 82 | const x = (parseFloat(target.getAttribute('data-x')) || 0) 83 | const y = (parseFloat(target.getAttribute('data-y')) || 0) 84 | 85 | updateOverlay(x, y, event) 86 | }, 87 | move (event) { 88 | const target = event.target 89 | let x = (parseFloat(target.getAttribute('data-x')) || 0) 90 | let y = (parseFloat(target.getAttribute('data-y')) || 0) 91 | 92 | // update the element's style 93 | target.style.width = event.rect.width + 'px' 94 | target.style.height = event.rect.height + 'px' 95 | 96 | // translate when resizing from top or left edges 97 | x += event.deltaRect.left 98 | y += event.deltaRect.top 99 | 100 | target.style.webkitTransform = target.style.transform = 101 | 'translate(' + x + 'px, 0px)' 102 | 103 | target.setAttribute('data-x', x) 104 | target.setAttribute('data-y', y) 105 | 106 | updateOverlay(x, y, event) 107 | checkSelection() 108 | }, 109 | end (event) { 110 | const target = event.target 111 | const update = { 112 | x: target.getAttribute('data-x') || '0', 113 | y: target.getAttribute('data-y') || '0', 114 | width: target.style.width, 115 | height: target.style.height, 116 | transform: target.style.transform 117 | } 118 | const contentWindow = event.target.contentWindow 119 | if (contentWindow !== null) { 120 | contentWindow.postMessage({ 121 | type: 'store', 122 | value: update 123 | }, '<%= it.turtledoveHost %>') 124 | } 125 | 126 | const overlay = document.getElementById(resizeOverlayId) 127 | if (overlay !== null) { 128 | overlay.parentNode.removeChild(overlay) 129 | } 130 | } 131 | }, 132 | modifiers: [ 133 | // keep the edges inside the parent 134 | interact.modifiers.restrictEdges({ 135 | outer: 'parent' 136 | }), 137 | 138 | // minimum size 139 | interact.modifiers.restrictSize({ 140 | min: { width: 100, height: 50 } 141 | }) 142 | ], 143 | 144 | inertia: true 145 | }) 146 | 147 | function toggleLogConsole () { 148 | const foundLog = document.getElementById(consoleIframeId) 149 | if (foundLog) { 150 | if (foundLog.style?.visibility === 'visible') { 151 | foundLog.style.visibility = 'hidden' 152 | if (foundLog?.contentWindow !== null) { 153 | foundLog.contentWindow.postMessage({ type: 'hide' }, tdDemoAddress) 154 | } 155 | } else { 156 | foundLog.style.visibility = 'visible' 157 | if (foundLog?.contentWindow !== null) { 158 | foundLog.contentWindow.postMessage({ type: 'show' }, tdDemoAddress) 159 | } 160 | } 161 | } 162 | } 163 | 164 | function enableLog () { 165 | const consoleStyle = document.createElement('style') 166 | consoleStyle.setAttribute('type', 'text/css') 167 | consoleStyle.appendChild(document.createTextNode(consoleCss)) 168 | document.head.appendChild(consoleStyle) 169 | 170 | const icon = document.createElement('img') 171 | icon.src = tdDemoAddress + '/static/images/turtledove-question-mark.png' 172 | icon.id = consoleIconId 173 | setInterval(() => { 174 | if (document.getElementById(consoleIframeId)?.style?.visibility !== 'visible') { icon.style.backgroundColor = 'rgba(255,94,25,0.6)' } 175 | }, 4000) 176 | setTimeout(() => setInterval(() => { icon.style.backgroundColor = 'rgba(0,0,0,0)' }, 4000), 2000) 177 | icon.onclick = toggleLogConsole 178 | 179 | const iframe = document.createElement('iframe') 180 | iframe.src = tdDemoAddress + '/console' 181 | iframe.id = consoleIframeId 182 | iframe.style.visibility = 'hidden' 183 | iframe.onload = () => { 184 | iframe.contentWindow.postMessage({ type: 'load' }, tdDemoAddress) 185 | } 186 | 187 | document.body.appendChild(iframe) 188 | document.body.appendChild(icon) 189 | 190 | // if receive response from log iframe update its position and size 191 | window.addEventListener('message', (event) => { 192 | if (event.origin === tdDemoAddress) { 193 | const initData = event.data 194 | if (initData) { 195 | iframe.setAttribute('data-x', initData.x || 0) 196 | iframe.setAttribute('data-y', initData.y || 0) 197 | if (initData.width) { 198 | iframe.style.width = initData.width 199 | } 200 | if (initData.height) { 201 | iframe.style.height = initData.height 202 | } 203 | if (initData.transform) { 204 | iframe.style.transform = initData.transform 205 | } 206 | } 207 | if (initData?.hidden === false) { 208 | iframe.style.visibility = 'visible' 209 | } 210 | window.removeEventListener('message', this) 211 | } 212 | }) 213 | } 214 | 215 | export { enableLog } 216 | -------------------------------------------------------------------------------- /turtledove-server/content/turtledove.js: -------------------------------------------------------------------------------- 1 | import { enableLog } from './turtledove-console.js' 2 | import { RenderingRequest, StoreRequest } from './static/js/iframe-api.js' 3 | 4 | /** 5 | * This file contains a simulated API of TURTLEDOVE (https://github.com/WICG/turtledove). 6 | * In the future exported functions should be provided by the browser. 7 | * 8 | * In this simulation all data (interest group membership, fetched ads etc) are stored in window.localStorage of 9 | * domain <%= it.turtledoveHost %>. This is the cause of an iframe-based construction of the whole simulation. 10 | */ 11 | const tdDemoAddress = '<%= it.turtledoveHost %>' 12 | const storeIframeId = 'td-demo-store' 13 | const storeQueue = [] 14 | let storeLoaded = false 15 | let logsEnabled = false 16 | let productLevelEnabled = true 17 | 18 | /** 19 | * Adds and initializes an iframe that later is used to save TD data to localStorage. 20 | */ 21 | function addStoreIframe () { 22 | const iframe = document.createElement('iframe') 23 | iframe.id = storeIframeId 24 | iframe.style.display = 'none' 25 | iframe.src = tdDemoAddress + '/store' 26 | iframe.onload = () => { 27 | storeLoaded = true 28 | handleStoreQueue() 29 | } 30 | document.body.appendChild(iframe) 31 | } 32 | 33 | function handleStoreQueue () { 34 | if (!storeLoaded) { 35 | return 36 | } 37 | const iframeContent = document.getElementById(storeIframeId).contentWindow 38 | while (storeQueue.length > 0) { 39 | iframeContent.postMessage(storeQueue.shift(), tdDemoAddress) 40 | } 41 | } 42 | 43 | /** 44 | * Remove interest group from TURTLEDOVE internal store. It also removes ads for this group. 45 | * 46 | * @param {InterestGroup} group 47 | */ 48 | function leaveAdInterestGroup (group) { 49 | storeQueue.push(new StoreRequest('remove', group, null, logsEnabled)) 50 | handleStoreQueue() 51 | } 52 | 53 | /** 54 | * Adds interest group to TURTLEDOVE internal store. Allows to fetch an ad for this group. 55 | * 56 | * @param {InterestGroup} group 57 | * @param {number} membershipTimeout 58 | */ 59 | function joinAdInterestGroup (group, membershipTimeout) { 60 | storeQueue.push(new StoreRequest('store', group, membershipTimeout, logsEnabled, productLevelEnabled)) 61 | handleStoreQueue() 62 | } 63 | 64 | /** 65 | * Sends a request to render a TURTLEDOVE ad in an iframe with a given ID. Note, that this iframe have to be initialized 66 | * earlier during an initializeTurtledove call. 67 | * @param {string} iframeId 68 | * @param {Map} contextualBidRequests - a map that for every entry (ad partner address) contains 69 | * contextual bid request for it (custom object, as specified by ad partner) 70 | */ 71 | function renderAds (iframeId, contextualBidRequests) { 72 | const renderingRequest = new RenderingRequest(contextualBidRequests, logsEnabled, productLevelEnabled) 73 | const turtledoveRenderAdSrc = tdDemoAddress + '/render-ad' 74 | const ad = document.getElementById(iframeId) 75 | if (ad === null) { 76 | console.error(`There is no iframe with id ${iframeId}!`) 77 | return 78 | } 79 | if (ad.contentWindow !== null && ad.src === turtledoveRenderAdSrc) { 80 | ad.contentWindow.postMessage(renderingRequest, tdDemoAddress) 81 | } else { 82 | ad.onload = () => ad.contentWindow.postMessage(renderingRequest, tdDemoAddress) 83 | ad.src = turtledoveRenderAdSrc 84 | } 85 | } 86 | 87 | /** 88 | * Initializes a demo of TURTLEDOVE. Allows to call currently unavailable methods 89 | * of Navigator that in the future will be a part of browser API. 90 | * 91 | * @param options - dict that allows to configure TURTLEDOVE simulation. Currently it accepts two keys: 92 | * * logs: boolean - if set to true adds an turtledove icon and a log that shows last TURTLEDOVE demo actions 93 | * * productLevel: boolean - if set to false disables Product-Level extension of TURTLEDOVE demo. 94 | */ 95 | export function initTurtledove (options) { 96 | if (options.logs) { 97 | logsEnabled = true 98 | enableLog() 99 | } 100 | if (options.productLevel === false) { 101 | productLevelEnabled = false 102 | } 103 | addStoreIframe() 104 | 105 | window.navigator.renderAds = renderAds 106 | window.navigator.joinAdInterestGroup = joinAdInterestGroup 107 | window.navigator.leaveAdInterestGroup = leaveAdInterestGroup 108 | } 109 | 110 | /** 111 | * The class describing an interest group. 112 | */ 113 | export class InterestGroup { 114 | constructor (owner, name, readers, products) { 115 | this.owner = owner // site to which this group refers to, 116 | this.name = name // identifier of this group 117 | this.readers = readers // ad networks that will be asked to provide an ad for this group 118 | this.products = products // products associated with this InterestGroup in this browser. Used only in product-level TURTLEDOVE. 119 | } 120 | 121 | setTimeout (membershipTimeoutMs) { 122 | this.timeout = new Date(Date.now() + membershipTimeoutMs) 123 | } 124 | 125 | static fromJson (jsonString) { 126 | 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /turtledove-server/turtledove-server.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import fs from 'fs' 3 | import cors from 'cors' 4 | import eta from 'eta' 5 | import path from 'path' 6 | import { ports, addresses } from '../config.js' 7 | const __dirname = path.resolve('./turtledove-server/') 8 | 9 | function jsEmbeddedIntoHtml (pathToJs) { 10 | return (req, res) => { 11 | res.set('Content-Type', 'text/html') 12 | res.send('' + 13 | '' + 14 | ``) 15 | } 16 | } 17 | 18 | function serveHtml (file) { 19 | return (req, res) => { 20 | res.set('Content-Type', 'text/html') 21 | res.send(fs.readFileSync(path.join(__dirname, file))) 22 | } 23 | } 24 | 25 | function serveTemplatedJs (file) { 26 | const turtledoveJs = eta.renderFile(path.join(__dirname, file), addresses) 27 | return async (req, res) => { 28 | res.set('Content-Type', 'application/javascript') 29 | return res.send(await turtledoveJs) 30 | } 31 | } 32 | 33 | const app = express() 34 | app.use(cors()) 35 | app.use('/static', express.static(path.join(__dirname, 'content/static'))) 36 | app.get('/turtledove.js', serveTemplatedJs('/content/turtledove.js')) 37 | app.get('/turtledove-console.js', serveTemplatedJs('/content/turtledove-console.js')) 38 | 39 | app.get('/store', jsEmbeddedIntoHtml('./static/js/store.js')) 40 | app.get('/render-ad', jsEmbeddedIntoHtml('./static/js/render-ad.js')) 41 | app.get('/ad-remove', serveHtml('/content/static/ad-remove.html')) 42 | app.get('/product-remove', serveHtml('/content/static/product-remove.html')) 43 | app.get('/console', serveHtml('/content/static/console.html')) 44 | app.get('/', serveHtml('/content/static/user-interface.html')) 45 | 46 | app.listen(ports.turtledovePort, 47 | () => console.log(`TD demo server listening at http://localhost:${ports.turtledovePort}`)) 48 | -------------------------------------------------------------------------------- /websites/content/aboutanimals.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | About animals 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Website about animals

24 |

Our website uses only privacy-first online advertising 25 | with the usage of state-of-the-art TURTLEDOVE ads.

26 |

27 | We've included iframes marked with class td-demo-ad. Later the script 28 | turtledove.js that simulates future TURTLEDOVE API initializes them with some 29 | personalized ads. Check the website source to see more details! 30 |

31 |

32 | This website doesn't include denied terms for ads, but because of the topic of the site, ad providers 33 | will value more ads about animals. 34 |

35 | 37 |
38 |
39 |

This website is an example publisher that identifies itself as a website about animals.

40 |

Would you like to see more ads? Check out a website about planes 41 | to check out how denied terms list works.

42 |

You can also generate more ads by visiting a website where you can decide if you like 43 | cats or dogs 44 | and by saying that you like planes or trains or by going 45 | to a sports shop. 46 |

47 |

To see how Product-Level TURTLEDOVE works you can visit a specially prepared clothes shop.

48 | 50 |

51 | To check out more details about the state of TURTLEDOVE simulation on your browser you can visit its 52 | user interface. 53 |

54 |
55 |
56 |
57 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /websites/content/aboutplanes.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | About planes 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Website about planes

24 |

Our website uses only privacy-first online advertising 25 | with the usage of state-of-the-art TURTLEDOVE ads.

26 |

27 | We've included iframes marked with class td-demo-ad. Later the script 28 | turtledove.js that simulates future TURTLEDOVE API initializes them with some 29 | personalized ads. Check the website source to see more details! 30 |

31 |

32 | This website, in contrast to the other, has a brand-safety policy - it doesn't like ads with other means 33 | of transport than planes - so trains or bikes ads will not display even if they are more valuable. 34 |

35 | 37 |
38 |
39 |

40 | This website is an example publisher that identifies itself as a website about planes. 41 |

42 |

Would you like to see more ads? See a website about animals to 43 | check out how context-aware prices are working.

44 |

You can also generate more ads by visiting a website where you can decide if you like 45 | cats or dogs 46 | and by saying that you like planes or trains or going to 47 | a sports shop. 48 |

49 |

To see how Product-Level TURTLEDOVE works you can visit a specially prepared clothes shop.

50 | 52 |

53 | To check out more details about the state of TURTLEDOVE simulation on your browser, you can visit its 54 | user interface. 55 |

56 |
57 |
58 |
59 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /websites/content/catordog-product.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CatOrDog - food for <%= it.animal %> 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 39 | 40 |
41 |
42 |

Amazing food for <%= it.animal %>s!

43 | <%= it.animal %> image
44 |

45 | This great food is exactly the food that <%= it.animal %>s lover should buy for their loved <%= it.animal %> 46 | . 47 |

48 |

49 | Click a button below to add yourself to ad interest group. 50 |

51 | 52 |

53 |

54 | Return to main page 55 |

56 |

57 |
58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /websites/content/catordog.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CatOrDog 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 22 | 44 |
45 |

Do you like cats or dogs?

46 |
47 | 53 | 59 |
60 |

61 | This website is a dummy advertiser that shows its ads based on the previously collected information. 62 |

63 |

If you want to choose between trains and plains you should check out this website. 65 | Or you can visit a sports shop. 66 | Would you like to see some ads? Visit a website about animals or planes. 69 |

70 |

71 | To check out more details about the state of TURTLEDOVE simulation on your browser, you can visit its 72 | user interface. 73 |

74 |
75 |
76 |
77 | 78 | 79 | -------------------------------------------------------------------------------- /websites/content/clothes-category.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cool clothes - <%= it.product %> 6 | 7 | 8 | 9 | 10 | 23 | 24 | 25 | 30 | 31 |
32 |

Check out our <%= it.product %>!

33 |
34 | <% it.colors.forEach(function(color) { %> 35 | '> 36 |
37 |
<%= color + ' ' + it.product %>
38 | <%= color + ' ' + it.product %> image
39 |
40 |
41 | <% }) %> 42 |
43 |

44 |

45 | Return to main page 46 |

47 |

48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /websites/content/clothes-product.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cool clothes - <%= it.color %> <%= it.product %> 6 | 7 | 8 | 9 | 10 | 21 | 22 | 23 | 47 | 48 |
49 |

Check out our <%= it.color %> <%= it.product %>!

50 |
51 |
<%= it.color %> <%= it.product %>
52 | <%= it.color %> <%= it.product %> image
53 |
54 |

55 |

56 | Return to listing 57 |

58 |

59 | Return to main page 60 |

61 |

62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /websites/content/clothes.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cool clothes 6 | 7 | 8 | 9 | 10 | 25 | 26 | 27 | 32 |
33 |

Shop with clothes

34 |
35 | <% it.products.forEach(function(product) { %> 36 | 42 | <% }) %> 43 |
44 |

45 | We leverage Product-Level TURTLEDOVE to implement last seen recommender on our ads. 46 | Every time you enter an offer site, the list of last seen offers in our first-party storage is updated and on its base offers for InterestGroup are selected. 47 | Try taking a look at various products on our page, and each time check how our ad changes. Remember, that we don't have any information about your preferences from TURTLEDOVE, so even if you remove an ad or a particular product we may still add it again on the next visit. If you want to reset our internal first-party storage use the button below. 48 |

49 |

50 | Content of our storage:

51 | 52 |

53 |

54 | To get better understanding of Product-Level TURTLEDOVE visit our repository. 55 |

56 |

If you want to choose between trains and plains you should check out 57 | this website. Or if you'd like to choose between cats and 58 | dogs you have to check out another site. 59 | Would you like to see some ads? Visit a website about planes or a website abut animals. 60 |

61 |

62 | To check out more details about the state of TURTLEDOVE simulation on your browser you can visit its 63 | user interface. 64 |

65 |
66 |
67 |
68 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /websites/content/sportequipment-product.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sport equipment- <%= it.product %> 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 31 | 32 |
33 |

Check out our <%= it.product %>!

34 |
35 | 36 |
37 |
Black <%= it.product %>
38 | <%= it.product %> image
39 |
40 |
41 |
Red <%= it.product %>
42 | <%= it.product %> image
43 |
44 |
45 |
Blue <%= it.product %>
46 | <%= it.product %> image
47 |
48 |
49 |
Green <%= it.product %>
50 | <%= it.product %> image
51 |
52 |
53 |

54 |

55 | Return to main page 56 |

57 |

58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /websites/content/sportequipment.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sport equipment 6 | 7 | 8 | 9 | 10 | 23 | 24 | 25 | 26 | 31 |
32 |

Shop with sport equipment

33 |
34 | 40 | 46 | 52 |
53 |

54 | This website is a dummy advertiser that is doing a simple retargeting based on TURTLEDOVE. 55 | Every time someone enters a product listing it removes all InterestsGroups and adds the newest one. 56 |

57 |

If you want to choose between trains and plains you should check out 58 | this website. Or if you'd like to choose between cats and 59 | dogs you have to check out another site. 60 | Would you like to see some ads? Visit a website about animals or a website about planes. 63 |

64 |

65 | To check out more details about the state of TURTLEDOVE simulation on your browser you can visit its 66 | user interface. 67 |

68 |
69 |
70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /websites/content/static/images/animals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dervan/turtledove-js/55954a2baaca60a29c36d5d0fd9cc7d8a4366c72/websites/content/static/images/animals.png -------------------------------------------------------------------------------- /websites/content/static/images/black-caps.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 52 | 55 | 58 | 61 | 65 | 69 | 73 | 140 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /websites/content/static/images/blue-caps.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 52 | 55 | 58 | 61 | 65 | 69 | 73 | 140 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /websites/content/static/images/eagle-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dervan/turtledove-js/55954a2baaca60a29c36d5d0fd9cc7d8a4366c72/websites/content/static/images/eagle-background.png -------------------------------------------------------------------------------- /websites/content/static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dervan/turtledove-js/55954a2baaca60a29c36d5d0fd9cc7d8a4366c72/websites/content/static/images/favicon.png -------------------------------------------------------------------------------- /websites/content/static/images/green-caps.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 52 | 55 | 58 | 61 | 65 | 69 | 73 | 140 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /websites/content/static/images/lot.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /websites/content/static/images/pkp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dervan/turtledove-js/55954a2baaca60a29c36d5d0fd9cc7d8a4366c72/websites/content/static/images/pkp.png -------------------------------------------------------------------------------- /websites/content/static/images/plane-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dervan/turtledove-js/55954a2baaca60a29c36d5d0fd9cc7d8a4366c72/websites/content/static/images/plane-background.png -------------------------------------------------------------------------------- /websites/content/static/images/plane-background.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /websites/content/static/images/purple-caps.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 52 | 55 | 58 | 61 | 65 | 69 | 73 | 140 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /websites/content/static/images/red-caps.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 52 | 55 | 58 | 61 | 65 | 69 | 73 | 140 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /websites/content/static/images/rollerblades-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /websites/content/static/images/rollerblades-green.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /websites/content/static/images/rollerblades-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /websites/content/static/images/rollerblades.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /websites/content/static/images/train2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dervan/turtledove-js/55954a2baaca60a29c36d5d0fd9cc7d8a4366c72/websites/content/static/images/train2.png -------------------------------------------------------------------------------- /websites/content/static/js/retargeting.js: -------------------------------------------------------------------------------- 1 | const viewedKey = 'viewedOffers' 2 | export function saveViewedProduct (productName) { 3 | const sizeLimit = 8 4 | let viewedOffers = JSON.parse(window.localStorage.getItem(viewedKey)) || [] 5 | viewedOffers.unshift(productName) 6 | if (viewedOffers.length > sizeLimit) { 7 | viewedOffers = viewedOffers.slice(0, sizeLimit) 8 | } 9 | window.localStorage.setItem(viewedKey, JSON.stringify(viewedOffers)) 10 | return viewedOffers 11 | } 12 | 13 | export function readViewedProducts () { 14 | return JSON.parse(window.localStorage.getItem(viewedKey)) || [] 15 | } 16 | -------------------------------------------------------------------------------- /websites/content/trainorplane.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TrainOrPlane 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 22 | 41 |
42 |

Do you like trains or planes?

43 |
44 | 50 | 56 |
57 |

58 | This website is a dummy advertiser that shows some external ads basing on the collected 59 | information. 60 |

61 |

If you want to choose between cats and dogs, you have to check out this 62 | website. 63 | Or you can visit a sports shop. 64 | Would you like to see some ads? Visit a website about animals 65 | or planes.

66 |

67 | To check out more details about the state of TURTLEDOVE simulation on your browser, you can visit its 68 | user interface. 69 |

70 |
71 |
72 |
73 | 74 | 75 | -------------------------------------------------------------------------------- /websites/websites.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import eta from 'eta' 3 | import path from 'path' 4 | import { ports, addresses } from '../config.js' 5 | 6 | const __dirname = path.resolve('./websites/') 7 | const statics = express.static(`${__dirname}/content/static`) 8 | 9 | function getRenderedHtml (filename, environment) { 10 | return eta.renderFile(`${__dirname}/content/${filename}`, environment) 11 | } 12 | 13 | const aboutAnimals = express() 14 | aboutAnimals.get('/', async (req, res) => res.send(await getRenderedHtml('aboutanimals.html.ejs', addresses))) 15 | aboutAnimals.use('/static', statics) 16 | aboutAnimals.listen(ports.animalsPublisherPort, 17 | () => console.log(`Animals publisher app listening at http://localhost:${ports.animalsPublisherPort}`)) 18 | 19 | const aboutPlanes = express() 20 | aboutPlanes.get('/', async (req, res) => res.send(await getRenderedHtml('aboutplanes.html.ejs', addresses))) 21 | aboutPlanes.use('/static', statics) 22 | aboutPlanes.listen(ports.planesPublisherPort, 23 | () => console.log(`Planes publisher app listening at http://localhost:${ports.planesPublisherPort}`)) 24 | 25 | const trainOrPlane = express() 26 | trainOrPlane.get('/', async (req, res) => res.send(await getRenderedHtml('trainorplane.html.ejs', addresses))) 27 | trainOrPlane.use('/static', statics) 28 | trainOrPlane.listen(ports.transportAdvertiserPort, 29 | () => console.log(`Transport advertiser app listening at http://localhost:${ports.transportAdvertiserPort}`)) 30 | 31 | const catOrDog = express() 32 | catOrDog.get('/', async (req, res) => res.send(await getRenderedHtml('catordog.html.ejs', addresses))) 33 | catOrDog.get('/catfood', async (req, res) => res.send(await getRenderedHtml('catordog-product.html.ejs', { 34 | ...addresses, 35 | animal: 'cat' 36 | }))) 37 | catOrDog.get('/dogfood', async (req, res) => res.send(await getRenderedHtml('catordog-product.html.ejs', { 38 | ...addresses, 39 | animal: 'dog' 40 | }))) 41 | 42 | catOrDog.use('/static', statics) 43 | catOrDog.listen(ports.animalsAdvertiserPort, 44 | () => console.log(`Animals advertiser app listening at http://localhost:${ports.animalsAdvertiserPort}`)) 45 | 46 | const sportEquipment = express() 47 | sportEquipment.get('/', async (req, res) => res.send(await getRenderedHtml('sportequipment.html.ejs', addresses))) 48 | for (const product of ['scooters', 'bikes', 'rollerblades']) { 49 | sportEquipment.get('/' + product, async (req, res) => res.send(await getRenderedHtml('sportequipment-product.html.ejs', { 50 | ...addresses, 51 | product: product 52 | }))) 53 | } 54 | sportEquipment.use('/static', statics) 55 | sportEquipment.listen(ports.sportEquipmentAdvertiserPort, 56 | () => console.log(`Sports advertiser app listening at http://localhost:${ports.sportEquipmentAdvertiserPort}`)) 57 | 58 | const clothesStore = express() 59 | const colors = ['blue', 'red', 'green', 'purple'] 60 | const products = ['jackets', 'scarfs', 'caps'] 61 | 62 | clothesStore.get('/', async (req, res) => res.send(await getRenderedHtml('clothes.html.ejs', { 63 | ...addresses, 64 | products: products 65 | }))) 66 | for (const product of products) { 67 | clothesStore.get('/' + product, async (req, res) => res.send(await getRenderedHtml('clothes-category.html.ejs', { 68 | ...addresses, 69 | product: product, 70 | colors: colors 71 | }))) 72 | for (const color of colors) { 73 | clothesStore.get('/' + color + '-' + product, async (req, res) => res.send(await getRenderedHtml('clothes-product.html.ejs', { 74 | ...addresses, 75 | color: color, 76 | product: product 77 | }))) 78 | } 79 | } 80 | clothesStore.use('/static', statics) 81 | clothesStore.listen(ports.clothesAdvertiserPort, 82 | () => console.log(`Clothes advertiser app listening at http://localhost:${ports.clothesAdvertiserPort}`)) 83 | --------------------------------------------------------------------------------