Skip to content

Commit

Permalink
Implement native sharing
Browse files Browse the repository at this point in the history
  • Loading branch information
nightscape committed Nov 18, 2024
1 parent 51fc595 commit a87c6d4
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 1 deletion.
12 changes: 12 additions & 0 deletions plugs/share/share.plug.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ functions:
events:
- share:options

handleShareTarget:
path: share.ts:handleShareTarget
events:
- http:request:/share_target

clipboardMarkdownShare:
path: share.ts:clipboardMarkdownShare
events:
Expand All @@ -31,3 +36,10 @@ functions:
path: publish.ts:publishShare
events:
- share:publish

config:
# Built-in configuration schemas
schema.config.properties:
shareTargetPage:
type: string
format: page-ref
100 changes: 100 additions & 0 deletions plugs/share/share.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
editor,
events,
markdown,
space,
system,
} from "@silverbulletmd/silverbullet/syscalls";
import { findNodeOfType, renderToText } from "../../plug-api/lib/tree.ts";
Expand All @@ -11,6 +12,11 @@ import {
encodePageURI,
parsePageRef,
} from "@silverbulletmd/silverbullet/lib/page_ref";
import type { EndpointRequest } from "@silverbulletmd/silverbullet/types";
import { localDateString } from "$lib/dates.ts";
import { cleanPageRef } from "@silverbulletmd/silverbullet/lib/resolve";
import { builtinFunctions } from "$lib/builtin_query_functions.ts";
import { renderTheTemplate } from "$common/syscalls/template.ts";

type ShareOption = {
id: string;
Expand Down Expand Up @@ -134,3 +140,97 @@ export async function clipboardRichTextShare(text: string) {
await editor.copyToClipboard(new Blob([html], { type: "text/html" }));
await editor.flashNotification("Copied to rich text to clipboard!");
}

function parseMultipartFormData(body: string, boundary: string) {
const parts = body.split(`--${boundary}`);
return parts.slice(1, -1).map((part) => {
const [headers, content] = part.split("\r\n\r\n");
const nameMatch = headers.match(/name="([^"]+)"/);
if (!nameMatch) {
throw new Error("Could not parse form field name");
}
const name = nameMatch[1];
const value = content.trim();
return { name, value };
});
}
export async function handleShareTarget(request: EndpointRequest) {
console.log("Share target received:", {
method: request.method,
headers: request.headers,
body: request.body,
});

try {
// Parse multipart form data
const contentType = request.headers["content-type"];
if (!contentType) {
throw new Error(
`No content type found in ${JSON.stringify(request.headers)}`,
);
}
const boundary = contentType.split("boundary=")[1];
if (!boundary) {
throw new Error(`No multipart boundary found in ${contentType}`);
}
const formData = parseMultipartFormData(request.body, boundary);
const { title = "", text = "", url = "" } = formData.reduce(
(acc: Record<string, string>, curr: { name: string; value: string }) => {
acc[curr.name] = curr.value;
return acc;
},
{},
);

// Format the shared content
const timestamp = localDateString(new Date());
const sharedContent = `\n\n## ${title}
${text}
${url ? `URL: ${url}` : ""}\nAdded at ${timestamp}`;

// Get the target page from space config, with fallback
let targetPage = "Inbox";
try {
targetPage = cleanPageRef(
await renderTheTemplate(
await system.getSpaceConfig("shareTargetPage", "Inbox"),
{},
{},
builtinFunctions,
),
);
} catch (e: any) {
console.error("Error parsing share target page from config", e);
}

// Try to read existing page content
let currentContent = "";
try {
currentContent = await space.readPage(targetPage);
} catch (_e) {
// If page doesn't exist, create it with a header
currentContent = `# ${targetPage}\n`;
}

// Append the new content
const newContent = currentContent + sharedContent;

// Write the updated content back to the page
await space.writePage(targetPage, newContent);

// Return a redirect response to the target page
return {
status: 303, // "See Other" redirect
headers: {
"Location": `/${targetPage}`,
},
body: "Content shared successfully",
};
} catch (e: any) {
console.error("Error handling share:", e);
return {
status: 500,
body: "Error processing share: " + e.message,
};
}
}
12 changes: 11 additions & 1 deletion web/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,15 @@
"display_override": ["window-controls-overlay"],
"scope": "/",
"theme_color": "#e1e1e1",
"description": "Markdown as a platform"
"description": "Markdown as a platform",
"share_target": {
"action": "/_/share_target",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url"
}
}
}
2 changes: 2 additions & 0 deletions website/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ An attempt at documenting the changes/new features introduced in each release.

## Edge
_These features are not yet properly released, you need to use [the edge builds](https://community.silverbullet.md/t/living-on-the-edge-builds/27) to try them._
* Native [[Share]] functionality, allowing you to use your OS'es native share functionality to share data with SilverBullet.
Target page can be configured as `shareTargetPage` in [[^SETTINGS]].

* (Security) Implemented a lockout mechanism after a number of failed login attempts for [[Authentication]] (configured via [[Install/Configuration#Authentication]]) (by [Peter Weston](https://github.com/silverbulletmd/silverbullet/pull/1152))

Expand Down
4 changes: 4 additions & 0 deletions website/SETTINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,8 @@ emoji:
aliases:
smile: 😀
sweat_smile: 😅
# Share Configuration
# Page where shared content will be stored (defaults to "Shared Items")
shareTargetPage: "Inbox"
```

0 comments on commit a87c6d4

Please sign in to comment.