Add Google integrations and documentation
This commit is contained in:
11
README.md
11
README.md
@@ -2,13 +2,15 @@
|
|||||||
|
|
||||||
A curated collection of practical OpenClaw skills by Stefano.
|
A curated collection of practical OpenClaw skills by Stefano.
|
||||||
|
|
||||||
This repository is intended to be a simple skill source: install the repo (or a single skill path), then use each skill through OpenClaw.
|
This repository contains practical OpenClaw skills and companion integrations. Install the repo (or a single path), then use each skill through OpenClaw and each integration as a local helper CLI.
|
||||||
|
|
||||||
## Where to get information
|
## Where to get information
|
||||||
|
|
||||||
- Skill docs index: [`docs/README.md`](docs/README.md)
|
- Skill docs index: [`docs/README.md`](docs/README.md)
|
||||||
- Skill implementation files: `skills/<skill-name>/`
|
- Skill implementation files: `skills/<skill-name>/`
|
||||||
- Per-skill runtime instructions: `skills/<skill-name>/SKILL.md`
|
- Per-skill runtime instructions: `skills/<skill-name>/SKILL.md`
|
||||||
|
- Integration implementation files: `integrations/<integration-name>/`
|
||||||
|
- Integration docs: `docs/*.md`
|
||||||
|
|
||||||
## Skills
|
## Skills
|
||||||
|
|
||||||
@@ -20,6 +22,13 @@ This repository is intended to be a simple skill source: install the repo (or a
|
|||||||
| `searxng` | Search through a local or self-hosted SearXNG instance for web, news, images, and more. | `skills/searxng` |
|
| `searxng` | Search through a local or self-hosted SearXNG instance for web, news, images, and more. | `skills/searxng` |
|
||||||
| `web-automation` | Automate browsing/scraping with Playwright + Camoufox (auth flows, extraction, bot-protected sites). | `skills/web-automation` |
|
| `web-automation` | Automate browsing/scraping with Playwright + Camoufox (auth flows, extraction, bot-protected sites). | `skills/web-automation` |
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
| Integration | What it does | Path |
|
||||||
|
|---|---|---|
|
||||||
|
| `google-maps` | Traffic-aware ETA and leave-by calculations using Google Maps APIs. | `integrations/google-maps` |
|
||||||
|
| `google-workspace` | Gmail and Google Calendar helper CLI for profile, mail, calendar search, and event creation. | `integrations/google-workspace` |
|
||||||
|
|
||||||
## Install ideas
|
## Install ideas
|
||||||
|
|
||||||
- Install the whole repo as a skill source.
|
- Install the whole repo as a skill source.
|
||||||
|
|||||||
@@ -9,3 +9,9 @@ This folder contains detailed docs for each skill in this repository.
|
|||||||
- [`portainer`](portainer.md) — Portainer stack management (list, lifecycle, updates, image pruning)
|
- [`portainer`](portainer.md) — Portainer stack management (list, lifecycle, updates, image pruning)
|
||||||
- [`searxng`](searxng.md) — Privacy-respecting metasearch via a local or self-hosted SearXNG instance
|
- [`searxng`](searxng.md) — Privacy-respecting metasearch via a local or self-hosted SearXNG instance
|
||||||
- [`web-automation`](web-automation.md) — Playwright + Camoufox browser automation and scraping
|
- [`web-automation`](web-automation.md) — Playwright + Camoufox browser automation and scraping
|
||||||
|
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
- [`google-maps`](google-maps.md) — Traffic-aware ETA and leave-by calculations via Google Maps APIs
|
||||||
|
- [`google-workspace`](google-workspace.md) — Gmail and Google Calendar helper CLI
|
||||||
|
|||||||
33
docs/google-maps.md
Normal file
33
docs/google-maps.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# google-maps integration
|
||||||
|
|
||||||
|
Google Maps traffic/ETA helper CLI using Geocoding API and Routes API.
|
||||||
|
|
||||||
|
## What this integration is for
|
||||||
|
|
||||||
|
- Drive ETA between two places
|
||||||
|
- Leave-by time estimation for a target arrival
|
||||||
|
- Quick traffic-aware route summaries
|
||||||
|
|
||||||
|
## Runtime
|
||||||
|
|
||||||
|
- `node`
|
||||||
|
- no package dependencies beyond built-in runtime APIs on current Node
|
||||||
|
|
||||||
|
## Auth
|
||||||
|
|
||||||
|
Preferred:
|
||||||
|
- `GOOGLE_MAPS_API_KEY` environment variable
|
||||||
|
|
||||||
|
Fallback key file:
|
||||||
|
- `~/.openclaw/credentials/google-maps/apikey.txt`
|
||||||
|
|
||||||
|
Required Google APIs for the key:
|
||||||
|
- Geocoding API
|
||||||
|
- Routes API
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node integrations/google-maps/traffic.js eta --from "DFW Airport" --to "Love Field" --departAt now
|
||||||
|
node integrations/google-maps/traffic.js leave-by --from "Home" --to "DFW Airport" --arriveBy 2026-03-17T08:30:00-05:00
|
||||||
|
```
|
||||||
47
docs/google-workspace.md
Normal file
47
docs/google-workspace.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# google-workspace integration
|
||||||
|
|
||||||
|
Google Workspace helper CLI for Gmail and Google Calendar.
|
||||||
|
|
||||||
|
## What this integration is for
|
||||||
|
|
||||||
|
- Show mailbox identity/profile
|
||||||
|
- Send email
|
||||||
|
- Search mail
|
||||||
|
- Search calendar events
|
||||||
|
- Create calendar events
|
||||||
|
|
||||||
|
## Runtime
|
||||||
|
|
||||||
|
- `node`
|
||||||
|
- dependency: `googleapis`
|
||||||
|
|
||||||
|
Install inside the integration folder if needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd integrations/google-workspace
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auth and defaults
|
||||||
|
|
||||||
|
Default impersonation:
|
||||||
|
- `stefano@fiorinis.com`
|
||||||
|
|
||||||
|
Key lookup order:
|
||||||
|
1. `GW_KEY_PATH`
|
||||||
|
2. `~/.openclaw/workspace/.clawdbot/credentials/google-workspace/service-account.json`
|
||||||
|
3. `~/.clawdbot/credentials/google-workspace/service-account.json`
|
||||||
|
|
||||||
|
Optional env:
|
||||||
|
- `GW_IMPERSONATE`
|
||||||
|
- `GW_KEY_PATH`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node integrations/google-workspace/gw.js whoami
|
||||||
|
node integrations/google-workspace/gw.js send --to "user@example.com" --subject "Hello" --body "Hi there"
|
||||||
|
node integrations/google-workspace/gw.js search-mail --query "from:someone@example.com newer_than:7d" --max 10
|
||||||
|
node integrations/google-workspace/gw.js search-calendar --timeMin 2026-03-17T00:00:00-05:00 --timeMax 2026-03-18T00:00:00-05:00 --max 20
|
||||||
|
node integrations/google-workspace/gw.js create-event --summary "Meeting" --start 2026-03-20T09:00:00-05:00 --end 2026-03-20T10:00:00-05:00
|
||||||
|
```
|
||||||
188
integrations/google-maps/traffic.js
Executable file
188
integrations/google-maps/traffic.js
Executable file
@@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const DEFAULT_KEY_PATH = path.join(process.env.HOME || '', '.openclaw/credentials/google-maps/apikey.txt');
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const out = { _: [] };
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const t = argv[i];
|
||||||
|
if (t.startsWith('--')) {
|
||||||
|
const k = t.slice(2);
|
||||||
|
const n = argv[i + 1];
|
||||||
|
if (!n || n.startsWith('--')) out[k] = true;
|
||||||
|
else {
|
||||||
|
out[k] = n;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else out._.push(t);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
console.log(`Google Maps Traffic CLI
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
eta --from "Origin" --to "Destination" [--departAt now|ISO]
|
||||||
|
leave-by --from "Origin" --to "Destination" --arriveBy ISO
|
||||||
|
|
||||||
|
Optional flags:
|
||||||
|
--keyPath <path> API key file path (default: ${DEFAULT_KEY_PATH})
|
||||||
|
--timeZone <IANA> Display timezone (default: America/Chicago)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Requires Google Maps APIs (Routes API + Geocoding API) enabled for the key.
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function must(opts, keys) {
|
||||||
|
const miss = keys.filter((k) => !opts[k]);
|
||||||
|
if (miss.length) throw new Error(`Missing: ${miss.map((k) => '--' + k).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readApiKey(opts) {
|
||||||
|
if (process.env.GOOGLE_MAPS_API_KEY) return process.env.GOOGLE_MAPS_API_KEY.trim();
|
||||||
|
const p = opts.keyPath || DEFAULT_KEY_PATH;
|
||||||
|
if (!fs.existsSync(p)) throw new Error(`API key file not found: ${p}`);
|
||||||
|
const key = fs.readFileSync(p, 'utf8').trim();
|
||||||
|
if (!key) throw new Error(`API key file is empty: ${p}`);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function geocode(address, key) {
|
||||||
|
const u = new URL('https://maps.googleapis.com/maps/api/geocode/json');
|
||||||
|
u.searchParams.set('address', address);
|
||||||
|
u.searchParams.set('key', key);
|
||||||
|
const r = await fetch(u);
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.status !== 'OK' || !j.results?.length) {
|
||||||
|
throw new Error(`Geocoding failed for "${address}": ${j.status}${j.error_message ? ` (${j.error_message})` : ''}`);
|
||||||
|
}
|
||||||
|
const loc = j.results[0].geometry.location;
|
||||||
|
return { lat: loc.lat, lng: loc.lng, formatted: j.results[0].formatted_address };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGoogleDuration(s) {
|
||||||
|
// e.g. "1534s"
|
||||||
|
const m = /^(-?\d+)s$/.exec(String(s || ''));
|
||||||
|
if (!m) return null;
|
||||||
|
return Number(m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtMinutes(sec) {
|
||||||
|
return `${Math.round(sec / 60)} min`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function computeRoute({ from, to, departAt, key }) {
|
||||||
|
const [o, d] = await Promise.all([geocode(from, key), geocode(to, key)]);
|
||||||
|
const body = {
|
||||||
|
origin: { location: { latLng: { latitude: o.lat, longitude: o.lng } } },
|
||||||
|
destination: { location: { latLng: { latitude: d.lat, longitude: d.lng } } },
|
||||||
|
travelMode: 'DRIVE',
|
||||||
|
routingPreference: 'TRAFFIC_AWARE',
|
||||||
|
computeAlternativeRoutes: false,
|
||||||
|
languageCode: 'en-US',
|
||||||
|
units: 'IMPERIAL',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (departAt && departAt !== 'now') body.departureTime = new Date(departAt).toISOString();
|
||||||
|
|
||||||
|
const res = await fetch('https://routes.googleapis.com/directions/v2:computeRoutes', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Goog-Api-Key': key,
|
||||||
|
'X-Goog-FieldMask': 'routes.duration,routes.distanceMeters,routes.staticDuration',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text();
|
||||||
|
throw new Error(`Routes API error ${res.status}: ${txt}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.routes?.length) throw new Error('No route found');
|
||||||
|
|
||||||
|
const r = data.routes[0];
|
||||||
|
const durationSec = parseGoogleDuration(r.duration);
|
||||||
|
const staticSec = parseGoogleDuration(r.staticDuration);
|
||||||
|
return {
|
||||||
|
origin: o.formatted,
|
||||||
|
destination: d.formatted,
|
||||||
|
durationSec,
|
||||||
|
staticSec,
|
||||||
|
distanceMiles: r.distanceMeters ? r.distanceMeters / 1609.344 : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function localIso(ts, tz = 'America/Chicago') {
|
||||||
|
return new Date(ts).toLocaleString('en-US', { timeZone: tz, hour12: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdEta(opts) {
|
||||||
|
must(opts, ['from', 'to']);
|
||||||
|
const key = readApiKey(opts);
|
||||||
|
const tz = opts.timeZone || 'America/Chicago';
|
||||||
|
const departTs = opts.departAt && opts.departAt !== 'now' ? new Date(opts.departAt).getTime() : Date.now();
|
||||||
|
|
||||||
|
const route = await computeRoute({ from: opts.from, to: opts.to, departAt: opts.departAt || 'now', key });
|
||||||
|
const arriveTs = departTs + (route.durationSec || 0) * 1000;
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
from: route.origin,
|
||||||
|
to: route.destination,
|
||||||
|
departureLocal: localIso(departTs, tz),
|
||||||
|
arrivalLocal: localIso(arriveTs, tz),
|
||||||
|
eta: fmtMinutes(route.durationSec),
|
||||||
|
trafficDelay: route.staticSec && route.durationSec ? fmtMinutes(Math.max(0, route.durationSec - route.staticSec)) : null,
|
||||||
|
distanceMiles: route.distanceMiles ? Number(route.distanceMiles.toFixed(1)) : null,
|
||||||
|
timeZone: tz,
|
||||||
|
}, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdLeaveBy(opts) {
|
||||||
|
must(opts, ['from', 'to', 'arriveBy']);
|
||||||
|
const key = readApiKey(opts);
|
||||||
|
const tz = opts.timeZone || 'America/Chicago';
|
||||||
|
const arriveTs = new Date(opts.arriveBy).getTime();
|
||||||
|
if (!Number.isFinite(arriveTs)) throw new Error('Invalid --arriveBy ISO datetime');
|
||||||
|
|
||||||
|
// two-pass estimate
|
||||||
|
let departGuess = arriveTs - 45 * 60 * 1000;
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const route = await computeRoute({ from: opts.from, to: opts.to, departAt: new Date(departGuess).toISOString(), key });
|
||||||
|
departGuess = arriveTs - (route.durationSec || 0) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalRoute = await computeRoute({ from: opts.from, to: opts.to, departAt: new Date(departGuess).toISOString(), key });
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
from: finalRoute.origin,
|
||||||
|
to: finalRoute.destination,
|
||||||
|
leaveByLocal: localIso(departGuess, tz),
|
||||||
|
targetArrivalLocal: localIso(arriveTs, tz),
|
||||||
|
eta: fmtMinutes(finalRoute.durationSec),
|
||||||
|
trafficDelay: finalRoute.staticSec && finalRoute.durationSec ? fmtMinutes(Math.max(0, finalRoute.durationSec - finalRoute.staticSec)) : null,
|
||||||
|
distanceMiles: finalRoute.distanceMiles ? Number(finalRoute.distanceMiles.toFixed(1)) : null,
|
||||||
|
timeZone: tz,
|
||||||
|
}, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function main() {
|
||||||
|
try {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
const cmd = args._[0];
|
||||||
|
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') return usage();
|
||||||
|
|
||||||
|
if (cmd === 'eta') return await cmdEta(args);
|
||||||
|
if (cmd === 'leave-by') return await cmdLeaveBy(args);
|
||||||
|
throw new Error(`Unknown command: ${cmd}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`ERROR: ${e.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
248
integrations/google-workspace/gw.js
Executable file
248
integrations/google-workspace/gw.js
Executable file
@@ -0,0 +1,248 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/*
|
||||||
|
Google Workspace helper CLI
|
||||||
|
Commands:
|
||||||
|
whoami
|
||||||
|
send --to <email> --subject <text> --body <text> [--html]
|
||||||
|
search-mail --query <gmail query> [--max 10]
|
||||||
|
search-calendar --query <text> [--max 10] [--timeMin ISO] [--timeMax ISO] [--calendar primary]
|
||||||
|
create-event --summary <text> --start <ISO> --end <ISO> [--timeZone America/Chicago] [--description <text>] [--location <text>] [--calendar primary]
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { google } = require('googleapis');
|
||||||
|
|
||||||
|
const DEFAULT_SUBJECT = process.env.GW_IMPERSONATE || 'stefano@fiorinis.com';
|
||||||
|
const DEFAULT_KEY_CANDIDATES = [
|
||||||
|
process.env.GW_KEY_PATH,
|
||||||
|
path.join(process.env.HOME || '', '.openclaw/workspace/.clawdbot/credentials/google-workspace/service-account.json'),
|
||||||
|
path.join(process.env.HOME || '', '.clawdbot/credentials/google-workspace/service-account.json'),
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const SCOPES = [
|
||||||
|
'https://www.googleapis.com/auth/gmail.send',
|
||||||
|
'https://www.googleapis.com/auth/gmail.compose',
|
||||||
|
'https://www.googleapis.com/auth/gmail.readonly',
|
||||||
|
'https://www.googleapis.com/auth/calendar',
|
||||||
|
'https://www.googleapis.com/auth/calendar.events',
|
||||||
|
];
|
||||||
|
|
||||||
|
function resolveKeyPath() {
|
||||||
|
for (const p of DEFAULT_KEY_CANDIDATES) {
|
||||||
|
if (p && fs.existsSync(p)) return p;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const out = { _: [] };
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const token = argv[i];
|
||||||
|
if (token.startsWith('--')) {
|
||||||
|
const key = token.slice(2);
|
||||||
|
const next = argv[i + 1];
|
||||||
|
if (!next || next.startsWith('--')) {
|
||||||
|
out[key] = true;
|
||||||
|
} else {
|
||||||
|
out[key] = next;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out._.push(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
console.log(`Google Workspace CLI\n
|
||||||
|
Env (optional):
|
||||||
|
GW_IMPERSONATE user to impersonate (default: ${DEFAULT_SUBJECT})
|
||||||
|
GW_KEY_PATH service-account key path
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
whoami
|
||||||
|
send --to <email> --subject <text> --body <text> [--html]
|
||||||
|
search-mail --query <gmail query> [--max 10]
|
||||||
|
search-calendar --query <text> [--max 10] [--timeMin ISO] [--timeMax ISO] [--calendar primary]
|
||||||
|
create-event --summary <text> --start <ISO> --end <ISO> [--timeZone America/Chicago] [--description <text>] [--location <text>] [--calendar primary]
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertRequired(opts, required) {
|
||||||
|
const missing = required.filter((k) => !opts[k]);
|
||||||
|
if (missing.length) {
|
||||||
|
throw new Error(`Missing required options: ${missing.map((m) => `--${m}`).join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRawEmail({ from, to, subject, body, isHtml = false }) {
|
||||||
|
const contentType = isHtml ? 'text/html; charset="UTF-8"' : 'text/plain; charset="UTF-8"';
|
||||||
|
const msg = [
|
||||||
|
`From: ${from}`,
|
||||||
|
`To: ${to}`,
|
||||||
|
`Subject: ${subject}`,
|
||||||
|
'MIME-Version: 1.0',
|
||||||
|
`Content-Type: ${contentType}`,
|
||||||
|
'',
|
||||||
|
body,
|
||||||
|
].join('\r\n');
|
||||||
|
|
||||||
|
return Buffer.from(msg)
|
||||||
|
.toString('base64')
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_')
|
||||||
|
.replace(/=+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getClients() {
|
||||||
|
const keyPath = resolveKeyPath();
|
||||||
|
if (!keyPath) {
|
||||||
|
throw new Error('Service account key not found. Set GW_KEY_PATH or place the file in ~/.openclaw/workspace/.clawdbot/credentials/google-workspace/service-account.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = new google.auth.GoogleAuth({
|
||||||
|
keyFile: keyPath,
|
||||||
|
scopes: SCOPES,
|
||||||
|
clientOptions: { subject: DEFAULT_SUBJECT },
|
||||||
|
});
|
||||||
|
|
||||||
|
const authClient = await auth.getClient();
|
||||||
|
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
||||||
|
const calendar = google.calendar({ version: 'v3', auth: authClient });
|
||||||
|
|
||||||
|
return { gmail, calendar, keyPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdWhoami(clients) {
|
||||||
|
const profile = await clients.gmail.users.getProfile({ userId: 'me' });
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
impersonating: DEFAULT_SUBJECT,
|
||||||
|
keyPath: clients.keyPath,
|
||||||
|
profile: profile.data,
|
||||||
|
}, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdSend(clients, opts) {
|
||||||
|
assertRequired(opts, ['to', 'subject', 'body']);
|
||||||
|
const raw = makeRawEmail({
|
||||||
|
from: DEFAULT_SUBJECT,
|
||||||
|
to: opts.to,
|
||||||
|
subject: opts.subject,
|
||||||
|
body: opts.body,
|
||||||
|
isHtml: !!opts.html,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await clients.gmail.users.messages.send({
|
||||||
|
userId: 'me',
|
||||||
|
requestBody: { raw },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(JSON.stringify({ ok: true, id: res.data.id, threadId: res.data.threadId }, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdSearchMail(clients, opts) {
|
||||||
|
assertRequired(opts, ['query']);
|
||||||
|
const maxResults = Math.max(1, Math.min(50, Number(opts.max || 10)));
|
||||||
|
|
||||||
|
const list = await clients.gmail.users.messages.list({
|
||||||
|
userId: 'me',
|
||||||
|
q: opts.query,
|
||||||
|
maxResults,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ids = (list.data.messages || []).map((m) => m.id).filter(Boolean);
|
||||||
|
const out = [];
|
||||||
|
for (const id of ids) {
|
||||||
|
const msg = await clients.gmail.users.messages.get({
|
||||||
|
userId: 'me',
|
||||||
|
id,
|
||||||
|
format: 'metadata',
|
||||||
|
metadataHeaders: ['From', 'To', 'Subject', 'Date'],
|
||||||
|
});
|
||||||
|
const headers = Object.fromEntries((msg.data.payload?.headers || []).map((h) => [h.name, h.value]));
|
||||||
|
out.push({
|
||||||
|
id,
|
||||||
|
threadId: msg.data.threadId,
|
||||||
|
snippet: msg.data.snippet,
|
||||||
|
from: headers.From,
|
||||||
|
to: headers.To,
|
||||||
|
subject: headers.Subject,
|
||||||
|
date: headers.Date,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify({ count: out.length, messages: out }, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdSearchCalendar(clients, opts) {
|
||||||
|
const maxResults = Math.max(1, Math.min(50, Number(opts.max || 10)));
|
||||||
|
|
||||||
|
const res = await clients.calendar.events.list({
|
||||||
|
calendarId: opts.calendar || 'primary',
|
||||||
|
q: opts.query || undefined,
|
||||||
|
timeMin: opts.timeMin,
|
||||||
|
timeMax: opts.timeMax,
|
||||||
|
singleEvents: true,
|
||||||
|
orderBy: 'startTime',
|
||||||
|
maxResults,
|
||||||
|
});
|
||||||
|
|
||||||
|
const events = (res.data.items || []).map((e) => ({
|
||||||
|
id: e.id,
|
||||||
|
summary: e.summary,
|
||||||
|
status: e.status,
|
||||||
|
start: e.start,
|
||||||
|
end: e.end,
|
||||||
|
location: e.location,
|
||||||
|
hangoutLink: e.hangoutLink,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(JSON.stringify({ count: events.length, events }, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdCreateEvent(clients, opts) {
|
||||||
|
assertRequired(opts, ['summary', 'start', 'end']);
|
||||||
|
const tz = opts.timeZone || 'America/Chicago';
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
summary: opts.summary,
|
||||||
|
description: opts.description,
|
||||||
|
location: opts.location,
|
||||||
|
start: { dateTime: opts.start, timeZone: tz },
|
||||||
|
end: { dateTime: opts.end, timeZone: tz },
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await clients.calendar.events.insert({
|
||||||
|
calendarId: opts.calendar || 'primary',
|
||||||
|
requestBody: event,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(JSON.stringify({ ok: true, id: res.data.id, htmlLink: res.data.htmlLink }, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function main() {
|
||||||
|
try {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
const cmd = args._[0];
|
||||||
|
|
||||||
|
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
||||||
|
usage();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clients = await getClients();
|
||||||
|
|
||||||
|
if (cmd === 'whoami') return await cmdWhoami(clients);
|
||||||
|
if (cmd === 'send') return await cmdSend(clients, args);
|
||||||
|
if (cmd === 'search-mail') return await cmdSearchMail(clients, args);
|
||||||
|
if (cmd === 'search-calendar') return await cmdSearchCalendar(clients, args);
|
||||||
|
if (cmd === 'create-event') return await cmdCreateEvent(clients, args);
|
||||||
|
|
||||||
|
throw new Error(`Unknown command: ${cmd}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`ERROR: ${err.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
1093
integrations/google-workspace/package-lock.json
generated
Normal file
1093
integrations/google-workspace/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
integrations/google-workspace/package.json
Normal file
16
integrations/google-workspace/package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "google-workspace",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "gw.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"googleapis": "^171.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user