Skip to content

Commit 1376adb

Browse files
committed
adview-manager - move code in modules and document
1 parent 0ed60a3 commit 1376adb

File tree

3 files changed

+694
-631
lines changed

3 files changed

+694
-631
lines changed

adview-manager/src/helpers.rs

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
use std::ops::{Add, Mul};
2+
3+
use adex_primitives::{
4+
campaign::Validators,
5+
sentry::{Event, EventType, InsertEventsRequest, CLICK, IMPRESSION},
6+
supermarket::units_for_slot::response::AdUnit,
7+
BigNum, CampaignId,
8+
};
9+
use num_integer::Integer;
10+
11+
use crate::{
12+
manager::{Options, Size},
13+
WAIT_FOR_IMPRESSION,
14+
};
15+
16+
const IPFS_GATEWAY: &str = "https://ipfs.moonicorn.network/ipfs/";
17+
18+
fn normalize_url(url: &str) -> String {
19+
if url.starts_with("ipfs://") {
20+
url.replacen("ipfs://", IPFS_GATEWAY, 1)
21+
} else {
22+
url.to_string()
23+
}
24+
}
25+
26+
fn image_html(on_load: &str, size: Option<Size>, image_url: &str) -> String {
27+
let size = size
28+
.map(|Size { width, height }| format!("width=\"{width}\" height=\"{height}\""))
29+
.unwrap_or_default();
30+
31+
format!("<img loading=\"lazy\" src=\"{image_url}\" alt=\"AdEx ad\" rel=\"nofollow\" onload=\"{on_load}\" {size}>")
32+
}
33+
34+
fn video_html(on_load: &str, size: Option<Size>, image_url: &str, media_mime: &str) -> String {
35+
let size = size
36+
.map(|Size { width, height }| format!("width=\"{width}\" height=\"{height}\""))
37+
.unwrap_or_default();
38+
39+
format!(
40+
"<video {size} loop autoplay onloadeddata=\"{on_load}\" muted>
41+
<source src=\"{image_url}\" type=\"{media_mime}\">
42+
</video>",
43+
)
44+
}
45+
46+
fn adex_icon() -> &'static str {
47+
r#"<a href="https://www.adex.network" target="_blank" rel="noopener noreferrer"
48+
style="position: absolute; top: 0; right: 0;"
49+
>
50+
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="18px"
51+
height="18px" viewBox="0 0 18 18" style="enable-background:new 0 0 18 18;" xml:space="preserve">
52+
<style type="text/css">
53+
.st0{fill:#FFFFFF;}
54+
.st1{fill:#1B75BC;}
55+
</style>
56+
<defs>
57+
</defs>
58+
<rect class="st0" width="18" height="18"/>
59+
<path class="st1" d="M14,12.1L10.9,9L14,5.9L12.1,4L9,7.1L5.9,4L4,5.9L7.1,9L4,12.1L5.9,14L9,10.9l3.1,3.1L14,12.1z M7.9,2L6.4,3.5
60+
L7.9,5L9,3.9L10.1,5l1.5-1.5L10,1.9l-1-1L7.9,2 M7.9,16l-1.5-1.5L7.9,13L9,14.1l1.1-1.1l1.5,1.5L10,16.1l-1,1L7.9,16"/>
61+
</svg>
62+
</a>"#
63+
}
64+
65+
pub(crate) fn is_video(ad_unit: &AdUnit) -> bool {
66+
ad_unit.media_mime.split('/').next() == Some("video")
67+
}
68+
69+
/// Does not copy the JS impl, instead it generates the BigNum from the IPFS CID bytes
70+
pub(crate) fn randomized_sort_pos(ad_unit: &AdUnit, seed: BigNum) -> BigNum {
71+
let bytes = ad_unit.id.0.to_bytes();
72+
73+
let unit_id = BigNum::from_bytes_be(&bytes);
74+
75+
let x: BigNum = unit_id.mul(seed).add(BigNum::from(12345));
76+
77+
x.mod_floor(&BigNum::from(0x80000000))
78+
}
79+
80+
/// Generates the AdUnit HTML for a given ad
81+
pub(crate) fn get_unit_html(
82+
size: Option<Size>,
83+
ad_unit: &AdUnit,
84+
hostname: &str,
85+
on_load: &str,
86+
on_click: &str,
87+
) -> String {
88+
// replace all `"` quotes with a single quote `'`
89+
// these values are used inside `onclick` & `onload` html attributes
90+
let on_load = on_load.replace('\"', "'");
91+
let on_click = on_click.replace('\"', "'");
92+
let image_url = normalize_url(&ad_unit.media_url);
93+
94+
let element_html = if is_video(ad_unit) {
95+
video_html(&on_load, size, &image_url, &ad_unit.media_mime)
96+
} else {
97+
image_html(&on_load, size, &image_url)
98+
};
99+
100+
// @TODO click protection page
101+
let final_target_url = ad_unit.target_url.replace(
102+
"utm_source=adex_PUBHOSTNAME",
103+
&format!("utm_source=AdEx+({hostname})", hostname = hostname),
104+
);
105+
106+
let max_min_size = size
107+
.map(|Size { width, height }| {
108+
format!(
109+
"max-width: {width}px; min-width: {min_width}px; height: {height}px;",
110+
// u64 / 2 will floor the result!
111+
min_width = width / 2
112+
)
113+
})
114+
.unwrap_or_default();
115+
116+
format!("<div style=\"position: relative; overflow: hidden; {style}\">
117+
<a href=\"{final_target_url}\" target=\"_blank\" onclick=\"{on_click}\" rel=\"noopener noreferrer\">
118+
{element_html}
119+
</a>
120+
{adex_icon}
121+
</div>", style=max_min_size, adex_icon=adex_icon())
122+
}
123+
124+
/// Generates the HTML for showing an Ad ([`AdUnit`]), as well as, the code for sending the events.
125+
///
126+
/// `no_impression` - whether or not an [`IMPRESSION`] event should be sent with `onload`.
127+
///
128+
/// - [`WAIT_FOR_IMPRESSION`] - The time that needs to pass before sending the [`IMPRESSION`] event to all validators.
129+
pub fn get_unit_html_with_events(
130+
options: &Options,
131+
ad_unit: &AdUnit,
132+
hostname: &str,
133+
campaign_id: CampaignId,
134+
validators: &Validators,
135+
no_impression: bool,
136+
) -> String {
137+
let get_fetch_code = |event_type: EventType| -> String {
138+
let event = match event_type {
139+
EventType::Impression => Event::Impression {
140+
publisher: options.publisher_addr,
141+
ad_unit: ad_unit.id,
142+
ad_slot: options.market_slot,
143+
referrer: Some("document.referrer".to_string()),
144+
},
145+
EventType::Click => Event::Click {
146+
publisher: options.publisher_addr,
147+
ad_unit: ad_unit.id,
148+
ad_slot: options.market_slot,
149+
referrer: Some("document.referrer".to_string()),
150+
},
151+
};
152+
let events_body = InsertEventsRequest {
153+
events: vec![event],
154+
};
155+
let body =
156+
serde_json::to_string(&events_body).expect("It should always serialize EventBody");
157+
158+
// TODO: check whether the JSON body with `''` quotes executes correctly!
159+
let fetch_opts = format!("var fetchOpts = {{ method: 'POST', headers: {{ 'content-type': 'application/json' }}, body: {body} }};");
160+
161+
let validators: String = validators
162+
.iter()
163+
.map(|validator| {
164+
let fetch_url = format!(
165+
"{}/campaign/{}/events?pubAddr={}",
166+
validator.url, campaign_id, options.publisher_addr
167+
);
168+
169+
format!("fetch('{}', fetchOpts)", fetch_url)
170+
})
171+
.collect::<Vec<_>>()
172+
.join("; ");
173+
174+
format!("{fetch_opts} {validators}")
175+
};
176+
177+
let get_timeout_code = |event_type: EventType| -> String {
178+
format!(
179+
"setTimeout(function() {{ {code} }}, {timeout})",
180+
code = get_fetch_code(event_type),
181+
timeout = WAIT_FOR_IMPRESSION.num_milliseconds()
182+
)
183+
};
184+
185+
let on_load = if no_impression {
186+
String::new()
187+
} else {
188+
get_timeout_code(IMPRESSION)
189+
};
190+
191+
get_unit_html(
192+
options.size,
193+
ad_unit,
194+
hostname,
195+
&on_load,
196+
&get_fetch_code(CLICK),
197+
)
198+
}
199+
200+
#[cfg(test)]
201+
mod test {
202+
use super::*;
203+
use adex_primitives::test_util::DUMMY_IPFS;
204+
205+
fn get_ad_unit(media_mime: &str) -> AdUnit {
206+
AdUnit {
207+
id: DUMMY_IPFS[0],
208+
media_url: "".to_string(),
209+
media_mime: media_mime.to_string(),
210+
target_url: "".to_string(),
211+
}
212+
}
213+
214+
#[test]
215+
fn test_is_video() {
216+
assert!(is_video(&get_ad_unit("video/avi")));
217+
assert!(!is_video(&get_ad_unit("image/jpeg")));
218+
}
219+
220+
#[test]
221+
fn normalization_of_url() {
222+
// IPFS case
223+
assert_eq!(format!("{}123", IPFS_GATEWAY), normalize_url("ipfs://123"));
224+
assert_eq!(
225+
format!("{}123ipfs://", IPFS_GATEWAY),
226+
normalize_url("ipfs://123ipfs://")
227+
);
228+
229+
// Non-IPFS case
230+
assert_eq!("http://123".to_string(), normalize_url("http://123"));
231+
}
232+
233+
mod randomized_sort_pos {
234+
235+
use super::*;
236+
237+
#[test]
238+
fn test_randomized_position() {
239+
let ad_unit = AdUnit {
240+
id: DUMMY_IPFS[0],
241+
media_url: "ipfs://QmWWQSuPMS6aXCbZKpEjPHPUZN2NjB3YrhJTHsV4X3vb2t".to_string(),
242+
media_mime: "image/jpeg".to_string(),
243+
target_url: "https://google.com".to_string(),
244+
};
245+
246+
let result = randomized_sort_pos(&ad_unit, 5.into());
247+
248+
// The seed is responsible for generating different results since the AdUnit IPFS can be the same
249+
assert_eq!(BigNum::from(177_349_401), result);
250+
}
251+
}
252+
}

0 commit comments

Comments
 (0)