Skip to content

pdaxt/substack-opera-control

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Substack Opera Control

Lightweight library to control Substack on Opera browser via Chrome DevTools Protocol (CDP).

No Puppeteer bloat. No Playwright overhead. Just direct CDP over WebSocket.

Why?

Existing browser automation libraries (Puppeteer, Playwright) launch their own browser instances. This library connects to your existing Opera session where you're already logged in.

Built specifically for Substack automation:

  • Read Notes feed
  • Like notes
  • Post comments
  • Compose new notes
  • Take screenshots

Requirements

  • Opera browser running with remote debugging enabled
  • Node.js 18+

Enable Remote Debugging in Opera

Start Opera with the debugging flag:

/Applications/Opera.app/Contents/MacOS/Opera --remote-debugging-port=9222

Or add --remote-debugging-port=9222 to your Opera shortcut.

Installation

npm install substack-opera-control

Or clone and use locally:

git clone https://github.com/yourusername/substack-opera-control.git
cd substack-opera-control
npm install

CLI Usage

# List notes from feed
node cli.js notes 10

# Take screenshot
node cli.js screenshot /tmp/screenshot.png

# Navigate
node cli.js goto https://substack.com/notes

# Get current URL
node cli.js url

# Scroll feed
node cli.js scroll 1000

# Like a note (by index)
node cli.js like 0

# Open comments on a note
node cli.js comment 0

# Post a comment (after opening comments)
node cli.js post-comment "Great insight!"

# Compose a new note (doesn't post)
node cli.js compose "Your note text here"

# Submit the composed note
node cli.js submit

Library Usage

import { SubstackController } from 'substack-opera-control';

const controller = new SubstackController();

async function main() {
    // Connect to Opera on port 9222
    await controller.connect();

    // Navigate to Notes
    await controller.goToNotes();

    // Get visible notes
    const notes = await controller.getNotes(10);
    console.log(notes);

    // Like the first note
    await controller.likeNote(0);

    // Take a screenshot
    await controller.screenshot('/tmp/substack.png');

    // Post a comment
    await controller.openComments(0);
    await controller.postComment('Great post!');

    // Disconnect (keeps Opera open)
    controller.disconnect();
}

main();

API

SubstackController

Notes Feed

Method Description
connect(port) Connect to Opera on given port (default: 9222)
goToNotes() Navigate to Substack Notes feed
goToHome() Navigate to Substack Home
getNotes(limit) Get visible notes from feed
likeNote(index) Like a note by index
openComments(index) Open comments for a note
postComment(text) Post a comment (must open comments first)
replyToNote(text) Reply to note on detail page (robust method)
postNote(text) Compose a new note
submitNote() Submit the composed note
screenshot(path) Take and save screenshot
scrollDown(pixels) Scroll down by pixels
disconnect() Disconnect from browser (keeps Opera open)

Publication Settings (NEW)

Method Description
goToSettings(publication) Navigate to publication settings
getNavigationItems(publication) Get nav bar items with visibility status
setNavigationVisibility(pub, item, visible) Show/hide nav item (e.g., 'Leaderboard')

About Page (NEW)

Method Description
goToAboutPageEditor(publication) Navigate to /about page editor
goToAboutSettings(publication) Navigate to subscription About content
setEditorContent(html) Set content in ProseMirror editor
getEditorContent() Get current editor content
clickContinue() Click Continue button
configurePublishOptions(options) Configure audience/email settings
clickPublish() Click publish button
clickSave() Click save button

Sections (NEW)

Method Description
goToSections(publication) Navigate to sections management
createSection(pub, {name, description, emoji}) Create a new section
getSections(publication) Get list of existing sections

Utilities

Method Description
waitForElement(selector, timeout) Wait for element to appear
clickButtonByText(text) Find and click button by text
getButtons() Debug: get all buttons on page
pageContains(text) Check if page contains text

Stealth Publish (NEW)

Publish content to web without sending email notifications.

Method Description
disableEmailDelivery() Uncheck email delivery (call after Continue)
publishStealth(audience) Publish to web only, no email ('everyone' or 'paid')
publishAboutPageStealth(pub, html) Full workflow: edit + publish About page silently

Publication Settings Examples

import { SubstackController } from 'substack-opera-control';

const controller = new SubstackController();
await controller.connect();

// Hide Leaderboard from navigation
await controller.setNavigationVisibility('bskiller', 'Leaderboard', false);

// Create a new section
await controller.createSection('bskiller', {
    name: 'Code Drops',
    emoji: '💻',
    description: 'Production code you can use'
});

// Edit About page (manual steps)
await controller.goToAboutPageEditor('bskiller');
await controller.setEditorContent('<p>Hello world</p>');
await controller.clickContinue();
await controller.configurePublishOptions({ audience: 'everyone', sendEmail: false });
await controller.clickPublish();

controller.disconnect();

Stealth Publish Example

Publish content to web only (no email notifications to subscribers):

import { SubstackController } from 'substack-opera-control';

const controller = new SubstackController();
await controller.connect();

// Option 1: Full workflow in one call
const result = await controller.publishAboutPageStealth('bskiller', `
    <h1>About BSKiller</h1>
    <p>The AI Intelligence Platform that calls BS on hype.</p>
`);
console.log('Published:', result.url);

// Option 2: Manual steps with stealth publish
await controller.goToAboutPageEditor('bskiller');
await controller.setEditorContent('<p>Your content here</p>');
await controller.clickContinue();
await controller.publishStealth('everyone'); // No email sent!

controller.disconnect();

CDPClient (Low-level)

For custom automation needs:

import { CDPClient } from 'substack-opera-control';

const cdp = new CDPClient();
await cdp.connect(9222);

// Navigate
await cdp.navigate('https://example.com');

// Execute JavaScript
const title = await cdp.evaluate('document.title');

// Click element
await cdp.click('button.submit');

// Type text
await cdp.type('input[name="email"]', '[email protected]');

// Take screenshot
const buffer = await cdp.screenshot();

License

MIT

Author

BSKiller - bskiller.com

About

Lightweight library to control Substack on Opera browser via CDP

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •