addEventListener('fetch', event => {
event.respondWith(processRequest(event.request));
});
async function processRequest(request) {
const requestUrl = new URL(request.url);
if (request.method === 'GET' && requestUrl.pathname === '/') {
return new Response(generateHTMLForm(), {
headers: {
'Content-Type': 'text/html;charset=UTF-8',
...securityHeaders,
},
});
}
if (request.method === 'POST' && requestUrl.pathname === '/send') {
try {
const formData = await request.formData();
// 验证并清理服务器 URL
let apiServerUrl;
try {
apiServerUrl = sanitizeApiServerUrl(formData.get('server') || 'https://api.day.app');
} catch (error) {
return new Response(
`<div class="notification error">无效的服务器 URL,请输入正确的 HTTPS 地址。</div><a href="/">返回</a>`,
{
headers: {
'Content-Type': 'text/html;charset=UTF-8',
...securityHeaders,
},
}
);
}
const apiKey = formData.get('key');
const notificationTitle = formData.get('title') || '';
const notificationBody = formData.get('body') || '';
let apiPath = `/${encodeURIComponent(apiKey)}`;
if (notificationTitle) {
apiPath += `/${encodeURIComponent(notificationTitle)}`;
}
apiPath += `/${encodeURIComponent(notificationBody)}`;
const queryParams = new URLSearchParams();
const shouldArchive = formData.get('isArchive') === '1';
['level', 'sound', 'icon', 'group', 'badge', 'url', 'copy', 'autocopy'].forEach(param => {
const value = formData.get(param);
if (value) queryParams.append(param, value);
});
const completeApiUrl = `${apiServerUrl}${apiPath}?${queryParams.toString()}`;
let apiResponse;
const requestMethod = (formData.get('method') || 'GET').toUpperCase();
// 验证请求方法
if (requestMethod !== 'GET' && requestMethod !== 'POST') {
return new Response('无效的请求方法', { status: 400 });
}
if (requestMethod === 'POST') {
const postData = {
title: notificationTitle,
body: notificationBody,
};
if (shouldArchive) {
postData.isArchive = '1';
} else {
postData.isArchive = '2';
}
['level', 'sound', 'icon', 'group', 'badge', 'url', 'copy', 'autocopy'].forEach(param => {
const value = formData.get(param);
if (value) postData[param] = value;
});
apiResponse = await fetch(`${apiServerUrl}/${encodeURIComponent(apiKey)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData),
});
} else {
if (shouldArchive) {
queryParams.append('isArchive', '1');
} else {
queryParams.append('isArchive', '2');
}
apiResponse = await fetch(completeApiUrl, { method: 'GET' });
}
if (apiResponse.ok) {
return new Response(generateSuccessHTML(), {
headers: {
'Content-Type': 'text/html;charset=UTF-8',
...securityHeaders,
},
});
} else {
// 避免将详细错误信息暴露给用户
return new Response(
`<div class="notification error">推送失败,请稍后重试。</div><a href="/">返回</a>`,
{
headers: {
'Content-Type': 'text/html;charset=UTF-8',
...securityHeaders,
},
}
);
}
} catch (error) {
// 可在此记录错误日志,例如 console.error(error);
return new Response(
`<div class="notification error">发生未知错误,请稍后重试。</div><a href="/">返回</a>`,
{
headers: {
'Content-Type': 'text/html;charset=UTF-8',
...securityHeaders,
},
}
);
}
}
return new Response('Not Found', { status: 404 });
}
function sanitizeApiServerUrl(url) {
try {
const parsedUrl = new URL(url);
if (parsedUrl.protocol !== 'https:') {
throw new Error('只允许使用 HTTPS 协议的服务器地址');
}
return parsedUrl.origin;
} catch (error) {
throw new Error('无效的服务器 URL');
}
}
// 安全头信息
const securityHeaders = {
'Content-Security-Policy':
"default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; font-src https://fonts.googleapis.com https://fonts.gstatic.com;",
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Referrer-Policy': 'no-referrer',
'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
};
function generateHTMLForm() {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bark Sender</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22><text y=%2214%22 font-size=%2214%22>⚡</text></svg>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after {
box-sizing: border-box;
}
:root {
--primary-color: #7C3AED;
--primary-hover-color: #6D28D9;
--background-color: #F3F4F6;
--card-background-color: #FFFFFF;
--text-color: #1F2937;
--border-color: #E5E7EB;
--success-color: #10B981;
--error-color: #EF4444;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.5;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 2rem auto;
padding: 2rem;
background-color: var(--card-background-color);
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
h1 {
font-size: 2.3rem;
font-weight: 600;
text-align: center;
margin-bottom: 1.5rem;
color: var(--primary-color);
}
form {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
label {
font-weight: 500;
display: block;
margin-bottom: 0.5rem;
}
input[type="text"],
input[type="url"],
input[type="number"],
select,
textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 0.25rem;
font-size: 1rem;
transition: border-color 0.3s ease;
}
input[type="text"]:focus,
input[type="url"]:focus,
input[type="number"]:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
}
.checkbox-group {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
}
.checkbox-group input[type="checkbox"] {
width: auto;
}
button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 0.75rem 1rem;
font-size: 1rem;
font-weight: 500;
border-radius: 0.25rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: var(--primary-hover-color);
}
.tooltip {
position: relative;
display: inline-block;
margin-left: 0.5rem;
cursor: help;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 200px;
background-color: #333;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
.notification {
padding: 1rem;
border-radius: 0.25rem;
margin-bottom: 1rem;
text-align: center;
}
.notification.success {
background-color: var(--success-color);
color: white;
}
.notification.error {
background-color: var(--error-color);
color: white;
}
@media (max-width: 640px) {
.container {
margin: 1rem;
padding: 1rem;
}
}
</style>
</head>
<body>
<div class="container">
<h1>Bark Sender</h1>
<form action="/send" method="post">
<div>
<label for="server">
Bark 后端服务器域名*
</label>
<input type="text" id="server" name="server" placeholder="必填项,无需填写到/push端点,默认为官方服务器。" value="https://api.day.app" required>
</div>
<div>
<label for="key">
Key*
</label>
<input type="text" id="key" name="key" placeholder="必填项,请输入 Bark App 在添加服务器后生成的密钥。" required>
</div>
<div>
<label for="body">
通知内容*
</label>
<textarea id="body" name="body" rows="4" placeholder="必填项,请输入需要推送的通知内容。" required></textarea>
</div>
<div>
<label for="title">
通知标题
</label>
<input type="text" id="title" name="title" placeholder="可设置通知的标题。">
</div>
<div>
<label for="copy">
指定复制内容
</label>
<input type="text" id="copy" name="copy" placeholder="可指定点击复制的内容,留空将默认复制通知内容。">
</div>
<div>
<label for="group">
分组
</label>
<input type="text" id="group" name="group" placeholder="可指定推送消息的分组名称,便于在历史记录中分类查看。">
</div>
<div>
<label for="url">
跳转 URL
</label>
<input type="url" id="url" name="url" placeholder="点击通知,可跳转至填写的URL,支持URL Scheme/Universal Link。">
</div>
<div>
<label for="icon">
自定义图标
</label>
<input type="url" id="icon" name="icon" placeholder="可填入图床链接, iOS15+ 系统版本支持。">
</div>
<div>
<label for="sound">
自定义通知铃声
</label>
<input type="text" id="sound" name="sound" list="sound-options" placeholder="可选择预设的通知铃声,亦可前往 Bark App 自行上传。">
<datalist id="sound-options">
<option value="alarm">
<option value="anticipate">
<option value="bell">
<option value="birdsong">
<option value="bloom">
<option value="calypso">
<option value="chime">
<option value="choo">
<option value="descent">
<option value="electronic">
<option value="fanfare">
<option value="glass">
<option value="gotosleep">
<option value="healthnotification">
<option value="horn">
<option value="ladder">
<option value="mailsent">
<option value="minuet">
<option value="multiwayinvitation">
<option value="newmail">
<option value="newsflash">
<option value="noir">
<option value="paymentsuccess">
<option value="shake">
<option value="sherwoodforest">
<option value="silence">
<option value="spell">
<option value="suspense">
<option value="telegraph">
<option value="tiptoes">
<option value="typewriters">
<option value="update">
</datalist>
</div>
<div>
<label for="badge">
自定义消息数目
</label>
<input type="number" id="badge" name="badge" min="1" placeholder="可选,影响单个通知带来的应用角标变动数目,最小值为 1 。">
</div>
<div>
<label for="method">
请求方式
</label>
<select id="method" name="method">
<option value="POST">POST</option>
<option value="GET">GET(请注意 URL 2k字符长度限制)</option>
</select>
</div>
<div>
<label for="level">
通知方式
</label>
<select id="level" name="level">
<option value="active">立即亮屏</option>
<option value="passive">静默通知</option>
<option value="timeSensitive">强提醒</option>
</select>
</div>
<div class="checkbox-group">
<input type="checkbox" id="isArchive" name="isArchive" value="1">
<label for="isArchive">
保存到历史记录
</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="automaticallyCopy" name="automaticallyCopy" value="1">
<label for="automaticallyCopy">
自动复制推送内容
<span class="tooltip">💡
<span class="tooltiptext">启用后,通过长按/下拉通知可快速复制通知/指定内容到剪贴板, iOS14.5- 系统版本支持自动复制。</span>
</span>
</label>
</div>
<button type="submit">确认发送</button>
</form>
</div>
<script>
document.querySelector('form').addEventListener('submit', function(event) {
const serverInput = document.getElementById('server');
const serverUrl = serverInput.value.trim();
if (!/^https?:\\/\\/.+/.test(serverUrl)) {
event.preventDefault();
alert('请输入有效的服务器地址,以 http:// 或 https:// 开头');
serverInput.focus();
}
});
</script>
</body>
</html>
`;
}
function generateSuccessHTML() {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Done - Bark Sender</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22><text y=%2214%22 font-size=%2214%22>✅</text></svg>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--background-color: #F6F9FC;
--text-color: #1A1F36;
--success-green: #00C853;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.5;
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
position: relative;
z-index: 140;
width: 100%;
max-width: 440px;
margin: auto;
background-color: white;
border-radius: 8px;
padding: 24px 20px;
text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.success-icon-container {
width: 100%;
height: 80px;
background-color: var(--success-green);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.success-icon {
width: 48px;
height: 48px;
}
.message {
margin-bottom: 8px;
font-size: 20px;
font-weight: 500;
}
.description {
margin-bottom: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="success-icon-container">
<svg class="success-icon" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="24" fill="white"></circle>
<path d="M32.5 17.5L20.8125 30.5L15.5 25.5" stroke="var(--success-green)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</div>
<div>
<h3 class="message">Done. </h3>
<span>(Returning in 3 seconds...)</span>
</div>
</div>
<script>
setTimeout(function() {
window.location.href = '/';
}, 3000);
</script>
</body>
</html>`;
}