Initial commit (by create-cloudflare CLI)

Details:
  C3 = create-cloudflare@2.50.2
  project name = gdrive-upload-robot
  package manager = npm@11.2.0
  wrangler = wrangler@4.22.0
  git = 2.45.2.windows.1
This commit is contained in:
xixu-me committed 2025-07-03 22:00:11 +08:00
commit b712232b5e
8 files changed
+3425

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
# http://editorconfig.org
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_style = space
+172
View File
@@ -0,0 +1,172 @@
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
# wrangler project
.dev.vars
.wrangler/
+6
View File
@@ -0,0 +1,6 @@
{
"printWidth": 140,
"semi": true,
"singleQuote": true,
"useTabs": true
}
+3032
View File
File diff suppressed because it is too large. Load diff
+16
View File
@@ -0,0 +1,16 @@
{
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.8.19",
"vitest": "~3.2.0",
"wrangler": "^4.23.0"
},
"name": "gdrive-upload-robot",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"test": "vitest"
},
"version": "0.0.0"
}
+133
View File
@@ -0,0 +1,133 @@
export default {
async fetch(request, env) {
if (request.method === 'POST') {
const payload = await request.json();
if (payload.message && payload.message.document) {
await handleDocument(payload.message, env);
} else if (payload.message && payload.message.photo) {
await handlePhoto(payload.message, env);
}
}
return new Response('OK');
},
};
async function handleDocument(message, env) {
const fileId = message.document.file_id;
const fileName = message.document.file_name;
await uploadToDrive(fileId, fileName, env);
}
async function handlePhoto(message, env) {
const photo = message.photo[message.photo.length - 1]; // Get the highest resolution photo
const fileId = photo.file_id;
const fileName = `${fileId}.jpg`;
await uploadToDrive(fileId, fileName, env);
}
async function uploadToDrive(fileId, fileName, env) {
const botToken = env.TELEGRAM_BOT_TOKEN;
const driveFolderId = env.GOOGLE_DRIVE_FOLDER_ID;
const adminChatId = env.ADMIN_CHAT_ID;
const googleCredentials = JSON.parse(env.GOOGLE_CREDENTIALS);
// Get file path from Telegram
const fileDetailsUrl = `https://api.telegram.org/bot${botToken}/getFile?file_id=${fileId}`;
const fileDetailsResponse = await fetch(fileDetailsUrl);
const fileDetails = await fileDetailsResponse.json();
if (!fileDetails.ok) {
await sendMessage(adminChatId, `Error getting file details: ${fileDetails.description}`, botToken);
return;
}
const filePath = fileDetails.result.file_path;
const fileUrl = `https://api.telegram.org/file/bot${botToken}/${filePath}`;
// Download file from Telegram
const fileResponse = await fetch(fileUrl);
const fileData = await fileResponse.arrayBuffer();
// Authenticate with Google Drive
const jwtToken = await getGoogleAuthToken(googleCredentials);
// Upload file to Google Drive
const uploadUrl = 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart';
const metadata = {
name: fileName,
parents: [driveFolderId],
};
const formData = new FormData();
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
formData.append('file', new Blob([fileData]));
const uploadResponse = await fetch(uploadUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${jwtToken}`,
},
body: formData,
});
const uploadResult = await uploadResponse.json();
if (uploadResult.id) {
await sendMessage(adminChatId, `File "${fileName}" uploaded to Google Drive successfully!`, botToken);
} else {
await sendMessage(adminChatId, `Error uploading file: ${JSON.stringify(uploadResult.error)}`, botToken);
}
}
async function getGoogleAuthToken(credentials) {
const header = { alg: 'RS256', typ: 'JWT' };
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: credentials.client_email,
scope: 'https://www.googleapis.com/auth/drive',
aud: 'https://oauth2.googleapis.com/token',
exp: now + 3600,
iat: now,
};
const encodedHeader = btoa(JSON.stringify(header)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const encodedPayload = btoa(JSON.stringify(payload)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const dataToSign = `${encodedHeader}.${encodedPayload}`;
const privateKey = await crypto.subtle.importKey(
'pkcs8',
str2ab(atob(credentials.private_key.replace('-----BEGIN PRIVATE KEY-----\n', '').replace('\n-----END PRIVATE KEY-----\n', ''))),
{ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', privateKey, str2ab(dataToSign));
const encodedSignature = btoa(ab2str(signature)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const jwt = `${dataToSign}.${encodedSignature}`;
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`,
});
const tokenData = await tokenResponse.json();
return tokenData.access_token;
}
async function sendMessage(chatId, text, botToken) {
const url = `https://api.telegram.org/bot${botToken}/sendMessage?chat_id=${chatId}&text=${encodeURIComponent(text)}`;
await fetch(url);
}
function str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
+11
View File
@@ -0,0 +1,11 @@
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.jsonc' },
},
},
},
});
+43
View File
@@ -0,0 +1,43 @@
/**
* For more details on how to configure Wrangler, refer to:
* https://developers.cloudflare.com/workers/wrangler/configuration/
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
"compatibility_date": "2025-07-03",
"main": "src/index.js",
"name": "gdrive-upload-robot",
"observability": {
"enabled": true
}
/**
* Smart Placement
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
*/
// "placement": { "mode": "smart" },
/**
* Bindings
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
* databases, object storage, AI inference, real-time communication and more.
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
*/
/**
* Environment Variables
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
*/
// "vars": { "MY_VARIABLE": "production_value" },
/**
* Note: Use secrets to store sensitive data.
* https://developers.cloudflare.com/workers/configuration/secrets/
*/
/**
* Static Assets
* https://developers.cloudflare.com/workers/static-assets/binding/
*/
// "assets": { "directory": "./public/", "binding": "ASSETS" },
/**
* Service Bindings (communicate between multiple Workers)
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
*/
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
}