Adding Slack modules
This commit is contained in:
301
README.md
301
README.md
@@ -21,6 +21,8 @@ No Elgato software required — communicates directly with the device over USB H
|
||||
- Runs as a systemd user service (Linux) or launchd agent (macOS), starts automatically at login
|
||||
- No Stream Deck app, no Node.js, no Electron
|
||||
|
||||
- **Modules** — define reusable, parameterised commands in `modules.yaml` with Go templates; secrets stay in env vars, config stays in dotfiles. First built-in example: Slack (status, presence, snooze). See [Modules](#modules).
|
||||
|
||||
**Planned:** text/label overlays on keys, multi-page layouts, AUR package — see [Roadmap](#roadmap)
|
||||
|
||||
---
|
||||
@@ -37,13 +39,16 @@ streamdeck-go/
|
||||
├── internal/
|
||||
│ ├── config/
|
||||
│ │ └── config.go # YAML parsing with XDG-aware defaults
|
||||
│ └── device/
|
||||
│ └── streamdeck.go # USB HID communication, image encoding, button reads
|
||||
│ ├── device/
|
||||
│ │ └── streamdeck.go # USB HID communication, image encoding, button reads
|
||||
│ └── modules/
|
||||
│ └── modules.go # Module registry, template resolution, env/expiry helpers
|
||||
├── systemd/
|
||||
│ └── streamdeck-go.service # systemd user service (Linux)
|
||||
├── launchd/
|
||||
│ └── com.woodarddigital.streamdeck-go.plist # launchd agent (macOS)
|
||||
├── config.example.yaml # Starter config (copied to ~/.config on install)
|
||||
├── modules.example.yaml # Example modules file (Slack integration)
|
||||
├── Makefile # build / install / uninstall
|
||||
├── go.mod
|
||||
└── go.sum
|
||||
@@ -53,10 +58,15 @@ streamdeck-go/
|
||||
|
||||
```
|
||||
~/.config/streamdeck-go/config.yaml
|
||||
~/.config/streamdeck-go/modules.yaml (optional)
|
||||
│
|
||||
├── fsnotify watcher ──── file saved? ──▶ cancel ctx → reload config → restart run()
|
||||
├── fsnotify watcher ──── file saved? ──▶ cancel ctx → reload both → restart run()
|
||||
│
|
||||
└──▶ run(ctx)
|
||||
└──▶ run(ctx, registry)
|
||||
│
|
||||
├── module resolution ──▶ key has module/function?
|
||||
│ └──▶ registry.Resolve() → rendered shell command
|
||||
│ (templates expanded, env vars resolved)
|
||||
│
|
||||
├── static icon ──▶ device.SetKeyImage() (scale → flip → JPEG → HID)
|
||||
│
|
||||
@@ -391,6 +401,94 @@ keys:
|
||||
|
||||
---
|
||||
|
||||
### Launching applications
|
||||
|
||||
On Linux, GUI apps are typically launched by their binary name (`firefox`, `ghostty`, `nautilus`). On macOS, apps live in `/Applications/` as `.app` bundles and need to be opened differently.
|
||||
|
||||
#### macOS — `open -a`
|
||||
|
||||
The `open -a` command launches (or focuses) a macOS application by name:
|
||||
|
||||
```yaml
|
||||
keys:
|
||||
0:
|
||||
icon: firefox.png
|
||||
command: "open -a Firefox"
|
||||
1:
|
||||
icon: slack.png
|
||||
command: "open -a Slack"
|
||||
2:
|
||||
icon: ghostty.png
|
||||
command: "open -a Ghostty"
|
||||
3:
|
||||
icon: finder.png
|
||||
command: "open -a Finder ~/Documents" # open Finder to a specific folder
|
||||
4:
|
||||
icon: vscode.png
|
||||
command: "open -a 'Visual Studio Code'" # quote names with spaces
|
||||
```
|
||||
|
||||
**How `open -a` works:**
|
||||
- If the app is already running, it brings it to the front (no duplicate launched).
|
||||
- If the app is not running, it launches it.
|
||||
- The app name matches what's in `/Applications/` minus the `.app` extension.
|
||||
- To find the exact name: `ls /Applications/` or `osascript -e 'tell application "System Events" to get name of every process whose background only is false'`
|
||||
|
||||
**Opening files and URLs:**
|
||||
|
||||
```yaml
|
||||
keys:
|
||||
5:
|
||||
icon: project.png
|
||||
command: "open -a 'Visual Studio Code' ~/Projects/myproject" # open a folder in VS Code
|
||||
6:
|
||||
icon: notes.png
|
||||
command: "open ~/Documents/notes.txt" # opens in default app for .txt
|
||||
7:
|
||||
icon: github.png
|
||||
command: "open https://github.com" # opens in default browser
|
||||
```
|
||||
|
||||
#### Linux — direct binary
|
||||
|
||||
```yaml
|
||||
keys:
|
||||
0:
|
||||
icon: firefox.png
|
||||
command: firefox
|
||||
1:
|
||||
icon: slack.png
|
||||
command: slack
|
||||
2:
|
||||
icon: ghostty.png
|
||||
command: ghostty
|
||||
3:
|
||||
icon: files.png
|
||||
command: nautilus ~/Documents
|
||||
```
|
||||
|
||||
Most Linux desktop apps can also be launched with their `.desktop` file via `gtk-launch`:
|
||||
|
||||
```yaml
|
||||
keys:
|
||||
4:
|
||||
icon: vscode.png
|
||||
command: "gtk-launch code" # uses the .desktop file name (without .desktop)
|
||||
```
|
||||
|
||||
#### Cross-platform keys
|
||||
|
||||
If you use the same config on both platforms, you can use a shell one-liner:
|
||||
|
||||
```yaml
|
||||
keys:
|
||||
0:
|
||||
icon: browser.png
|
||||
command: "if [ \"$(uname)\" = \"Darwin\" ]; then open -a Firefox; else firefox; fi"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Terminal & SSH commands
|
||||
|
||||
Any shell command works — including launching terminals and SSH sessions.
|
||||
@@ -404,7 +502,7 @@ keys:
|
||||
command: ghostty # Linux
|
||||
1:
|
||||
icon: terminal.png
|
||||
command: open -a Terminal # macOS — or: open -a iTerm
|
||||
command: open -a Ghostty # macOS — or: open -a Terminal, open -a iTerm
|
||||
```
|
||||
|
||||
**SSH:**
|
||||
@@ -506,6 +604,199 @@ commands:
|
||||
|
||||
---
|
||||
|
||||
### Modules
|
||||
|
||||
Modules let you define reusable commands in a separate `modules.yaml` file instead of inlining shell commands in your config. This is especially useful for API-driven integrations like Slack where commands are long `curl` calls with tokens and JSON payloads.
|
||||
|
||||
#### How it works
|
||||
|
||||
1. Create `~/.config/streamdeck-go/modules.yaml` (next to your `config.yaml`).
|
||||
2. Define modules with named functions. Each function has an `exec` template and optional default `params`.
|
||||
3. Reference them in `config.yaml` with `module`, `function`, and optional `params` overrides.
|
||||
|
||||
The daemon watches `modules.yaml` for changes alongside `config.yaml` — edits to either file trigger a live reload.
|
||||
|
||||
#### Module file format
|
||||
|
||||
```yaml
|
||||
# ~/.config/streamdeck-go/modules.yaml
|
||||
|
||||
modules:
|
||||
slack:
|
||||
set_status:
|
||||
params:
|
||||
emoji: ":speech_balloon:"
|
||||
text: "In a meeting"
|
||||
expiry: "1h"
|
||||
exec: |
|
||||
curl -s -X POST https://slack.com/api/users.profile.set \
|
||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"profile":{"status_emoji":"{{.emoji}}","status_text":"{{.text}}","status_expiration":{{expiry .expiry}}}}'
|
||||
|
||||
clear_status:
|
||||
exec: |
|
||||
curl -s -X POST https://slack.com/api/users.profile.set \
|
||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"profile":{"status_emoji":"","status_text":"","status_expiration":0}}'
|
||||
|
||||
set_presence:
|
||||
params:
|
||||
presence: "away"
|
||||
exec: |
|
||||
curl -s -X POST https://slack.com/api/users.setPresence \
|
||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"presence":"{{.presence}}"}'
|
||||
|
||||
snooze:
|
||||
params:
|
||||
minutes: "60"
|
||||
exec: |
|
||||
curl -s -X POST https://slack.com/api/dnd.setSnooze \
|
||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"num_minutes":{{.minutes}}}'
|
||||
|
||||
end_snooze:
|
||||
exec: |
|
||||
curl -s -X POST https://slack.com/api/dnd.endSnooze \
|
||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}"
|
||||
```
|
||||
|
||||
A full example is included at [`modules.example.yaml`](modules.example.yaml) in the repo root.
|
||||
|
||||
#### Using modules in config.yaml
|
||||
|
||||
Reference a module function instead of writing inline commands:
|
||||
|
||||
```yaml
|
||||
keys:
|
||||
# Set Slack status to "In a meeting" for 1 hour (uses module defaults)
|
||||
10:
|
||||
icon: meeting.png
|
||||
module: slack
|
||||
function: set_status
|
||||
|
||||
# Override default params — different emoji, text, and duration
|
||||
11:
|
||||
icon: lunch.png
|
||||
module: slack
|
||||
function: set_status
|
||||
params:
|
||||
emoji: ":knife_fork_plate:"
|
||||
text: "Out to lunch"
|
||||
expiry: "30m"
|
||||
|
||||
# Clear Slack status
|
||||
12:
|
||||
icon: clear-status.png
|
||||
module: slack
|
||||
function: clear_status
|
||||
|
||||
# Go away / come back
|
||||
13:
|
||||
icon: away.png
|
||||
module: slack
|
||||
function: set_presence
|
||||
params:
|
||||
presence: "away"
|
||||
|
||||
14:
|
||||
icon: active.png
|
||||
module: slack
|
||||
function: set_presence
|
||||
params:
|
||||
presence: "auto"
|
||||
|
||||
# Snooze notifications for 60 minutes
|
||||
15:
|
||||
icon: snooze.png
|
||||
module: slack
|
||||
function: snooze
|
||||
|
||||
# End snooze
|
||||
16:
|
||||
icon: unsnooze.png
|
||||
module: slack
|
||||
function: end_snooze
|
||||
```
|
||||
|
||||
Poll commands also support modules — use `module`, `function`, and `params` inside the `poll` block:
|
||||
|
||||
```yaml
|
||||
keys:
|
||||
17:
|
||||
icon_true: dnd-on.png
|
||||
icon_false: dnd-off.png
|
||||
module: slack
|
||||
function: snooze
|
||||
poll:
|
||||
module: slack
|
||||
function: check_dnd
|
||||
interval: 10s
|
||||
match: "snooze_enabled.*true"
|
||||
```
|
||||
|
||||
#### Template helpers
|
||||
|
||||
Two helpers are available in `exec` templates:
|
||||
|
||||
| Helper | Usage | Description |
|
||||
|---|---|---|
|
||||
| `env` | `{{env "VAR_NAME"}}` | Returns the value of an environment variable |
|
||||
| `expiry` | `{{expiry .duration}}` | Converts a Go duration string (e.g. `"1h"`, `"30m"`) to a Unix epoch timestamp. `"0"` or `""` returns `"0"` (no expiry) |
|
||||
|
||||
#### Secrets and tokens
|
||||
|
||||
**Tokens and secrets must never go in `modules.yaml` or `config.yaml`.** Use the `{{env "VAR_NAME"}}` template helper to read them from environment variables at runtime.
|
||||
|
||||
**Setting up your Slack token:**
|
||||
|
||||
1. Create a Slack app at [api.slack.com/apps](https://api.slack.com/apps) with these OAuth scopes:
|
||||
- `users.profile:write` — set/clear status
|
||||
- `users:write` — set presence (away/auto)
|
||||
- `dnd:write` — snooze/unsnooze notifications
|
||||
|
||||
2. Install the app to your workspace and copy the **User OAuth Token** (`xoxp-...`).
|
||||
|
||||
3. Export the token in your shell profile (`~/.zshrc`, `~/.bash_profile`, etc.):
|
||||
|
||||
```bash
|
||||
export SLACK_TOKEN="xoxp-your-token-here"
|
||||
```
|
||||
|
||||
4. Make sure the service can see the variable:
|
||||
|
||||
**macOS** — add to `~/Library/LaunchAgents/com.woodarddigital.streamdeck-go.plist` inside the `<dict>` block:
|
||||
```xml
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>SLACK_TOKEN</key>
|
||||
<string>xoxp-your-token-here</string>
|
||||
</dict>
|
||||
```
|
||||
|
||||
**Linux** — import into the systemd user environment:
|
||||
```bash
|
||||
systemctl --user import-environment SLACK_TOKEN
|
||||
```
|
||||
|
||||
Or add an `Environment=` line via `systemctl --user edit streamdeck-go`:
|
||||
```ini
|
||||
[Service]
|
||||
Environment=SLACK_TOKEN=xoxp-your-token-here
|
||||
```
|
||||
|
||||
> **Security notes:**
|
||||
> - The `modules.yaml` and `config.yaml` files can safely live in your dotfiles repo — they contain no secrets.
|
||||
> - On Linux, the systemd override file (`~/.config/systemd/user/streamdeck-go.service.d/override.conf`) is user-readable only (mode 600). Still, prefer `import-environment` over hardcoding tokens in unit files when possible.
|
||||
> - On macOS, launchd plist files in `~/Library/LaunchAgents/` are user-readable only by default.
|
||||
> - Never commit tokens to git. Add `.env` files to `.gitignore` if you use one.
|
||||
|
||||
---
|
||||
|
||||
### Supported icon formats
|
||||
|
||||
| Format | Notes |
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/WoodardDigital/streamdeck-go/internal/config"
|
||||
"github.com/WoodardDigital/streamdeck-go/internal/device"
|
||||
"github.com/WoodardDigital/streamdeck-go/internal/modules"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/srwiley/oksvg"
|
||||
"github.com/srwiley/rasterx"
|
||||
@@ -51,6 +52,11 @@ func main() {
|
||||
log.Fatalf("config: %v", err)
|
||||
}
|
||||
|
||||
reg, err := modules.LoadRegistry(config.ModulesPath(*cfgPath))
|
||||
if err != nil {
|
||||
log.Fatalf("modules: %v", err)
|
||||
}
|
||||
|
||||
// Open device — if not present at startup, wait for it.
|
||||
sd := mustConnect(cfg.Device.VendorID, cfg.Device.ProductID)
|
||||
|
||||
@@ -61,7 +67,7 @@ func main() {
|
||||
|
||||
startRun := func() {
|
||||
go func() {
|
||||
deviceDied := run(ctx, sd, cfg)
|
||||
deviceDied := run(ctx, sd, cfg, reg)
|
||||
runDone <- deviceDied
|
||||
}()
|
||||
}
|
||||
@@ -91,13 +97,15 @@ func main() {
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
startRun()
|
||||
|
||||
// Config file changed — reload and restart run().
|
||||
// Config file or modules file changed — reload and restart run().
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
if filepath.Clean(event.Name) != filepath.Clean(*cfgPath) {
|
||||
cfgChanged := filepath.Clean(event.Name) == filepath.Clean(*cfgPath)
|
||||
modChanged := filepath.Clean(event.Name) == filepath.Clean(config.ModulesPath(*cfgPath))
|
||||
if !cfgChanged && !modChanged {
|
||||
continue
|
||||
}
|
||||
if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) {
|
||||
@@ -114,6 +122,12 @@ func main() {
|
||||
} else {
|
||||
cfg = newCfg
|
||||
}
|
||||
newReg, err := modules.LoadRegistry(config.ModulesPath(*cfgPath))
|
||||
if err != nil {
|
||||
log.Printf("reload: bad modules: %v — keeping current modules", err)
|
||||
} else {
|
||||
reg = newReg
|
||||
}
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
startRun()
|
||||
|
||||
@@ -129,7 +143,7 @@ func main() {
|
||||
|
||||
// run initialises the deck and handles button presses until ctx is cancelled
|
||||
// or the device dies. Returns true if the device died, false if ctx was cancelled.
|
||||
func run(ctx context.Context, sd *device.StreamDeck, cfg *config.Config) (deviceDied bool) {
|
||||
func run(ctx context.Context, sd *device.StreamDeck, cfg *config.Config, reg *modules.Registry) (deviceDied bool) {
|
||||
// Recover from any panic inside this goroutine tree.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -157,6 +171,24 @@ func run(ctx context.Context, sd *device.StreamDeck, cfg *config.Config) (device
|
||||
triggers := make(map[int]chan struct{})
|
||||
|
||||
for keyIdx, keyCfg := range cfg.Keys {
|
||||
// Resolve module-based commands to shell strings before any other logic.
|
||||
if keyCfg.Module != "" {
|
||||
resolved, err := reg.Resolve(keyCfg.Module, keyCfg.Function, keyCfg.Params)
|
||||
if err != nil {
|
||||
log.Printf("key %d: module: %v", keyIdx, err)
|
||||
continue
|
||||
}
|
||||
keyCfg.Command = resolved
|
||||
}
|
||||
if keyCfg.Poll != nil && keyCfg.Poll.Module != "" {
|
||||
resolved, err := reg.Resolve(keyCfg.Poll.Module, keyCfg.Poll.Function, keyCfg.Poll.Params)
|
||||
if err != nil {
|
||||
log.Printf("key %d: poll module: %v", keyIdx, err)
|
||||
continue
|
||||
}
|
||||
keyCfg.Poll.Command = resolved
|
||||
}
|
||||
|
||||
// Toggle/status key: managed by a polling goroutine.
|
||||
if keyCfg.Poll != nil {
|
||||
if keyCfg.IconTrue == "" || keyCfg.IconFalse == "" {
|
||||
|
||||
@@ -13,6 +13,11 @@ type PollConfig struct {
|
||||
Command string `yaml:"command"` // shell command whose output is checked
|
||||
Interval string `yaml:"interval"` // how often to poll, e.g. "2s" (default: "2s")
|
||||
Match string `yaml:"match"` // substring to find in output → "on" state; omit to use exit code 0
|
||||
|
||||
// Module-based alternative to Command: resolved to a shell string at startup.
|
||||
Module string `yaml:"module"`
|
||||
Function string `yaml:"function"`
|
||||
Params map[string]string `yaml:"params"`
|
||||
}
|
||||
|
||||
// KeyConfig defines what a single Stream Deck key does.
|
||||
@@ -24,6 +29,16 @@ type KeyConfig struct {
|
||||
IconTrue string `yaml:"icon_true"` // icon when poll match is true
|
||||
IconFalse string `yaml:"icon_false"` // icon when poll match is false
|
||||
Poll *PollConfig `yaml:"poll"`
|
||||
|
||||
// Module-based alternative to Command: resolved to a shell string at startup.
|
||||
Module string `yaml:"module"`
|
||||
Function string `yaml:"function"`
|
||||
Params map[string]string `yaml:"params"`
|
||||
}
|
||||
|
||||
// ModulesPath returns the path to the modules.yaml file next to the given config file.
|
||||
func ModulesPath(cfgPath string) string {
|
||||
return filepath.Join(filepath.Dir(cfgPath), "modules.yaml")
|
||||
}
|
||||
|
||||
// Config is the top-level structure of the YAML config file.
|
||||
|
||||
117
internal/modules/modules.go
Normal file
117
internal/modules/modules.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// FunctionDef is a single callable function within a module.
|
||||
type FunctionDef struct {
|
||||
Exec string `yaml:"exec"` // Go text/template that renders to a shell command
|
||||
Params map[string]string `yaml:"params"` // default parameter values
|
||||
}
|
||||
|
||||
// ModuleDef maps function names to their definitions.
|
||||
type ModuleDef map[string]FunctionDef
|
||||
|
||||
// Registry holds all loaded modules.
|
||||
type Registry struct {
|
||||
Modules map[string]ModuleDef `yaml:"modules"`
|
||||
}
|
||||
|
||||
// LoadRegistry reads a modules.yaml file.
|
||||
// A missing file is not an error — returns an empty Registry so callers don't need to
|
||||
// special-case systems without a modules.yaml.
|
||||
func LoadRegistry(path string) (*Registry, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return &Registry{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read modules %q: %w", path, err)
|
||||
}
|
||||
var r Registry
|
||||
if err := yaml.Unmarshal(data, &r); err != nil {
|
||||
return nil, fmt.Errorf("parse modules %q: %w", path, err)
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// Resolve looks up module/function, merges params (defaults overridden by overrides),
|
||||
// and renders the exec template. Returns the shell command string ready for sh -c.
|
||||
func (r *Registry) Resolve(module, function string, overrides map[string]string) (string, error) {
|
||||
if r == nil || r.Modules == nil {
|
||||
return "", fmt.Errorf("no modules loaded")
|
||||
}
|
||||
mod, ok := r.Modules[module]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unknown module %q", module)
|
||||
}
|
||||
fn, ok := mod[function]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("module %q has no function %q", module, function)
|
||||
}
|
||||
|
||||
// Merge: start with defaults, apply overrides on top.
|
||||
params := make(map[string]string, len(fn.Params)+len(overrides))
|
||||
for k, v := range fn.Params {
|
||||
params[k] = v
|
||||
}
|
||||
for k, v := range overrides {
|
||||
params[k] = v
|
||||
}
|
||||
|
||||
tmpl, err := template.New("exec").Funcs(funcMap()).Parse(fn.Exec)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("module %q function %q: parse template: %w", module, function, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, params); err != nil {
|
||||
return "", fmt.Errorf("module %q function %q: render template: %w", module, function, err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Execute resolves the module/function and runs it via sh -c.
|
||||
func (r *Registry) Execute(module, function string, overrides map[string]string) error {
|
||||
shell, err := r.Resolve(module, function, overrides)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := exec.Command("sh", "-c", shell)
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
return c.Run()
|
||||
}
|
||||
|
||||
// funcMap returns the template helper functions available in exec templates.
|
||||
func funcMap() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
// env returns the value of an environment variable.
|
||||
// Use this to keep secrets (tokens, passwords) out of modules.yaml.
|
||||
"env": os.Getenv,
|
||||
|
||||
// expiry converts a duration string like "+1h" to a Unix epoch timestamp string.
|
||||
// Pass "" or "0" to get "0" (no expiry). Supports Go duration syntax (e.g. "30m", "2h").
|
||||
// Note: time.ParseDuration does not support days (d) or weeks (w).
|
||||
"expiry": func(s string) string {
|
||||
if s == "" || s == "0" {
|
||||
return "0"
|
||||
}
|
||||
d, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return "0"
|
||||
}
|
||||
return strconv.FormatInt(time.Now().Add(d).Unix(), 10)
|
||||
},
|
||||
}
|
||||
}
|
||||
58
modules.example.yaml
Normal file
58
modules.example.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
# Example modules.yaml — copy to ~/.config/streamdeck-go/modules.yaml
|
||||
#
|
||||
# Required Slack token scopes: users.profile:write, users:write, dnd:write
|
||||
# Export your token: export SLACK_TOKEN="xoxp-..."
|
||||
|
||||
modules:
|
||||
slack:
|
||||
set_status:
|
||||
params:
|
||||
emoji: ":speech_balloon:"
|
||||
text: "In a meeting"
|
||||
expiry: "1h"
|
||||
exec: |
|
||||
curl -s -X POST https://slack.com/api/users.profile.set \
|
||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"profile":{"status_emoji":"{{.emoji}}","status_text":"{{.text}}","status_expiration":{{expiry .expiry}}}}'
|
||||
|
||||
clear_status:
|
||||
exec: |
|
||||
curl -s -X POST https://slack.com/api/users.profile.set \
|
||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"profile":{"status_emoji":"","status_text":"","status_expiration":0}}'
|
||||
|
||||
set_presence:
|
||||
params:
|
||||
presence: "away"
|
||||
exec: |
|
||||
curl -s -X POST https://slack.com/api/users.setPresence \
|
||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"presence":"{{.presence}}"}'
|
||||
|
||||
snooze:
|
||||
params:
|
||||
minutes: "60"
|
||||
exec: |
|
||||
curl -s -X POST https://slack.com/api/dnd.setSnooze \
|
||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"num_minutes":{{.minutes}}}'
|
||||
|
||||
go_offline:
|
||||
exec: |
|
||||
curl -s -X POST https://slack.com/api/users.setPresence \
|
||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"presence":"away"}' && \
|
||||
curl -s -X POST https://slack.com/api/users.profile.set \
|
||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"profile":{"status_emoji":"","status_text":"","status_expiration":0}}'
|
||||
|
||||
end_snooze:
|
||||
exec: |
|
||||
curl -s -X POST https://slack.com/api/dnd.endSnooze \
|
||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}"
|
||||
Reference in New Issue
Block a user