1
0

Improve ad security, add CMP changes (#2399)

This commit is contained in:
Geometrically
2024-09-13 20:33:51 -07:00
committed by GitHub
parent 3d619e6a98
commit 95cd48571e
16 changed files with 220 additions and 169 deletions

View File

@@ -4,7 +4,7 @@ import { get as getCreds } from '@/helpers/mr_auth.js'
import { handleError } from '@/store/notifications.js'
import { get_user } from '@/helpers/cache.js'
import { ChevronRightIcon } from '@modrinth/assets'
import { init_ads_window } from '@/helpers/ads.js'
import { init_ads_window, open_ads_link, record_ads_click } from '@/helpers/ads.js'
import { listen } from '@tauri-apps/api/event'
const showAd = ref(true)
@@ -73,6 +73,11 @@ function updateAdPosition(overrideShown = false) {
}
}
async function openPlusLink() {
await record_ads_click()
await open_ads_link('https://modrinth.com/plus', 'https://modrinth.com')
}
const unlisten = await listen('ads-scroll', (event) => {
if (adsWrapper.value) {
adsWrapper.value.parentNode.scrollTop += event.payload.scroll
@@ -105,17 +110,17 @@ onUnmounted(() => {
class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised cursor-pointer"
>
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
<p class="m-0 text-2xl font-bold text-contrast">90% of ad revenue goes to creators</p>
<a
href="https://modrinth.com/plus"
class="mt-auto items-center gap-1 text-purple hover:underline"
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
<button
class="mt-auto items-center gap-1 text-purple hover:underline bg-transparent border-none text-left cursor-pointer outline-none"
@click="openPlusLink"
>
<span>
Support creators and Modrinth ad-free with
<span class="font-bold">Modrinth+</span>
</span>
<ChevronRightIcon class="relative top-[3px] h-5 w-5" />
</a>
</button>
</div>
</div>
</template>

View File

@@ -11,3 +11,11 @@ export async function show_ads_window() {
export async function hide_ads_window(reset) {
return await invoke('plugin:ads|hide_ads_window', { reset })
}
export async function record_ads_click() {
return await invoke('plugin:ads|record_ads_click')
}
export async function open_ads_link(path, origin) {
return await invoke('plugin:ads|open_link', { path, origin })
}

View File

@@ -226,6 +226,8 @@ fn main() {
"hide_ads_window",
"scroll_ads_window",
"show_ads_window",
"record_ads_click",
"open_link",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,

View File

@@ -9,7 +9,6 @@
"ads-window"
],
"permissions": [
"shell:allow-open",
"ads:default"
]
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"ads":{"identifier":"ads","description":"","remote":{"urls":["https://modrinth.com/*","http://localhost:3000/*"]},"local":false,"webviews":["ads-window"],"permissions":["shell:allow-open","ads:default"]},"core":{"identifier":"core","description":"","local":true,"windows":["main"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-create","core:window:allow-maximize","core:window:allow-toggle-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-start-dragging","core:webview:allow-set-webview-zoom"]},"plugins":{"identifier":"plugins","description":"","local":true,"windows":["main"],"permissions":["dialog:allow-open","dialog:allow-confirm","shell:allow-open","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","deep-link:default","window-state:default","window-state:allow-restore-state","window-state:allow-save-window-state","auth:default","import:default","jre:default","logs:default","metadata:default","mr-auth:default","profile-create:default","pack:default","process:default","profile:default","cache:default","settings:default","tags:default","utils:default","ads:default"]},"updater":{"identifier":"updater","description":"","local":true,"windows":["main"],"permissions":["updater:default"]}}
{"ads":{"identifier":"ads","description":"","remote":{"urls":["https://modrinth.com/*","http://localhost:3000/*"]},"local":false,"webviews":["ads-window"],"permissions":["ads:default"]},"core":{"identifier":"core","description":"","local":true,"windows":["main"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-create","core:window:allow-maximize","core:window:allow-toggle-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-start-dragging","core:webview:allow-set-webview-zoom"]},"plugins":{"identifier":"plugins","description":"","local":true,"windows":["main"],"permissions":["dialog:allow-open","dialog:allow-confirm","shell:allow-open","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","deep-link:default","window-state:default","window-state:allow-restore-state","window-state:allow-save-window-state","auth:default","import:default","jre:default","logs:default","metadata:default","mr-auth:default","profile-create:default","pack:default","process:default","profile:default","cache:default","settings:default","tags:default","utils:default","ads:default"]},"updater":{"identifier":"updater","description":"","local":true,"windows":["main"],"permissions":["updater:default"]}}

View File

@@ -320,6 +320,20 @@
"ads:allow-init-ads-window"
]
},
{
"description": "ads:allow-open-link -> Enables the open_link command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-open-link"
]
},
{
"description": "ads:allow-record-ads-click -> Enables the record_ads_click command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-record-ads-click"
]
},
{
"description": "ads:allow-scroll-ads-window -> Enables the scroll_ads_window command without any pre-configured scope.",
"type": "string",
@@ -348,6 +362,20 @@
"ads:deny-init-ads-window"
]
},
{
"description": "ads:deny-open-link -> Denies the open_link command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-open-link"
]
},
{
"description": "ads:deny-record-ads-click -> Denies the record_ads_click command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-record-ads-click"
]
},
{
"description": "ads:deny-scroll-ads-window -> Denies the scroll_ads_window command without any pre-configured scope.",
"type": "string",

View File

@@ -320,6 +320,20 @@
"ads:allow-init-ads-window"
]
},
{
"description": "ads:allow-open-link -> Enables the open_link command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-open-link"
]
},
{
"description": "ads:allow-record-ads-click -> Enables the record_ads_click command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-record-ads-click"
]
},
{
"description": "ads:allow-scroll-ads-window -> Enables the scroll_ads_window command without any pre-configured scope.",
"type": "string",
@@ -348,6 +362,20 @@
"ads:deny-init-ads-window"
]
},
{
"description": "ads:deny-open-link -> Denies the open_link command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-open-link"
]
},
{
"description": "ads:deny-record-ads-click -> Denies the record_ads_click command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-record-ads-click"
]
},
{
"description": "ads:deny-scroll-ads-window -> Denies the scroll_ads_window command without any pre-configured scope.",
"type": "string",

View File

@@ -1,6 +1,8 @@
if (!window.modrinthClickListener) {
window.modrinthClickListener = true
document.addEventListener('click', function (e) {
document.addEventListener(
'click',
function (e) {
window.top.postMessage({ modrinthAdClick: true }, 'https://modrinth.com')
let target = e.target
while (target != null) {
if (target.matches('a')) {
@@ -12,8 +14,9 @@ if (!window.modrinthClickListener) {
}
target = target.parentElement
}
})
}
},
true,
)
window.open = (url, target, features) => {
window.top.postMessage({ modrinthOpenUrl: url }, 'https://modrinth.com')

View File

@@ -1,12 +1,19 @@
use serde::Serialize;
use std::collections::HashSet;
use std::time::{Duration, Instant};
use tauri::plugin::TauriPlugin;
use tauri::{Emitter, LogicalPosition, LogicalSize, Manager, Runtime};
use tauri::{
Emitter, Listener, LogicalPosition, LogicalSize, Manager, Runtime,
};
use tauri_plugin_shell::{open, ShellExt};
use tokio::sync::RwLock;
pub struct AdsState {
pub shown: bool,
pub size: Option<LogicalSize<f32>>,
pub position: Option<LogicalPosition<f32>>,
pub last_click: Option<Instant>,
pub malicious_origins: HashSet<String>,
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
@@ -16,6 +23,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
shown: true,
size: None,
position: None,
last_click: None,
malicious_origins: HashSet::new(),
}));
// We refresh the ads window every 5 minutes for performance
@@ -43,6 +52,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
hide_ads_window,
scroll_ads_window,
show_ads_window,
record_ads_click,
open_link,
])
.build()
}
@@ -75,7 +86,7 @@ pub async fn init_ads_window<R: Runtime>(
let _ = webview.set_size(LogicalSize::new(width, height));
}
} else if let Some(window) = app.get_window("main") {
let _ = window.add_child(
let window = window.add_child(
tauri::webview::WebviewBuilder::new(
"ads-window",
WebviewUrl::External(
@@ -93,6 +104,12 @@ pub async fn init_ads_window<R: Runtime>(
},
LogicalSize::new(width, height),
);
if let Ok(window) = window {
window.listen_any("click", |event| {
println!("click: {:?}", event);
});
}
}
Ok(())
@@ -159,3 +176,43 @@ pub async fn scroll_ads_window<R: Runtime>(
Ok(())
}
#[tauri::command]
pub async fn record_ads_click<R: Runtime>(
app: tauri::AppHandle<R>,
) -> crate::api::Result<()> {
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
state.last_click = Some(Instant::now());
Ok(())
}
#[tauri::command]
pub async fn open_link<R: Runtime>(
app: tauri::AppHandle<R>,
path: String,
origin: String,
) -> crate::api::Result<()> {
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
if url::Url::parse(&path).is_ok()
&& !state.malicious_origins.contains(&origin)
{
if let Some(last_click) = state.last_click {
if last_click.elapsed() < Duration::from_millis(100) {
let _ = app.shell().open(&path, None);
state.last_click = None;
return Ok(());
}
}
}
tracing::info!("Malicious click: {path} origin {origin}");
state.malicious_origins.insert(origin);
Ok(())
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised">
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
<p class="m-0 text-2xl font-bold text-contrast">90% of ad revenue goes to creators</p>
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
<nuxt-link to="/plus" class="mt-auto items-center gap-1 text-purple hover:underline">
<span>
Support creators and Modrinth ad-free with

View File

@@ -7,7 +7,7 @@
You have
<strong>{{ $formatMoney(userBalance.available) }}</strong>
available to withdraw. <strong>{{ $formatMoney(userBalance.pending) }}</strong> of your
balance is <a class="text-link" href="#">pending</a>.
balance is <nuxt-link class="text-link" to="/legal/cmp-info#pending">pending</nuxt-link>.
</p>
</div>
<p v-else>
@@ -15,7 +15,7 @@
<strong>{{ $formatMoney(userBalance.available) }}</strong
>, which is under the minimum of ${{ minWithdraw }} to withdraw.
<strong>{{ $formatMoney(userBalance.pending) }}</strong> of your balance is
<a class="text-link" href="#">pending</a>.
<nuxt-link class="text-link" to="/legal/cmp-info#pending">pending</nuxt-link>.
</p>
<div class="input-group mt-4">
<nuxt-link

View File

@@ -1,7 +1,7 @@
<template>
<div class="markdown-body">
<h1>Rewards Program Information</h1>
<p><em>Last modified: May 13, 2024</em></p>
<p><em>Last modified: Sep 12, 2024</em></p>
<p>
This page was created for transparency for how the rewards program works on Modrinth. Feel
free to join our Discord or email
@@ -15,10 +15,11 @@
<h2>Rewards Distribution</h2>
<p>
We collect ad revenue on our website and app through our ad network
<a href="https://adrinth.com">Adrinth</a>. We then distribute this ad revenue to creators.
<a href="https://adrinth.com">Adrinth</a>, which is powered by
<a href="https://aditude.io">Aditude</a>. We then distribute this ad revenue to creators.
</p>
<p>
The advertising revenue of the entire website and app is split 90% to creators and 10% to
The advertising revenue of the entire website and app is split 75% to creators and 25% to
Modrinth.
</p>
<p>
@@ -42,10 +43,10 @@
</ul>
<p>In this scenario, the earnings for each creator and Modrinth would be as follows:</p>
<ul>
<li>Modrinth: $10 (10% of $100, the site's earnings for the day)</li>
<li>User A: $58.69 ($90 * (10 + 30 + 100 + 10)/230)</li>
<li>User B: $12.52 (0.4 * $90 * (50 + 20 + 10 + 0)/230)</li>
<li>User C: $18.78 (0.6 * $90 * (50 + 20 + 10 + 0)/230)</li>
<li>Modrinth: $25 (25% of $100, the site's earnings for the day)</li>
<li>User A: $48.91 ($75 * (10 + 30 + 100 + 10)/230)</li>
<li>User B: $10.43 (0.4 * $75 * (50 + 20 + 10 + 0)/230)</li>
<li>User C: $15.65 (0.6 * $75 * (50 + 20 + 10 + 0)/230)</li>
<li>Note: 230 is the sum of all page views and in-app downloads from above</li>
</ul>
<p>
@@ -72,19 +73,62 @@
</p>
<h3>What methods can I use withdraw money from my account? Are there any fees?</h3>
<p>
Right now, you can use PayPal or Venmo to withdraw money from your Modrinth account. We are
working on more methods to withdraw money from your account. There are fees to withdraw money
from your Modrinth account—see the revenue page in your dashboard for more information.
Right now, you can use PayPal or Venmo to withdraw money from your Modrinth account. Gift card
withdrawal is also available. We are working on more methods to withdraw money from your
account. There are fees to withdraw money from your Modrinth accountsee the revenue page in
your dashboard for more information.
</p>
<h3>Modrinth used to give 100% of project page revenue to creators. What changed?</h3>
<h3 id="pending">What does "pending" revenue mean in my dashboard?</h3>
<p>
While this is true, our new system (as of 08/05/23) gives more of the site's revenue to
creators, so creators will earn more. In the old system, we would earn revenue through
advertisements in search and user profile pages. This amounted on average each month to about
15-20% of the site's total advertising revenue (so a 80-85% split to creators). The new system
gives creators more revenue and a more favorable split towards creators (90%).
Modrinth receives ad revenue from our ad providers on a NET 60 day basis. Due to this, not all
revenue is not immediately available to withdraw. We pay creators as soon as we receive the
money from our ad providers, which is 60 days after the last day of each month. This table
outlines some example dates of how NET 60 payments are made:
</p>
<table>
<thead>
<tr>
<th>Date</th>
<th>Payment available date</th>
</tr>
</thead>
<tbody>
<tr>
<td>January 1st</td>
<td>March 31st</td>
</tr>
<tr>
<td>January 15th</td>
<td>March 31st</td>
</tr>
<tr>
<td>March 3rd</td>
<td>May 30th</td>
</tr>
<tr>
<td>June 30th</td>
<td>August 29th</td>
</tr>
<tr>
<td>July 14th</td>
<td>September 29th</td>
</tr>
<tr>
<td>October 12th</td>
<td>December 30th</td>
</tr>
</tbody>
</table>
<h3>How do I know Modrinth is being transparent about revenue?</h3>
<p>
We aim to be as transparent as possible with creator revenue. All of our code is open source,
including our
<a href="https://github.com/modrinth/labrinth/blob/master/src/queue/payouts.rs#L561">
revenue distribution system </a
>. We also have an
<a href="https://api.modrinth.com/v3/payout/platform_revenue">API route</a> that allows users
to query exact daily revenue for the site.
</p>
<h3></h3>
</div>
</template>

View File

@@ -37,7 +37,7 @@
border-radius: 1rem;
position: absolute;
left: 0;
bottom: 0;
top: 0;
z-index: 2;
}
</style>
@@ -58,19 +58,18 @@
]);
});
let lastUrl = null
window.addEventListener(
"message",
(event) => {
if (event.data.modrinthOpenUrl && window.__TAURI_INTERNALS__ && lastUrl !== event.data.modrinthOpenUrl) {
lastUrl = event.data.modrinthOpenUrl
// window.__TAURI_INTERNALS__.invoke("plugin:shell|open", {
// path: event.data.modrinthOpenUrl,
// });
if (event.data.modrinthAdClick && window.__TAURI_INTERNALS__) {
window.__TAURI_INTERNALS__.invoke("plugin:ads|record_ads_click", {});
}
setTimeout(() => {
lastUrl = null
}, 500)
if (event.data.modrinthOpenUrl && window.__TAURI_INTERNALS__) {
window.__TAURI_INTERNALS__.invoke("plugin:ads|open_link", {
path: event.data.modrinthOpenUrl,
origin: event.origin,
});
}
},
false,

View File

@@ -1,121 +0,0 @@
<template>
<div class="GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD">
<a href="https://modrinth.com/plus" rel="noopener nofollow sponsored" :target="target"></a>
<div class="GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-0">
<div class="GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-1">
<div class="GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-2">
<ModrinthPlusIcon class="GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-3" />
<span>
<span>
90% of ad revenue goes to creators. Go ad-free while supporting creators with
</span>
<strong>Modrinth Plus.</strong>
<span> Subscribe today!</span>
</span>
</div>
</div>
<div class="GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-4">
<a rel="noopener sponsored" :target="target" href="https://adrinth.com"> Ad via Adrinth </a>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ModrinthPlusIcon } from '@modrinth/assets'
const props = withDefaults(
defineProps<{
external: boolean
}>(),
{
external: true,
},
)
const target = computed(() => (props.external ? '_blank' : '_self'))
</script>
<style lang="scss" scoped>
.GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD {
position: relative;
margin-bottom: var(--gap-md);
background: var(--color-ad);
border: 1px solid var(--color-ad-raised);
border-radius: var(--radius-lg);
container-type: inline-size;
width: 100%;
> a {
position: absolute;
inset: 0;
}
&:has(> a:first-child:active) {
scale: 0.99;
}
}
.GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-0 {
font-size: 14px;
line-height: 1.3em;
}
.GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-1 {
color: var(--color-base);
padding: 1em;
text-align: left;
}
.GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-2 {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--color-base);
margin-right: 7.5rem;
&:hover {
text-decoration: none;
}
}
.GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-2 b,
.GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-2 strong {
color: var(--color-ad-highlight);
}
.GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-3 {
padding-top: 1px;
height: 1.5rem;
width: auto;
flex-shrink: 0;
}
.GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-4 a {
position: absolute;
bottom: -1px;
right: -1px;
text-align: center;
font-weight: 600;
text-transform: uppercase;
font-size: 0.8em;
color: var(--color-ad-contrast);
background: var(--color-ad-raised);
letter-spacing: 0.1ch;
margin: 0;
padding: 2px 10px;
border-top-left-radius: var(--radius-lg);
border-bottom-right-radius: var(--radius-lg);
display: flex;
align-items: center;
gap: 0.5rem;
&:hover {
text-decoration: none;
}
}
@media screen and (max-width: 800px) {
.GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-2 {
margin-bottom: 0.5rem;
}
.GBBNWLJVGRHFLYVGSZKSSKNTHFYXHMBD-2 a {
align-items: flex-start;
flex-direction: column;
}
}
</style>

View File

@@ -22,7 +22,6 @@ export { default as Page } from './base/Page.vue'
export { default as Pagination } from './base/Pagination.vue'
export { default as PopoutMenu } from './base/PopoutMenu.vue'
export { default as ProjectCard } from './base/ProjectCard.vue'
export { default as Promotion } from './base/Promotion.vue'
export { default as ScrollablePanel } from './base/ScrollablePanel.vue'
export { default as Slider } from './base/Slider.vue'
export { default as StatItem } from './base/StatItem.vue'