diff --git a/integrationExamples/gpt/afpExample.html b/integrationExamples/gpt/afpExample.html
new file mode 100644
index 00000000000..a1e6e800d69
--- /dev/null
+++ b/integrationExamples/gpt/afpExample.html
@@ -0,0 +1,242 @@
+
+
+
+
+ Prebid.js Banner Example
+
+
+
+
+ In-image
+
+
+
+
+
+
+
+ In-image Max
+
+
+
+
+
+
+
+ In-content Banner
+
+
+ In-content Stories
+
+
+ Action Scroller
+
+
+ Action Scroller Light
+
+
+ Just Banner
+
+
+ In-content Video
+
+
+
+
+
+
diff --git a/integrationExamples/gpt/afpGamExample.html b/integrationExamples/gpt/afpGamExample.html
new file mode 100644
index 00000000000..64cb169893c
--- /dev/null
+++ b/integrationExamples/gpt/afpGamExample.html
@@ -0,0 +1,152 @@
+
+
+
+
+ Prebid.js Banner Example
+
+
+
+
+
+ In-image
+
+
+
+
+
+
+
+
+
+ In-content Video
+
+
+ Action Scroller
+
+
+
diff --git a/modules/afpBidAdapter.js b/modules/afpBidAdapter.js
new file mode 100644
index 00000000000..68941ff17c9
--- /dev/null
+++ b/modules/afpBidAdapter.js
@@ -0,0 +1,166 @@
+import includes from 'core-js-pure/features/array/includes.js'
+import { registerBidder } from '../src/adapters/bidderFactory.js'
+import { Renderer } from '../src/Renderer.js'
+import { BANNER, VIDEO } from '../src/mediaTypes.js'
+
+export const IS_DEV = location.hostname === 'localhost'
+export const BIDDER_CODE = 'afp'
+export const SSP_ENDPOINT = 'https://ssp.afp.ai/api/prebid'
+export const REQUEST_METHOD = 'POST'
+export const TEST_PAGE_URL = 'https://rtbinsight.ru/smiert-bolshikh-dannykh-kto-na-novienkogo/'
+const SDK_PATH = 'https://cdn.afp.ai/ssp/sdk.js?auto_initialization=false&deploy_to_parent_window=true'
+const TTL = 60
+export const IN_IMAGE_BANNER_TYPE = 'In-image'
+export const IN_IMAGE_MAX_BANNER_TYPE = 'In-image Max'
+export const IN_CONTENT_BANNER_TYPE = 'In-content Banner'
+export const IN_CONTENT_VIDEO_TYPE = 'In-content Video'
+export const OUT_CONTENT_VIDEO_TYPE = 'Out-content Video'
+export const IN_CONTENT_STORY_TYPE = 'In-content Stories'
+export const ACTION_SCROLLER_TYPE = 'Action Scroller'
+export const ACTION_SCROLLER_LIGHT_TYPE = 'Action Scroller Light'
+export const JUST_BANNER_TYPE = 'Just Banner'
+
+export const mediaTypeByPlaceType = {
+ [IN_IMAGE_BANNER_TYPE]: BANNER,
+ [IN_IMAGE_MAX_BANNER_TYPE]: BANNER,
+ [IN_CONTENT_BANNER_TYPE]: BANNER,
+ [IN_CONTENT_STORY_TYPE]: BANNER,
+ [ACTION_SCROLLER_TYPE]: BANNER,
+ [ACTION_SCROLLER_LIGHT_TYPE]: BANNER,
+ [JUST_BANNER_TYPE]: BANNER,
+ [IN_CONTENT_VIDEO_TYPE]: VIDEO,
+ [OUT_CONTENT_VIDEO_TYPE]: VIDEO,
+}
+
+const wrapAd = (dataToCreatePlace) => {
+ return `
+
+
+
+
+
+
+
+
+
+ `
+}
+
+const bidRequestMap = {}
+
+const createRenderer = (bid, dataToCreatePlace) => {
+ const renderer = new Renderer({
+ targetId: bid.adUnitCode,
+ url: SDK_PATH,
+ callback() {
+ renderer.loaded = true
+ window.afp.createPlaceByData(dataToCreatePlace)
+ }
+ })
+
+ return renderer
+}
+
+export const spec = {
+ code: BIDDER_CODE,
+ supportedMediaTypes: [BANNER, VIDEO],
+ isBidRequestValid({mediaTypes, params}) {
+ if (typeof params !== 'object' || typeof mediaTypes !== 'object') {
+ return false
+ }
+
+ const {placeId, placeType, imageUrl, imageWidth, imageHeight} = params
+ const media = mediaTypes[mediaTypeByPlaceType[placeType]]
+
+ if (placeId && media) {
+ if (mediaTypeByPlaceType[placeType] === VIDEO) {
+ if (!media.playerSize) {
+ return false
+ }
+ } else if (mediaTypeByPlaceType[placeType] === BANNER) {
+ if (!media.sizes) {
+ return false
+ }
+ }
+ if (includes([IN_IMAGE_BANNER_TYPE, IN_IMAGE_MAX_BANNER_TYPE], placeType)) {
+ if (imageUrl && imageWidth && imageHeight) {
+ return true
+ }
+ } else {
+ return true
+ }
+ }
+ return false
+ },
+ buildRequests(validBidRequests, {refererInfo, gdprConsent}) {
+ const payload = {
+ pageUrl: IS_DEV ? TEST_PAGE_URL : refererInfo.referer,
+ gdprConsent: gdprConsent,
+ bidRequests: validBidRequests.map(validBidRequest => {
+ const {bidId, transactionId, sizes, params: {
+ placeId, placeType, imageUrl, imageWidth, imageHeight
+ }} = validBidRequest
+ bidRequestMap[bidId] = validBidRequest
+ const bidRequest = {
+ bidId,
+ transactionId,
+ sizes,
+ placeId,
+ }
+ if (includes([IN_IMAGE_BANNER_TYPE, IN_IMAGE_MAX_BANNER_TYPE], placeType)) {
+ Object.assign(bidRequest, {
+ imageUrl,
+ imageWidth: Math.floor(imageWidth),
+ imageHeight: Math.floor(imageHeight),
+ })
+ }
+ return bidRequest
+ })
+ }
+
+ return {
+ method: REQUEST_METHOD,
+ url: SSP_ENDPOINT,
+ data: payload,
+ options: {
+ contentType: 'application/json'
+ }
+ }
+ },
+ interpretResponse(serverResponse) {
+ let bids = serverResponse.body && serverResponse.body.bids
+ bids = Array.isArray(bids) ? bids : []
+
+ return bids.map(({bidId, cpm, width, height, creativeId, currency, netRevenue, adSettings, placeSettings}, index) => {
+ const bid = {
+ requestId: bidId,
+ cpm,
+ width,
+ height,
+ creativeId,
+ currency,
+ netRevenue,
+ meta: {
+ mediaType: mediaTypeByPlaceType[placeSettings.placeType],
+ },
+ ttl: TTL
+ }
+
+ const bidRequest = bidRequestMap[bidId]
+ const placeContainer = bidRequest.params.placeContainer
+ const dataToCreatePlace = { adSettings, placeSettings, placeContainer, isPrebid: true }
+
+ if (mediaTypeByPlaceType[placeSettings.placeType] === BANNER) {
+ bid.ad = wrapAd(dataToCreatePlace)
+ } else if (mediaTypeByPlaceType[placeSettings.placeType] === VIDEO) {
+ bid.vastXml = adSettings.content
+ bid.renderer = createRenderer(bid, dataToCreatePlace)
+ }
+ return bid
+ })
+ }
+}
+
+registerBidder(spec);
diff --git a/modules/afpBidAdapter.md b/modules/afpBidAdapter.md
new file mode 100644
index 00000000000..75ebf2bce48
--- /dev/null
+++ b/modules/afpBidAdapter.md
@@ -0,0 +1,348 @@
+# Overview
+
+
+**Module Name**: AFP Bidder Adapter
+**Module Type**: Bidder Adapter
+**Maintainer**: devops@astraone.io
+
+# Description
+
+You can use this adapter to get a bid from AFP.
+Please reach out to your AFP account team before using this plugin to get placeId.
+The code below returns a demo ad.
+
+About us: https://afp.ai
+
+# Test Parameters
+```js
+var adUnits = [{
+ code: 'iib-target',
+ mediaTypes: {
+ banner: {
+ sizes: [[0, 0]],
+ }
+ },
+ bids: [{
+ bidder: "afp",
+ params: {
+ placeType: "In-image",
+ placeId: "613221112871613d1517d181", // id from personal account
+ placeContainer: '#iib-container',
+ imageUrl: "https://rtbinsight.ru/content/images/size/w1000/2021/05/ximage-30.png.pagespeed.ic.IfuX4zAEPP.png",
+ imageWidth: 1000,
+ imageHeight: 524,
+ }
+ }]
+}];
+
+var adUnits = [{
+ code: 'iimb-target',
+ mediaTypes: {
+ banner: {
+ sizes: [[0, 0]],
+ }
+ },
+ bids: [{
+ bidder: "afp",
+ params: {
+ placeType: "In-image Max",
+ placeId: "6139ae472871613d1517dedd", // id from personal account
+ placeContainer: '#iimb-container',
+ imageUrl: "https://rtbinsight.ru/content/images/size/w1000/2021/05/ximage-30.png.pagespeed.ic.IfuX4zAEPP.png",
+ imageWidth: 1000,
+ imageHeight: 524,
+ }
+ }]
+}];
+
+var adUnits = [{
+ code: 'icb-target',
+ mediaTypes: {
+ banner: {
+ sizes: [[0, 0]],
+ }
+ },
+ bids: [{
+ bidder: "afp",
+ params: {
+ placeType: "In-content Banner",
+ placeId: "6139ae082871613d1517dec0", // id from personal account
+ placeContainer: '#icb-container',
+ }
+ }]
+}];
+
+var adUnits = [{
+ code: 'ics-target',
+ mediaTypes: {
+ banner: {
+ sizes: [[0, 0]],
+ }
+ },
+ bids: [{
+ bidder: "afp",
+ params: {
+ placeType: "In-content Stories",
+ placeId: "6139ae292871613d1517ded3", // id from personal account
+ placeContainer: '#ics-container',
+ }
+ }]
+}];
+
+var adUnits = [{
+ code: 'as-target',
+ mediaTypes: {
+ banner: {
+ sizes: [[0, 0]],
+ }
+ },
+ bids: [{
+ bidder: "afp",
+ params: {
+ placeType: "Action Scroller",
+ placeId: "6139adc12871613d1517deb0", // id from personal account
+ placeContainer: '#as-container',
+ }
+ }]
+}];
+
+var adUnits = [{
+ code: 'asl-target',
+ mediaTypes: {
+ banner: {
+ sizes: [[0, 0]],
+ }
+ },
+ bids: [{
+ bidder: "afp",
+ params: {
+ placeType: "Action Scroller Light",
+ placeId: "6139adda2871613d1517deb8", // id from personal account
+ placeContainer: '#asl-container',
+ }
+ }]
+}];
+
+var adUnits = [{
+ code: 'jb-target',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250]],
+ }
+ },
+ bids: [{
+ bidder: "afp",
+ params: {
+ placeType: "Just Banner",
+ placeId: "6139ae832871613d1517dee9", // id from personal account
+ placeContainer: '#jb-container',
+ }
+ }]
+}];
+
+var adUnits = [{
+ code: 'icv-target',
+ mediaTypes: {
+ video: {
+ playerSize: [[480, 320]],
+ }
+ },
+ bids: [{
+ bidder: "afp",
+ params: {
+ placeType: "In-content Video",
+ placeId: "6139ae182871613d1517deca", // id from personal account
+ placeContainer: '#icv-container',
+ }
+ }]
+}];
+
+var adUnits = [{
+ code: 'ocv-target',
+ mediaTypes: {
+ video: {
+ playerSize: [[480, 320]],
+ }
+ },
+ bids: [{
+ bidder: "afp",
+ params: {
+ placeType: "Out-content Video",
+ placeId: "6139ae5b2871613d1517dee2", // id from personal account
+ placeContainer: '#ocv-container', // only the "body" tag is used as a container
+ }
+ }]
+}];
+```
+
+# Example page
+
+```html
+
+
+
+
+ Prebid.js In-image Example
+
+
+
+
+ In-image
+
+
+
+
+
+
+
+ Just Banner
+
+
+
+
+```
+# Example page with GPT
+
+```html
+
+
+
+
+ Prebid.js In-image Example
+
+
+
+
+
+ In-image
+
+
+
+
+
+
+
+
+
+
+```
diff --git a/test/spec/modules/afpBidAdapter_spec.js b/test/spec/modules/afpBidAdapter_spec.js
new file mode 100644
index 00000000000..8e77a1f3e15
--- /dev/null
+++ b/test/spec/modules/afpBidAdapter_spec.js
@@ -0,0 +1,306 @@
+import includes from 'core-js-pure/features/array/includes.js'
+import cloneDeep from 'lodash/cloneDeep'
+import unset from 'lodash/unset'
+import { expect } from 'chai'
+import { BANNER, VIDEO } from '../../../src/mediaTypes.js'
+import {
+ spec,
+ IN_IMAGE_BANNER_TYPE,
+ IN_IMAGE_MAX_BANNER_TYPE,
+ IN_CONTENT_BANNER_TYPE,
+ IN_CONTENT_VIDEO_TYPE,
+ OUT_CONTENT_VIDEO_TYPE,
+ IN_CONTENT_STORY_TYPE,
+ ACTION_SCROLLER_TYPE,
+ ACTION_SCROLLER_LIGHT_TYPE,
+ JUST_BANNER_TYPE,
+ BIDDER_CODE,
+ SSP_ENDPOINT,
+ REQUEST_METHOD,
+ TEST_PAGE_URL,
+ IS_DEV, mediaTypeByPlaceType
+} from 'modules/afpBidAdapter.js'
+
+const placeId = '613221112871613d1517d181'
+const bidId = '2a67c5577ff6a5'
+const transactionId = '7e8515a2-2ed9-4733-b976-6c2596a03287'
+const imageUrl = 'https://rtbinsight.ru/content/images/size/w1000/2021/05/ximage-30.png.pagespeed.ic.IfuX4zAEPP.png'
+const placeContainer = '#container'
+const imageWidth = 600
+const imageHeight = 400
+const pageUrl = IS_DEV ? TEST_PAGE_URL : 'referer'
+const sizes = [[imageWidth, imageHeight]]
+const bidderRequest = {
+ refererInfo: { referer: pageUrl },
+}
+const mediaTypeBanner = { [BANNER]: {sizes: [[imageWidth, imageHeight]]} }
+const mediaTypeVideo = { [VIDEO]: {playerSize: [[imageWidth, imageHeight]]} }
+const commonParams = {
+ placeId,
+ placeContainer,
+}
+const commonParamsForInImage = Object.assign({}, commonParams, {
+ imageUrl,
+ imageWidth,
+ imageHeight,
+})
+const configByPlaceType = {
+ get [IN_IMAGE_BANNER_TYPE]() {
+ return cloneDeep({
+ mediaTypes: mediaTypeBanner,
+ params: Object.assign({}, commonParamsForInImage, {
+ placeType: IN_IMAGE_BANNER_TYPE
+ }),
+ })
+ },
+ get [IN_IMAGE_MAX_BANNER_TYPE]() {
+ return cloneDeep({
+ mediaTypes: mediaTypeBanner,
+ params: Object.assign({}, commonParamsForInImage, {
+ placeType: IN_IMAGE_MAX_BANNER_TYPE
+ }),
+ })
+ },
+ get [IN_CONTENT_BANNER_TYPE]() {
+ return cloneDeep({
+ mediaTypes: mediaTypeBanner,
+ params: Object.assign({}, commonParams, {
+ placeType: IN_CONTENT_BANNER_TYPE
+ }),
+ })
+ },
+ get [IN_CONTENT_VIDEO_TYPE]() {
+ return cloneDeep({
+ mediaTypes: mediaTypeVideo,
+ params: Object.assign({}, commonParams, {
+ placeType: IN_CONTENT_VIDEO_TYPE
+ }),
+ })
+ },
+ get [OUT_CONTENT_VIDEO_TYPE]() {
+ return cloneDeep({
+ mediaTypes: mediaTypeVideo,
+ params: Object.assign({}, commonParams, {
+ placeType: OUT_CONTENT_VIDEO_TYPE
+ }),
+ })
+ },
+ get [IN_CONTENT_STORY_TYPE]() {
+ return cloneDeep({
+ mediaTypes: mediaTypeBanner,
+ params: Object.assign({}, commonParams, {
+ placeType: IN_CONTENT_STORY_TYPE
+ }),
+ })
+ },
+ get [ACTION_SCROLLER_TYPE]() {
+ return cloneDeep({
+ mediaTypes: mediaTypeBanner,
+ params: Object.assign({}, commonParams, {
+ placeType: ACTION_SCROLLER_TYPE
+ }),
+ })
+ },
+ get [ACTION_SCROLLER_LIGHT_TYPE]() {
+ return cloneDeep({
+ mediaTypes: mediaTypeBanner,
+ params: Object.assign({}, commonParams, {
+ placeType: ACTION_SCROLLER_LIGHT_TYPE
+ }),
+ })
+ },
+ get [JUST_BANNER_TYPE]() {
+ return cloneDeep({
+ mediaTypes: mediaTypeBanner,
+ params: Object.assign({}, commonParams, {
+ placeType: JUST_BANNER_TYPE
+ }),
+ })
+ },
+}
+const getTransformedConfig = ({mediaTypes, params}) => {
+ return {
+ params: params,
+ sizes,
+ bidId,
+ bidder: BIDDER_CODE,
+ mediaTypes: mediaTypes,
+ transactionId,
+ }
+}
+const validBidRequests = Object.keys(configByPlaceType).map(key => getTransformedConfig(configByPlaceType[key]))
+
+describe('AFP Adapter', function() {
+ describe('isBidRequestValid method', function() {
+ describe('returns true', function() {
+ describe('when config has all mandatory params', () => {
+ Object.keys(configByPlaceType).forEach(placeType => {
+ it(`and ${placeType} config has the correct value`, function() {
+ const isBidRequestValid = spec.isBidRequestValid(configByPlaceType[placeType])
+ expect(isBidRequestValid).to.equal(true)
+ })
+ })
+ })
+ })
+ describe('returns false', function() {
+ const checkMissingParams = (placesTypes, missingParams) =>
+ placesTypes.forEach(placeType =>
+ missingParams.forEach(missingParam => {
+ const config = configByPlaceType[placeType]
+ it(`${placeType} does not have the ${missingParam}.`, function() {
+ unset(config, missingParam)
+ const isBidRequestValid = spec.isBidRequestValid(config)
+ expect(isBidRequestValid).to.equal(false)
+ })
+ })
+ )
+
+ describe('when params are not correct', function() {
+ checkMissingParams(Object.keys(configByPlaceType), ['params.placeId', 'params.placeType'])
+ checkMissingParams([IN_IMAGE_BANNER_TYPE, IN_IMAGE_MAX_BANNER_TYPE],
+ ['params.imageUrl', 'params.imageWidth', 'params.imageHeight'])
+
+ it('does not have a the correct placeType.', function() {
+ const config = configByPlaceType[IN_IMAGE_BANNER_TYPE]
+ config.params.placeType = 'something'
+ const isBidRequestValid = spec.isBidRequestValid(config)
+ expect(isBidRequestValid).to.equal(false)
+ })
+ })
+ describe('when video mediaType object is not correct.', function() {
+ checkMissingParams([IN_CONTENT_VIDEO_TYPE, OUT_CONTENT_VIDEO_TYPE],
+ [`mediaTypes.${VIDEO}.playerSize`, `mediaTypes.${VIDEO}`])
+ checkMissingParams([
+ IN_IMAGE_BANNER_TYPE,
+ IN_IMAGE_MAX_BANNER_TYPE,
+ IN_CONTENT_BANNER_TYPE,
+ IN_CONTENT_STORY_TYPE,
+ ACTION_SCROLLER_TYPE,
+ ACTION_SCROLLER_LIGHT_TYPE,
+ JUST_BANNER_TYPE
+ ], [`mediaTypes.${BANNER}.sizes`, `mediaTypes.${BANNER}`])
+ })
+ })
+ })
+
+ describe('buildRequests method', function() {
+ const request = spec.buildRequests(validBidRequests, bidderRequest)
+
+ it('Url should be correct', function() {
+ expect(request.url).to.equal(SSP_ENDPOINT)
+ })
+
+ it('Method should be correct', function() {
+ expect(request.method).to.equal(REQUEST_METHOD)
+ })
+
+ describe('Common data request should be correct', function() {
+ it('pageUrl should be correct', function() {
+ expect(request.data.pageUrl).to.equal(pageUrl)
+ })
+ it('bidRequests should be array', function() {
+ expect(Array.isArray(request.data.bidRequests)).to.equal(true)
+ })
+
+ request.data.bidRequests.forEach((bid, index) => {
+ describe(`bid with ${validBidRequests[index].params.placeType} should be correct`, function() {
+ it('bidId should be correct', function() {
+ expect(bid.bidId).to.equal(bidId)
+ })
+ it('placeId should be correct', function() {
+ expect(bid.placeId).to.equal(placeId)
+ })
+ it('transactionId should be correct', function() {
+ expect(bid.transactionId).to.equal(transactionId)
+ })
+ it('sizes should be correct', function() {
+ expect(bid.sizes).to.equal(sizes)
+ })
+
+ if (includes([IN_IMAGE_BANNER_TYPE, IN_IMAGE_MAX_BANNER_TYPE], validBidRequests[index].params.placeType)) {
+ it('imageUrl should be correct', function() {
+ expect(bid.imageUrl).to.equal(imageUrl)
+ })
+ it('imageWidth should be correct', function() {
+ expect(bid.imageWidth).to.equal(Math.floor(imageWidth))
+ })
+ it('imageHeight should be correct', function() {
+ expect(bid.imageHeight).to.equal(Math.floor(imageHeight))
+ })
+ }
+ })
+ })
+ })
+ })
+
+ describe('interpretResponse method', function() {
+ it('should return a void array, when the server response are not correct.', function() {
+ const request = { data: JSON.stringify({}) }
+ const serverResponse = {
+ body: {}
+ }
+ const bids = spec.interpretResponse(serverResponse, request)
+ expect(Array.isArray(bids)).to.equal(true)
+ expect(bids.length).to.equal(0)
+ })
+ it('should return a void array, when the server response have not got bids.', function() {
+ const request = { data: JSON.stringify({}) }
+ const serverResponse = { body: { bids: [] } }
+ const bids = spec.interpretResponse(serverResponse, request)
+ expect(Array.isArray(bids)).to.equal(true)
+ expect(bids.length).to.equal(0)
+ })
+ describe('when the server response return a bids', function() {
+ Object.keys(configByPlaceType).forEach(placeType => {
+ it(`should return a bid with ${placeType} placeType`, function() {
+ const cpm = 10
+ const currency = 'RUB'
+ const creativeId = '123'
+ const netRevenue = true
+ const width = sizes[0][0]
+ const height = sizes[0][1]
+ const adSettings = {
+ content: 'html'
+ }
+ const placeSettings = {
+ placeType,
+ }
+ const request = spec.buildRequests([validBidRequests[0]], bidderRequest)
+ const serverResponse = {
+ body: {
+ bids: [
+ {
+ bidId,
+ cpm,
+ currency,
+ creativeId,
+ netRevenue,
+ width,
+ height,
+ adSettings,
+ placeSettings,
+ }
+ ]
+ }
+ }
+ const bids = spec.interpretResponse(serverResponse, request)
+ expect(bids.length).to.equal(1)
+ expect(bids[0].requestId).to.equal(bidId)
+ expect(bids[0].meta.mediaType).to.equal(mediaTypeByPlaceType[placeSettings.placeType])
+ expect(bids[0].cpm).to.equal(cpm)
+ expect(bids[0].width).to.equal(width)
+ expect(bids[0].height).to.equal(height)
+ expect(bids[0].currency).to.equal(currency)
+ expect(bids[0].netRevenue).to.equal(netRevenue)
+
+ if (mediaTypeByPlaceType[placeSettings.placeType] === BANNER) {
+ expect(typeof bids[0].ad).to.equal('string')
+ } else if (mediaTypeByPlaceType[placeSettings.placeType] === VIDEO) {
+ expect(typeof bids[0].vastXml).to.equal('string')
+ expect(typeof bids[0].renderer).to.equal('object')
+ }
+ })
+ })
+ })
+ })
+})