Skip to content
Closed
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
146 changes: 146 additions & 0 deletions skills/jilycn/slack-extended/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
name: slack-extended
description: Upload files, manage canvases, and manage bookmarks in Slack. Use when you need to share files, create/edit canvases, or add/organize link bookmarks in Slack channels. Complements the core slack skill which handles messages, reactions, and pins.
metadata: { "openclaw": { "emoji": "📎" } }
---

# Slack Extended

Extends the core `slack` skill with file uploads, canvas management, and bookmarks. Uses Python scripts that call the Slack API directly with the bot token from `~/.openclaw/openclaw.json`.

**Requires OAuth scopes:** `files:write`, `canvases:write`, `bookmarks:write`, `bookmarks:read` (add at api.slack.com if missing).

## File Upload

Upload a local file to a Slack channel:

```bash
python3 scripts/slack_file_upload.py \
--channel C123ABC \
--file /path/to/file.png \
--title "Q4 Report" \
--message "Here's the latest report"
```

**Arguments:**
- `--channel` (required): Channel ID to share the file in
- `--file` (required): Path to the local file
- `--title`: Display title (defaults to filename)
- `--message`: Comment posted with the file

Returns JSON with `file_id`, `permalink`, and `channel`.

**Common patterns:**
- Share a generated chart: `--file /tmp/chart.png --title "Performance Chart"`
- Share a log file: `--file /var/log/app.log --title "Error Logs"`
- Share with context: `--message "Backtest results for GEM v2" --file results.csv`

## Canvas Operations

Manage Slack canvases (collaborative documents):

### Create a canvas

```bash
python3 scripts/slack_canvas.py create \
--title "Sprint Notes" \
--markdown "## Goals\n- Ship feature X\n- Fix bug Y"
```

### Edit a canvas

Append content:
```bash
python3 scripts/slack_canvas.py edit \
--canvas-id F07ABCD1234 \
--operation insert_at_end \
--markdown "## Update\nNew section added"
```

Replace a section:
```bash
python3 scripts/slack_canvas.py edit \
--canvas-id F07ABCD1234 \
--section-id temp:C:abc123 \
--operation replace \
--markdown "## Revised Section\nUpdated content"
```

**Operations:** `insert_at_start`, `insert_at_end`, `insert_after`, `replace`, `delete`

### Look up sections

```bash
python3 scripts/slack_canvas.py sections \
--canvas-id F07ABCD1234
```

### Delete a canvas

```bash
python3 scripts/slack_canvas.py delete \
--canvas-id F07ABCD1234
```

### Set access

```bash
python3 scripts/slack_canvas.py access \
--canvas-id F07ABCD1234 \
--channel C123ABC \
--level edit

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fix invalid canvas access level in usage example

The documented access command uses --level edit, but scripts/slack_canvas.py only accepts --level values read or write (argparse choices), so users following this example hit an immediate CLI error and cannot apply canvas permissions until they discover the mismatch themselves.

Useful? React with 👍 / 👎.

```

## Canvas Markdown

Canvases support: bold, italic, strikethrough, headings (h1-h3), bulleted/ordered lists, checklists, code blocks, code spans, links, tables (max 300 cells), blockquotes, dividers, emojis.

**Mentions:** `![](@USER_ID)` for users, `![](#CHANNEL_ID)` for channels.

## Bookmarks

Manage link bookmarks in the bookmark bar at the top of Slack channels.

**Limitation:** Slack API only supports **link** bookmarks. Folders are a UI-only feature and cannot be created via the API.

### List bookmarks

```bash
python3 scripts/slack_bookmark.py list \
--channel C123ABC
```

### Add a link bookmark

```bash
python3 scripts/slack_bookmark.py add \
--channel C123ABC \
--title "Design Docs" \
--link "https://example.com" \
--emoji ":link:"
```

### Edit a bookmark

```bash
python3 scripts/slack_bookmark.py edit \
--channel C123ABC \
--bookmark-id Bk123 \
--title "New Title"
```

### Remove a bookmark

```bash
python3 scripts/slack_bookmark.py remove \
--channel C123ABC \
--bookmark-id Bk123
```

## Troubleshooting

- **`missing_scope` error**: Add the required scope (`files:write`, `canvases:write`, `bookmarks:write`, or `bookmarks:read`) at api.slack.com, then reinstall the app to the workspace.
- **`channel_not_found`**: Use the channel ID (e.g. `C07ABC123`), not the channel name.
- **`not_authed`**: Bot token may have changed. Check `~/.openclaw/openclaw.json`.
- **Canvas edit fails**: Look up sections first to get valid `section_id` values.
- **Folders not supported**: Slack API does not support creating folders — only link bookmarks. Folders can only be created manually in the Slack UI.
9 changes: 9 additions & 0 deletions skills/jilycn/slack-extended/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"owner": "jilycn",
"slug": "slack-extended",
"displayName": "Slack Extended",
"latest": {
"version": "0.1.0"
},
"history": []
}
174 changes: 174 additions & 0 deletions skills/jilycn/slack-extended/scripts/slack_bookmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""Manage Slack channel bookmarks (add, list, edit, remove).

Note: Slack API only supports link bookmarks. Folders are UI-only and cannot be
created via the API.

Usage:
python3 slack_bookmark.py add --channel C123ABC --title "Design Docs" --link "https://example.com"
python3 slack_bookmark.py list --channel C123ABC
python3 slack_bookmark.py edit --channel C123ABC --bookmark-id Bk123 --title "New Title"
python3 slack_bookmark.py remove --channel C123ABC --bookmark-id Bk123
"""

import argparse
import json
import os
import sys
import urllib.request
import urllib.parse
import urllib.error

CONFIG_PATH = os.path.expanduser("~/.openclaw/openclaw.json")


def get_bot_token():
with open(CONFIG_PATH) as f:
config = json.load(f)
token = config.get("channels", {}).get("slack", {}).get("botToken")
if not token:
print("Error: No botToken found in", CONFIG_PATH, file=sys.stderr)
sys.exit(1)
return token


def slack_api(token, method, params):
"""POST to Slack API with application/x-www-form-urlencoded."""
url = f"https://slack.com/api/{method}"
headers = {"Authorization": f"Bearer {token}"}
body = urllib.parse.urlencode(params).encode()
req = urllib.request.Request(url, data=body, headers=headers)
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
error_body = e.read().decode()
print(f"Error: HTTP {e.code} from {method}: {error_body}", file=sys.stderr)
sys.exit(1)


def cmd_add(args):
token = get_bot_token()
if not args.link:
print("Error: --link is required (Slack API only supports link bookmarks, not folders)", file=sys.stderr)
sys.exit(1)
params = {
"channel_id": args.channel,
"title": args.title,
"type": "link",
"link": args.link,
}
if args.emoji:
params["emoji"] = args.emoji

resp = slack_api(token, "bookmarks.add", params)
if not resp.get("ok"):
print(f"Error: bookmarks.add failed: {resp.get('error', 'unknown')}", file=sys.stderr)
sys.exit(1)

bookmark = resp.get("bookmark", {})
print(json.dumps({
"ok": True,
"bookmark_id": bookmark.get("id"),
"title": bookmark.get("title"),
"type": bookmark.get("type"),
"link": bookmark.get("link"),
"channel": args.channel,
}, indent=2))


def cmd_list(args):
token = get_bot_token()
resp = slack_api(token, "bookmarks.list", {"channel_id": args.channel})
if not resp.get("ok"):
print(f"Error: bookmarks.list failed: {resp.get('error', 'unknown')}", file=sys.stderr)
sys.exit(1)

bookmarks = resp.get("bookmarks", [])
result = []
for b in bookmarks:
entry = {
"id": b.get("id"),
"title": b.get("title"),
"type": b.get("type"),
"link": b.get("link", ""),
}
if b.get("emoji"):
entry["emoji"] = b["emoji"]
result.append(entry)
print(json.dumps({"ok": True, "bookmarks": result}, indent=2))


def cmd_edit(args):
token = get_bot_token()
params = {
"channel_id": args.channel,
"bookmark_id": args.bookmark_id,
}
if args.title:
params["title"] = args.title
if args.link:
params["link"] = args.link
if args.emoji:
params["emoji"] = args.emoji

resp = slack_api(token, "bookmarks.edit", params)
if not resp.get("ok"):
print(f"Error: bookmarks.edit failed: {resp.get('error', 'unknown')}", file=sys.stderr)
sys.exit(1)

bookmark = resp.get("bookmark", {})
print(json.dumps({
"ok": True,
"bookmark_id": bookmark.get("id"),
"title": bookmark.get("title"),
}, indent=2))


def cmd_remove(args):
token = get_bot_token()
resp = slack_api(token, "bookmarks.remove", {
"channel_id": args.channel,
"bookmark_id": args.bookmark_id,
})
if not resp.get("ok"):
print(f"Error: bookmarks.remove failed: {resp.get('error', 'unknown')}", file=sys.stderr)
sys.exit(1)

print(json.dumps({"ok": True, "removed": args.bookmark_id}, indent=2))


def main():
parser = argparse.ArgumentParser(description="Manage Slack channel bookmarks")
sub = parser.add_subparsers(dest="command", required=True)

# add
p_add = sub.add_parser("add", help="Add a link bookmark")
p_add.add_argument("--channel", required=True, help="Channel ID")
p_add.add_argument("--title", required=True, help="Bookmark title")
p_add.add_argument("--link", required=True, help="URL for the bookmark")
p_add.add_argument("--emoji", help="Emoji icon (e.g. :link:)")

# list
p_list = sub.add_parser("list", help="List bookmarks in a channel")
p_list.add_argument("--channel", required=True, help="Channel ID")

# edit
p_edit = sub.add_parser("edit", help="Edit a bookmark")
p_edit.add_argument("--channel", required=True, help="Channel ID")
p_edit.add_argument("--bookmark-id", required=True, help="Bookmark ID to edit")
p_edit.add_argument("--title", help="New title")
p_edit.add_argument("--link", help="New URL")
p_edit.add_argument("--emoji", help="New emoji")

# remove
p_remove = sub.add_parser("remove", help="Remove a bookmark")
p_remove.add_argument("--channel", required=True, help="Channel ID")
p_remove.add_argument("--bookmark-id", required=True, help="Bookmark ID to remove")

args = parser.parse_args()
{"add": cmd_add, "list": cmd_list, "edit": cmd_edit, "remove": cmd_remove}[args.command](args)


if __name__ == "__main__":
main()
Loading