feat: add OpenAI OAuth, strict model overrides, and Gmail pull mode
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
# Gmail Push Revisit — Quick Reference
|
||||
|
||||
## New Config Fields
|
||||
|
||||
```typescript
|
||||
// src/config/schema.ts (line ~179)
|
||||
const gmailSchema = z.object({
|
||||
// ... existing fields ...
|
||||
|
||||
pubsub_subscription_id: z.string().optional(),
|
||||
pubsub_pull_interval: z.string().default('60s'),
|
||||
pubsub_max_messages: z.number().min(1).max(100).default(10),
|
||||
disable_push: z.boolean().default(false),
|
||||
});
|
||||
```
|
||||
|
||||
## New Methods
|
||||
|
||||
### setupPullSubscription()
|
||||
```typescript
|
||||
// src/automation/gmail.ts
|
||||
private async setupPullSubscription(): Promise<void>
|
||||
```
|
||||
- Parses `pubsub_subscription_id` (shorthand or full path)
|
||||
- Validates format
|
||||
- Starts interval timer with `pubsub_pull_interval`
|
||||
|
||||
### pullSubscriptionMessages()
|
||||
```typescript
|
||||
// src/automation/gmail.ts
|
||||
private async pullSubscriptionMessages(): Promise<void>
|
||||
```
|
||||
- Uses `@google-cloud/pubsub` SDK
|
||||
- Pulls up to `pubsub_max_messages`
|
||||
- Calls `handlePushNotification()` for each message
|
||||
- Acknowledges messages after processing
|
||||
|
||||
### buildWatchErrorHint()
|
||||
```typescript
|
||||
// src/automation/gmail.ts
|
||||
private buildWatchErrorHint(errMsg: string): string
|
||||
```
|
||||
- Matches error patterns ("Invalid topicName", "Permission denied", "Not found")
|
||||
- Returns actionable hints with gcloud commands
|
||||
|
||||
## Modified Methods
|
||||
|
||||
### connect()
|
||||
```typescript
|
||||
// Changes:
|
||||
if (!this.config.disable_push) {
|
||||
await this.setupWatch();
|
||||
} else {
|
||||
console.log('Push disabled');
|
||||
}
|
||||
|
||||
await this.setupPullSubscription();
|
||||
|
||||
console.log(`Connected (push=${...}, pull=${...}, poll_interval=${...})`);
|
||||
```
|
||||
|
||||
### disconnect()
|
||||
```typescript
|
||||
// Add:
|
||||
if (this.pullSubscriptionTimer) {
|
||||
clearInterval(this.pullSubscriptionTimer);
|
||||
this.pullSubscriptionTimer = undefined;
|
||||
}
|
||||
```
|
||||
|
||||
## Doctor Check Enhancement
|
||||
|
||||
```typescript
|
||||
// src/cli/doctor.ts (line ~246)
|
||||
const checkGmail: Check = async (ctx) => {
|
||||
// ... existing validation ...
|
||||
|
||||
// NEW: Mode detection
|
||||
const modes: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (gmail.pubsub_topic && !gmail.disable_push) {
|
||||
modes.push('push');
|
||||
if (ctx.config.server?.tailscale_only && !ctx.config.server?.tailscale?.funnel) {
|
||||
warnings.push('push requires public endpoint or Tailscale funnel');
|
||||
}
|
||||
}
|
||||
|
||||
if (gmail.pubsub_subscription_id) {
|
||||
modes.push('pull');
|
||||
}
|
||||
|
||||
if (modes.length === 0) {
|
||||
modes.push('poll');
|
||||
} else {
|
||||
modes.push('poll-fallback');
|
||||
}
|
||||
|
||||
const modeStr = modes.join(' + ');
|
||||
const detail = warnings.length > 0
|
||||
? `${modeStr} (⚠️ ${warnings.join(', ')})`
|
||||
: `${modeStr} → ${gmail.output.channel}/${gmail.output.peer}`;
|
||||
|
||||
return { status: warnings.length > 0 ? 'warn' : 'pass', label: 'Gmail configured', detail };
|
||||
};
|
||||
```
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Pull Subscription
|
||||
```typescript
|
||||
it('should set up pull subscription with full path', async () => {
|
||||
const config = {
|
||||
...baseConfig,
|
||||
pubsub_subscription_id: 'projects/test-project/subscriptions/gmail-pull',
|
||||
};
|
||||
const watcher = new GmailWatcher(config, channelLookup);
|
||||
await watcher.connect();
|
||||
|
||||
expect(watcher['subscriptionPath']).toBe('projects/test-project/subscriptions/gmail-pull');
|
||||
expect(watcher['pullSubscriptionTimer']).toBeDefined();
|
||||
});
|
||||
```
|
||||
|
||||
### Shorthand Auto-Prefix
|
||||
```typescript
|
||||
it('should auto-prefix subscription with project_id', async () => {
|
||||
const config = {
|
||||
...baseConfig,
|
||||
pubsub_subscription_id: 'gmail-pull',
|
||||
};
|
||||
const watcher = new GmailWatcher(config, channelLookup);
|
||||
await watcher.connect();
|
||||
|
||||
expect(watcher['subscriptionPath']).toBe('projects/test-project/subscriptions/gmail-pull');
|
||||
});
|
||||
```
|
||||
|
||||
### Disable Push
|
||||
```typescript
|
||||
it('should skip watch when disable_push is true', async () => {
|
||||
const config = {
|
||||
...baseConfig,
|
||||
pubsub_topic: 'gmail-push',
|
||||
disable_push: true,
|
||||
};
|
||||
const watcher = new GmailWatcher(config, channelLookup);
|
||||
await watcher.connect();
|
||||
|
||||
const mockWatch = vi.mocked(google.gmail().users.watch);
|
||||
expect(mockWatch).not.toHaveBeenCalled();
|
||||
});
|
||||
```
|
||||
|
||||
### Error Hints
|
||||
```typescript
|
||||
it('should return topic hint for Invalid topicName error', () => {
|
||||
const watcher = new GmailWatcher(baseConfig, channelLookup);
|
||||
const hint = watcher['buildWatchErrorHint']('Invalid topicName');
|
||||
|
||||
expect(hint).toContain('Set automation.gmail.pubsub_topic');
|
||||
expect(hint).toContain('projects/test-project/topics/gmail-push');
|
||||
});
|
||||
```
|
||||
|
||||
### Doctor Modes
|
||||
```typescript
|
||||
it('should detect hybrid mode', async () => {
|
||||
const config = {
|
||||
...baseConfig,
|
||||
automation: {
|
||||
gmail: {
|
||||
enabled: true,
|
||||
pubsub_topic: 'gmail-push',
|
||||
pubsub_subscription_id: 'gmail-pull',
|
||||
output: { channel: 'telegram', peer: '123' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await checkGmail({ config });
|
||||
expect(result.status).toBe('pass');
|
||||
expect(result.detail).toContain('push + pull + poll-fallback');
|
||||
});
|
||||
```
|
||||
|
||||
## Deployment Pattern Config Examples
|
||||
|
||||
### Public Endpoint (Push Only)
|
||||
```yaml
|
||||
automation:
|
||||
gmail:
|
||||
enabled: true
|
||||
credentials_file: ~/.config/flynn/gmail-credentials.json
|
||||
pubsub_topic: projects/my-project/topics/gmail-push
|
||||
output:
|
||||
channel: telegram
|
||||
peer: "123456789"
|
||||
```
|
||||
|
||||
### Tailscale Funnel (Push Only)
|
||||
```yaml
|
||||
server:
|
||||
tailscale:
|
||||
serve: true
|
||||
funnel: true
|
||||
|
||||
automation:
|
||||
gmail:
|
||||
enabled: true
|
||||
credentials_file: ~/.config/flynn/gmail-credentials.json
|
||||
pubsub_topic: gmail-push # Shorthand OK
|
||||
output:
|
||||
channel: telegram
|
||||
peer: "123456789"
|
||||
```
|
||||
|
||||
### Private Network (Pull Only)
|
||||
```yaml
|
||||
automation:
|
||||
gmail:
|
||||
enabled: true
|
||||
credentials_file: ~/.config/flynn/gmail-credentials.json
|
||||
pubsub_subscription_id: projects/my-project/subscriptions/gmail-pull
|
||||
pubsub_pull_interval: "60s"
|
||||
output:
|
||||
channel: telegram
|
||||
peer: "123456789"
|
||||
```
|
||||
|
||||
### Hybrid (Recommended)
|
||||
```yaml
|
||||
automation:
|
||||
gmail:
|
||||
enabled: true
|
||||
credentials_file: ~/.config/flynn/gmail-credentials.json
|
||||
pubsub_topic: gmail-push
|
||||
pubsub_subscription_id: gmail-pull
|
||||
pubsub_pull_interval: "60s"
|
||||
poll_interval: "300s"
|
||||
output:
|
||||
channel: telegram
|
||||
peer: "123456789"
|
||||
```
|
||||
|
||||
## GCP Setup Checklist
|
||||
|
||||
- [ ] Create project: `gcloud projects create my-flynn-project`
|
||||
- [ ] Enable APIs: `gcloud services enable gmail.googleapis.com pubsub.googleapis.com`
|
||||
- [ ] Create topic: `gcloud pubsub topics create gmail-push`
|
||||
- [ ] Grant IAM: `gcloud pubsub topics add-iam-policy-binding ... --member=serviceAccount:gmail-api-push@system.gserviceaccount.com --role=roles/pubsub.publisher`
|
||||
- [ ] Create OAuth credentials (Desktop app) via Cloud Console
|
||||
- [ ] Download credentials JSON to `~/.config/flynn/gmail-credentials.json`
|
||||
- [ ] Create push subscription (if using push): `gcloud pubsub subscriptions create gmail-push-sub --topic=gmail-push --push-endpoint=https://flynn.example.com/gmail/push`
|
||||
- [ ] Create pull subscription (if using pull): `gcloud pubsub subscriptions create gmail-pull-sub --topic=gmail-push`
|
||||
- [ ] Authenticate: `flynn gmail-auth`
|
||||
- [ ] Verify: `flynn doctor`
|
||||
|
||||
## Debugging Commands
|
||||
|
||||
```bash
|
||||
# Check if topic exists
|
||||
gcloud pubsub topics list --filter="name:gmail-push"
|
||||
|
||||
# Check subscriptions
|
||||
gcloud pubsub subscriptions list --filter="topic:gmail-push"
|
||||
|
||||
# Test pull subscription
|
||||
gcloud pubsub subscriptions pull gmail-pull-sub --limit=5
|
||||
|
||||
# Check IAM
|
||||
gcloud pubsub topics get-iam-policy projects/my-project/topics/gmail-push
|
||||
|
||||
# Verify Flynn config
|
||||
flynn doctor
|
||||
|
||||
# Check Flynn logs
|
||||
journalctl -u flynn -f
|
||||
```
|
||||
|
||||
## Error Message Mapping
|
||||
|
||||
| Error | Hint |
|
||||
|-------|------|
|
||||
| `Invalid topicName` | Set `pubsub_topic` to `projects/<project>/topics/<topic>`, ensure topic exists, check subscription configuration |
|
||||
| `Permission denied` / `Forbidden` | Grant Gmail API publish permission: `gcloud pubsub topics add-iam-policy-binding ...` |
|
||||
| `Not found` / `404` | Create topic: `gcloud pubsub topics create gmail-push` |
|
||||
| Other | Generic hint about pull subscription fallback |
|
||||
|
||||
## Dependencies
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@google-cloud/pubsub": "^4.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Files Modified Summary
|
||||
|
||||
| File | Lines Changed | New Code | Description |
|
||||
|------|---------------|----------|-------------|
|
||||
| `src/config/schema.ts` | +8 | 4 fields | Config schema additions |
|
||||
| `src/automation/gmail.ts` | +120 | 3 methods | Pull support, error hints |
|
||||
| `src/cli/doctor.ts` | +40 | Mode detection | Enhanced validation |
|
||||
| `package.json` | +1 | Dependency | `@google-cloud/pubsub` |
|
||||
| `README.md` | +150 | Full rewrite | Deployment patterns, setup |
|
||||
| `config/default.yaml` | +15 | Comments | Example configs |
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. ✅ Config schema fields
|
||||
2. ✅ `buildWatchErrorHint()` method (standalone)
|
||||
3. ✅ `setupPullSubscription()` method (requires new dependency)
|
||||
4. ✅ `pullSubscriptionMessages()` method
|
||||
5. ✅ Update `connect()` to call pull setup and check `disable_push`
|
||||
6. ✅ Update `disconnect()` to clear pull timer
|
||||
7. ✅ Enhance `checkGmail` doctor check
|
||||
8. ✅ Write unit tests
|
||||
9. ✅ Update documentation
|
||||
10. ✅ Manual integration testing
|
||||
Reference in New Issue
Block a user