Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The API is split into two parts:
- `TelegramStrategy`
- `DevtoStrategy`
- `NostrStrategy` (requires Node.js v22+)
- `OrgSocialStrategy`

Each strategy requires its own parameters that are specific to the service. If you only want to post to a particular service, you can just directly use the strategy for that service.

Expand All @@ -46,6 +47,7 @@ import {
TelegramStrategy,
DevtoStrategy,
NostrStrategy,
OrgSocialStrategy,
} from "@humanwhocodes/crosspost";

// Note: Use an app password, not your login password!
Expand Down Expand Up @@ -102,6 +104,13 @@ const nostr = new NostrStrategy({
relays: ["wss://relay.example.com", "wss://relay2.example.com"],
});

// Note: vfile and public URL required (from Org Social Host signup)
const orgSocial = new OrgSocialStrategy({
vfile: "http://host.org-social.org/vfile?token=YOUR_TOKEN&ts=TIMESTAMP&sig=SIGNATURE",
publicUrl: "http://host.org-social.org/your-nick/social.org",
host: "host.org-social.org", // optional, defaults to "host.org-social.org"
});

// create a client that will post to all services
const client = new Client({
strategies: [
Expand All @@ -114,6 +123,7 @@ const client = new Client({
telegram,
devto,
nostr,
orgSocial,
],
});

Expand Down Expand Up @@ -185,6 +195,7 @@ Usage: crosspost [options] ["Message to post."]
--telegram Post to Telegram.
--slack, -s Post to Slack.
--nostr, -n Post to Nostr.
--orgsocial, -o Post to Org Social.
--mcp Start MCP server.
--file The file to read the message from.
--image The image file to upload with the message.
Expand Down Expand Up @@ -247,6 +258,10 @@ Each strategy requires a set of environment variables in order to execute:
- Nostr
- `NOSTR_PRIVATE_KEY`
- `NOSTR_RELAYS`
- Org Social
- `ORGSOCIAL_VFILE`
- `ORGSOCIAL_PUBLIC_URL`
- `ORGSOCIAL_HOST` (optional)

Tip: You can load environment variables from a `.env` file by setting the environment variable `CROSSPOST_DOTENV`. Set it to `1` to use `.env` in the current working directory, or set it to a specific filepath to use a different location.

Expand Down Expand Up @@ -507,6 +522,66 @@ Nostr posts are "short text notes" (kind 1 events) with a 280 character limit. I

**Security:** Keep your private key secure and never share it. Consider using a dedicated key for crossposting rather than your main Nostr identity key.

### Org Social

To enable posting to Org Social:

Org Social is a decentralized social network that runs on Org Mode files over HTTP. To use it with Crosspost, you need to sign up at an Org Social Host instance.

1. Sign up at an Org Social Host instance (e.g., [host.org-social.org](https://host.org-social.org/)):

```bash
curl -X POST https://host.org-social.org/signup \
-H "Content-Type: application/json" \
-d '{"nick": "your-nickname"}'
```

2. You'll receive two important values:

- **`vfile`**: A virtual file URL with authentication (format: `http://host.org-social.org/vfile?token=...&ts=...&sig=...`)
- **`public-url`**: Your public social.org file URL (format: `http://host.org-social.org/your-nick/social.org`)

3. Use these values with the `OrgSocialStrategy`:
```js
const orgSocial = new OrgSocialStrategy({
vfile: "YOUR_VFILE_URL",
publicUrl: "YOUR_PUBLIC_URL",
host: "host.org-social.org", // optional
});
```

For the `ORGSOCIAL_VFILE` (required):

- The virtual file URL you received during signup
- Contains authentication tokens to update your social.org file
- Keep this URL secure and never share it

For the `ORGSOCIAL_PUBLIC_URL` (required):

- Your public social.org file URL that others can access
- This is what you share with followers

For the `ORGSOCIAL_HOST` (optional):

- The Org Social Host instance domain
- Defaults to `host.org-social.org` if not provided

**Important Notes:**

- Images are not supported in Org Social text posts. Include image links in the message text instead.
- Org Social has no character limit - you can write posts of any length.
- Org Social files use Org Mode format with rich text features like bold, italic, code blocks, tables, and more.
- Posts are appended at the end of your social.org file, after the last post.
- The first post after `* Posts` has no blank line before it, but subsequent posts have one blank line separator.
- Each post gets a unique RFC 3339 timestamp ID. If posting multiple times quickly, the strategy waits to ensure unique IDs.
- If you don't update your social.org at least once a month on the default host, you may lose your nickname.

**Learn more:**

- [Org Social Specification](https://github.com/tanrax/org-social)
- [Org Social Host Documentation](https://github.com/tanrax/org-social-host)
- [Org Social Community](https://org-social.org/social.org)

## License

Copyright 2024-2025 Nicholas C. Zakas
Expand Down
130 changes: 130 additions & 0 deletions examples/orgsocial-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* @fileoverview Example usage of OrgSocialStrategy
* @author Andros Fenollosa
*/

/* global setTimeout, AbortController */

import { OrgSocialStrategy, Client } from "@humanwhocodes/crosspost";

// Initialize the Org Social strategy with your vfile and public URL
// You get these from signing up at an Org Social Host instance
const orgSocial = new OrgSocialStrategy({
// The vfile URL you received during signup (contains authentication token)
vfile: "http://host.org-social.org/vfile?token=YOUR_TOKEN&ts=TIMESTAMP&sig=SIGNATURE",

// Your public social.org URL
publicUrl: "http://host.org-social.org/your-nick/social.org",

// Optional: Custom Org Social Host instance (defaults to "host.org-social.org")
host: "host.org-social.org",
});

// Example 1: Post a simple message
export async function postSimpleMessage() {
try {
const response = await orgSocial.post("Hello from Org Social! 🌍");
console.log("Post successful!");
console.log("Public URL:", response.data["public-url"]);
} catch (error) {
console.error("Failed to post:", error.message);
}
}

// Example 2: Post with multiple lines
export async function postMultilineMessage() {
try {
const message = `This is a multiline post with rich content.

I can include:
- Lists with multiple items
- *Bold text* and /italic text/
- Code snippets: ~print("hello")~
- Links: [[https://example.com][Example website]]

And much more!`;

const response = await orgSocial.post(message);
console.log("Multiline post successful!");
console.log("Public URL:", response.data["public-url"]);
} catch (error) {
console.error("Failed to post:", error.message);
}
}

// Example 3: Use with the Client to post to multiple services
export async function postToMultipleServices() {
// You can combine Org Social with other strategies
const client = new Client({
strategies: [
orgSocial,
// Add other strategies here (Twitter, Mastodon, etc.)
],
});

try {
const results = await client.post("Hello from all my social networks!");

results.forEach(result => {
if (result.ok) {
console.log(
`✓ ${result.name}: ${result.url || "Posted successfully"}`,
);
} else {
console.error(`✗ ${result.name}: ${result.reason.message}`);
}
});
} catch (error) {
console.error("Failed to post:", error.message);
}
}

// Example 4: Handle abort signals
export async function postWithAbortSignal() {
const controller = new AbortController();

// Cancel the post after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
await orgSocial.post("This post might be cancelled...", {
signal: controller.signal,
});
console.log("Post successful!");
} catch (error) {
if (error.name === "AbortError") {
console.log("Post was cancelled");
} else {
console.error("Failed to post:", error.message);
}
}
}

// Note: Images are not supported in Org Social text posts
// If you need to share images, include links to them in your message:
export async function postWithImageLinks() {
try {
const message = `Check out this beautiful sunset!

[[https://example.com/sunset.jpg][Beautiful Sunset]]`;

await orgSocial.post(message);
console.log("Post with image link successful!");
} catch (error) {
console.error("Failed to post:", error.message);
}
}

// Important notes about Org Social:
// - Posts are appended at the END of your social.org file, after the last post
// - No character limit - write as much as you want!
// - Supports full Org Mode syntax: bold, italic, code blocks, tables, etc.
// - Images not supported directly, but you can include image links

// Run examples
// Uncomment the function you want to test:
// postSimpleMessage();
// postMultilineMessage();
// postToMultipleServices();
// postWithAbortSignal();
// postWithImageLinks();
6 changes: 0 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,11 @@ export {
NostrEvent,
NostrEventResponse,
} from "./strategies/nostr.js";
export {
OrgSocialStrategy,
OrgSocialOptions,
OrgSocialErrorResponse,
OrgSocialSuccessResponse,
OrgSocialPost,
} from "./strategies/orgsocial.js";
export { Client, ClientOptions, Strategy } from "./client.js";
Loading