Commit32be4260Recorded25 Mar 2026Repositorytube

Use sigil-oauth for YouTube and Twitch token management

Message

Update require-youtube-client and require-twitch-client with priority-based auth: direct token > OAuth stored tokens > API key. OAuth mode auto-refreshes expired tokens and persists new ones.

Add tube-config fields for YOUTUBECLIENTID/SECRET and TWITCHCLIENTSECRET. Add 'tube auth' CLI command for running the OAuth authorization flow (opens browser, stores tokens).

Add sigil-oauth as dependency. Update README with OAuth setup docs.

Note: tube has a pre-existing module dependency cycle that prevents building — this is not caused by the oauth integration.

Changed
 README.md            | 30 +++++++++++++++++++++++++++++-
 dev-redirects.sgl    |  5 ++++-
 package.sgl          |  7 ++++++-
 src/tube/config.sgl  | 34 ++++++++++++++++++++++++++--------
 src/tube/main.sgl    | 50 +++++++++++++++++++++++++++++++++++++++++++++++++-
 src/tube/twitch.sgl  | 44 ++++++++++++++++++++++++++++++++++++++------
 src/tube/youtube.sgl | 49 +++++++++++++++++++++++++++++++++++++++++++------
 7 files changed, 195 insertions(+), 24 deletions(-)
Diff
README.mdmodified
@@ -71,7 +71,26 @@ This starts an MCP server exposing the following tools:
71
72
## Configuration
73
74
Set these environment variables:
+74
### OAuth Mode (Recommended)
+75
+76
Set client credentials and use `tube auth` to authorize:
+77
+78
```bash
+79
export YOUTUBE_CLIENT_ID="your-google-client-id"
+80
export YOUTUBE_CLIENT_SECRET="your-google-client-secret"
+81
export TWITCH_CLIENT_ID="your-twitch-client-id"
+82
export TWITCH_CLIENT_SECRET="your-twitch-client-secret"
+83
+84
# Authorize each platform (opens browser, stores tokens)
+85
tube auth youtube
+86
tube auth twitch
+87
```
+88
+89
Tokens are stored in `~/.config/sigil/<service>/tokens.json` and automatically refreshed when expired.
+90
+91
### Direct Token Mode (Legacy)
+92
+93
For manual token management, set access tokens directly:
94
95
| Variable | Required | Description |
96
|----------|----------|-------------|
@@ -81,6 +100,15 @@ Set these environment variables:
100
| `TWITCH_ACCESS_TOKEN` | For Twitch | Twitch OAuth2 access token |
101
| `TUBE_DEFAULT_PLATFORM` | No | Default platform: `youtube`, `twitch`, or `both` (default: `both`) |
102
+103
### OAuth Environment Variables
+104
+105
| Variable | Description |
+106
|----------|-------------|
+107
| `YOUTUBE_CLIENT_ID` | Google Cloud OAuth2 client ID |
+108
| `YOUTUBE_CLIENT_SECRET` | Google Cloud OAuth2 client secret |
+109
| `TWITCH_CLIENT_ID` | Twitch application client ID |
+110
| `TWITCH_CLIENT_SECRET` | Twitch application client secret |
+111
112
You only need credentials for the platforms you want to use. If you only have YouTube credentials, Twitch operations will gracefully report that credentials are missing, and vice versa.
113
114
## Architecture
dev-redirects.sglmodified
@@ -9,4 +9,7 @@
9
use: (from-path dir: "../sigil-youtube"))
10
(for-repo
11
url: "codeberg:sigil/sigil-twitch"
12
use: (from-path dir: "../sigil-twitch"))))
+12
use: (from-path dir: "../sigil-twitch"))
+13
(for-repo
+14
url: "codeberg:sigil/sigil-oauth"
+15
use: (from-path dir: "../sigil-oauth"))))
package.sglmodified
@@ -7,6 +7,7 @@
7
(define sigil-repo "codeberg:sigil/sigil")
8
(define youtube-repo "codeberg:sigil/sigil-youtube")
9
(define twitch-repo "codeberg:sigil/sigil-twitch")
+10
(define oauth-repo "codeberg:sigil/sigil-oauth")
11
12
(package
13
name: "tube"
@@ -38,7 +39,10 @@
39
mcp-server: #{ name: "tube"
40
args: #["serve"]
41
secret-env: #{ YOUTUBE_ACCESS_TOKEN: #t
+42
YOUTUBE_CLIENT_ID: #t
+43
YOUTUBE_CLIENT_SECRET: #t
44
TWITCH_CLIENT_ID: #t
+45
TWITCH_CLIENT_SECRET: #t
46
TWITCH_ACCESS_TOKEN: #t } })
47
48
dependencies: (list
@@ -51,7 +55,8 @@
55
(from-git url: sigil-repo package: "sigil-log")
56
(from-git url: sigil-repo package: "sigil-args")
57
(from-git url: youtube-repo package: "sigil-youtube")
54
(from-git url: twitch-repo package: "sigil-twitch"))
+58
(from-git url: twitch-repo package: "sigil-twitch")
+59
(from-git url: oauth-repo package: "sigil-oauth"))
60
61
tasks: (list
62
(task
src/tube/config.sglmodified
@@ -1,9 +1,11 @@
1
;;; (tube config) - Configuration from environment variables.
2
;;;
3
;;; Reads YouTube and Twitch credentials from environment.
4
;;; YOUTUBE_ACCESS_TOKEN / YOUTUBE_API_KEY for YouTube,
5
;;; TWITCH_CLIENT_ID / TWITCH_ACCESS_TOKEN for Twitch,
6
;;; TUBE_DEFAULT_PLATFORM for default platform preference.
+4
;;; Supports both direct access tokens and OAuth client credentials.
+5
;;;
+6
;;; Direct tokens: YOUTUBE_ACCESS_TOKEN, TWITCH_ACCESS_TOKEN
+7
;;; OAuth flow: YOUTUBE_CLIENT_ID/SECRET, TWITCH_CLIENT_ID/SECRET
+8
;;; (tokens stored in ~/.config/sigil/<service>/tokens.json)
9
10
(define-library (tube config)
11
(import (sigil core)
@@ -14,7 +16,10 @@
16
tube-config?
17
tube-config-youtube-access-token
18
tube-config-youtube-api-key
+19
tube-config-youtube-client-id
+20
tube-config-youtube-client-secret
21
tube-config-twitch-client-id
+22
tube-config-twitch-client-secret
23
tube-config-twitch-access-token
24
tube-config-default-platform
25
@@ -24,22 +29,35 @@
29
(define-struct tube-config
30
(youtube-access-token default: #f)
31
(youtube-api-key default: #f)
+32
(youtube-client-id default: #f)
+33
(youtube-client-secret default: #f)
34
(twitch-client-id default: #f)
+35
(twitch-client-secret default: #f)
36
(twitch-access-token default: #f)
37
(default-platform default: "both"))
38
39
;;; Load configuration from environment variables.
40
;;;
33
;;; - YOUTUBE_ACCESS_TOKEN — OAuth2 token for YouTube (required for mutations)
34
;;; - YOUTUBE_API_KEY — API key for YouTube read-only operations
35
;;; - TWITCH_CLIENT_ID — Twitch application client ID
36
;;; - TWITCH_ACCESS_TOKEN — Twitch OAuth2 token
37
;;; - TUBE_DEFAULT_PLATFORM — default platform: "youtube", "twitch", or "both"
+41
;;; Direct token mode:
+42
;;; YOUTUBE_ACCESS_TOKEN — OAuth2 token for YouTube (required for mutations)
+43
;;; YOUTUBE_API_KEY — API key for YouTube read-only operations
+44
;;; TWITCH_ACCESS_TOKEN — Twitch OAuth2 token
+45
;;;
+46
;;; OAuth mode (tokens auto-managed via sigil-oauth):
+47
;;; YOUTUBE_CLIENT_ID — Google Cloud OAuth2 client ID
+48
;;; YOUTUBE_CLIENT_SECRET — Google Cloud OAuth2 client secret
+49
;;; TWITCH_CLIENT_ID — Twitch application client ID
+50
;;; TWITCH_CLIENT_SECRET — Twitch application client secret
+51
;;;
+52
;;; TUBE_DEFAULT_PLATFORM — default platform: "youtube", "twitch", or "both"
53
(define (load-tube-config)
54
(tube-config
55
youtube-access-token: (getenv "YOUTUBE_ACCESS_TOKEN")
56
youtube-api-key: (getenv "YOUTUBE_API_KEY")
+57
youtube-client-id: (getenv "YOUTUBE_CLIENT_ID")
+58
youtube-client-secret: (getenv "YOUTUBE_CLIENT_SECRET")
59
twitch-client-id: (getenv "TWITCH_CLIENT_ID")
+60
twitch-client-secret: (getenv "TWITCH_CLIENT_SECRET")
61
twitch-access-token: (getenv "TWITCH_ACCESS_TOKEN")
62
default-platform: (getenv-default "TUBE_DEFAULT_PLATFORM" "both")))
63
src/tube/main.sglmodified
@@ -10,6 +10,10 @@
10
(sigil process)
11
(sigil args)
12
(sigil mcp server)
+13
(sigil time)
+14
(sigil oauth server)
+15
(youtube oauth)
+16
(twitch oauth)
17
(tube config)
18
(tube youtube)
19
(tube twitch)
@@ -110,6 +114,45 @@
114
(display (tool-status config params))
115
(newline)))
116
+117
;; ============================================================
+118
;; Auth Command
+119
;; ============================================================
+120
+121
(define (cli-auth-youtube config)
+122
(let ((client-id (tube-config-youtube-client-id config))
+123
(client-secret (tube-config-youtube-client-secret config)))
+124
(unless (and client-id client-secret)
+125
(display "Error: YOUTUBE_CLIENT_ID and YOUTUBE_CLIENT_SECRET must be set.\n")
+126
(exit 1))
+127
(let* ((oauth-cfg (youtube-oauth-config client-id client-secret))
+128
(tokens (oauth-run-authorization-flow oauth-cfg
+129
(current-second))))
+130
(youtube-save-tokens tokens)
+131
(display "YouTube authorization successful. Tokens saved.\n"))))
+132
+133
(define (cli-auth-twitch config)
+134
(let ((client-id (tube-config-twitch-client-id config))
+135
(client-secret (tube-config-twitch-client-secret config)))
+136
(unless (and client-id client-secret)
+137
(display "Error: TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET must be set.\n")
+138
(exit 1))
+139
(let* ((oauth-cfg (twitch-oauth-config client-id client-secret))
+140
(tokens (oauth-run-authorization-flow oauth-cfg
+141
(current-second))))
+142
(twitch-save-tokens tokens)
+143
(display "Twitch authorization successful. Tokens saved.\n"))))
+144
+145
(define (cli-auth config opts args)
+146
(let ((platform (if (pair? args) (car args) #f)))
+147
(cond
+148
((equal? platform "youtube") (cli-auth-youtube config))
+149
((equal? platform "twitch") (cli-auth-twitch config))
+150
(else
+151
(display "Usage: tube auth <youtube|twitch>\n\n")
+152
(display "Run the OAuth authorization flow for a platform.\n")
+153
(display "Requires CLIENT_ID and CLIENT_SECRET env vars.\n")
+154
(display "Tokens are stored in ~/.config/sigil/<service>/tokens.json\n")))))
+155
156
;; ============================================================
157
;; CLI Definition
158
;; ============================================================
@@ -191,7 +234,12 @@
234
name: "status"
235
description: "Platform overview"
236
options: (list (platform-option default-plat))
194
handler: (lambda (opts args) (cli-status config opts)))))))
+237
handler: (lambda (opts args) (cli-status config opts)))
+238
+239
(command
+240
name: "auth"
+241
description: "Authorize a platform via OAuth"
+242
handler: (lambda (opts args) (cli-auth config opts args)))))))
243
244
;; ============================================================
245
;; Entry Point
src/tube/twitch.sglmodified
@@ -3,6 +3,10 @@
3
;;; Channel info, stream scheduling, analytics (followers, subs, clips),
4
;;; and chat operations. All operations that need the broadcaster ID
5
;;; resolve it once via with-twitch-context to avoid repeated API calls.
+6
;;;
+7
;;; Supports two authentication modes:
+8
;;; 1. Direct access token via TWITCH_CLIENT_ID + TWITCH_ACCESS_TOKEN env vars
+9
;;; 2. OAuth via TWITCH_CLIENT_ID + TWITCH_CLIENT_SECRET with auto-managed tokens
10
11
(define-library (tube twitch)
12
(import (sigil core)
@@ -10,10 +14,12 @@
14
(sigil dict)
15
(sigil struct)
16
(sigil json)
+17
(sigil time)
18
(twitch)
19
(twitch schedule)
20
(twitch analytics)
21
(twitch chat)
+22
(twitch oauth)
23
(tube config))
24
(export require-twitch-client
25
with-twitch-context
@@ -32,14 +38,40 @@
38
format-twitch-video)
39
(begin
40
35
;;; Build a twitch-client from config, raising if no credentials.
+41
;;; Build a twitch-client from config.
+42
;;;
+43
;;; Priority:
+44
;;; 1. If TWITCH_ACCESS_TOKEN is set, use it with TWITCH_CLIENT_ID (legacy)
+45
;;; 2. If TWITCH_CLIENT_ID + TWITCH_CLIENT_SECRET are set, use OAuth
+46
;;; with stored tokens (auto-refreshes expired tokens)
+47
;;; 3. Otherwise, raise an error
48
(define (require-twitch-client config)
49
(let ((client-id (tube-config-twitch-client-id config))
38
(token (tube-config-twitch-access-token config)))
39
(unless (and client-id token)
40
(error "Twitch credentials not set. Set TWITCH_CLIENT_ID and TWITCH_ACCESS_TOKEN."))
41
(twitch-client client-id: client-id
42
access-token: token)))
+50
(token (tube-config-twitch-access-token config))
+51
(client-secret (tube-config-twitch-client-secret config)))
+52
(cond
+53
;; Direct token mode — needs both client-id and token
+54
((and client-id token)
+55
(twitch-client client-id: client-id
+56
access-token: token))
+57
;; OAuth mode — client-id + secret, load stored tokens
+58
((and client-id client-secret)
+59
(let* ((oauth-cfg (twitch-oauth-config client-id client-secret))
+60
(tokens (twitch-load-tokens)))
+61
(if (not tokens)
+62
(error (string-append
+63
"No stored Twitch tokens found. "
+64
"Run 'tube auth twitch' to authorize, "
+65
"or set TWITCH_ACCESS_TOKEN directly.")))
+66
(let-values (((client fresh-tokens)
+67
(twitch-ensure-client oauth-cfg tokens
+68
(current-second))))
+69
client)))
+70
(else
+71
(error (string-append
+72
"Twitch credentials not set. "
+73
"Set TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET for OAuth, "
+74
"or TWITCH_CLIENT_ID and TWITCH_ACCESS_TOKEN for direct mode."))))))
75
76
;;; Resolve client + broadcaster ID together. Caches the broadcaster ID
77
;;; lookup so multiple operations in the same tool handler share one call.
src/tube/youtube.sglmodified
@@ -2,6 +2,10 @@
2
;;;
3
;;; Upload with metadata, video listing with stats, analytics dashboard,
4
;;; livestream management, and playlist management.
+5
;;;
+6
;;; Supports two authentication modes:
+7
;;; 1. Direct access token via YOUTUBE_ACCESS_TOKEN env var
+8
;;; 2. OAuth via YOUTUBE_CLIENT_ID/SECRET with auto-managed token storage
9
10
(define-library (tube youtube)
11
(import (sigil core)
@@ -9,11 +13,13 @@
13
(sigil dict)
14
(sigil struct)
15
(sigil json)
+16
(sigil time)
17
(youtube)
18
(youtube upload)
19
(youtube analytics)
20
(youtube live)
21
(youtube playlist)
+22
(youtube oauth)
23
(tube config))
24
(export require-youtube-client
25
tube-upload-video
@@ -33,14 +39,45 @@
39
youtube-video-title)
40
(begin
41
36
;;; Build a youtube-client from config, raising if no credentials.
+42
;;; Build a youtube-client from config.
+43
;;;
+44
;;; Priority:
+45
;;; 1. If YOUTUBE_ACCESS_TOKEN is set, use it directly (legacy mode)
+46
;;; 2. If YOUTUBE_CLIENT_ID/SECRET are set, use OAuth with stored tokens
+47
;;; (auto-refreshes expired tokens)
+48
;;; 3. If YOUTUBE_API_KEY is set, use it for read-only access
+49
;;; 4. Otherwise, raise an error
50
(define (require-youtube-client config)
51
(let ((token (tube-config-youtube-access-token config))
39
(key (tube-config-youtube-api-key config)))
40
(unless (or token key)
41
(error "No YouTube credentials set. Set YOUTUBE_ACCESS_TOKEN or YOUTUBE_API_KEY."))
42
(youtube-client access-token: token
43
api-key: key)))
+52
(key (tube-config-youtube-api-key config))
+53
(client-id (tube-config-youtube-client-id config))
+54
(client-secret (tube-config-youtube-client-secret config)))
+55
(cond
+56
;; Direct token — use as-is
+57
(token
+58
(youtube-client access-token: token
+59
api-key: key))
+60
;; OAuth client credentials — load/refresh stored tokens
+61
((and client-id client-secret)
+62
(let* ((oauth-cfg (youtube-oauth-config client-id client-secret))
+63
(tokens (youtube-load-tokens)))
+64
(if (not tokens)
+65
(error (string-append
+66
"No stored YouTube tokens found. "
+67
"Run 'tube auth youtube' to authorize, "
+68
"or set YOUTUBE_ACCESS_TOKEN directly.")))
+69
(let-values (((client fresh-tokens)
+70
(youtube-ensure-client oauth-cfg tokens
+71
(current-second))))
+72
client)))
+73
;; API key only — read-only
+74
(key
+75
(youtube-client api-key: key))
+76
(else
+77
(error (string-append
+78
"No YouTube credentials set. "
+79
"Set YOUTUBE_CLIENT_ID and YOUTUBE_CLIENT_SECRET for OAuth, "
+80
"or YOUTUBE_ACCESS_TOKEN for direct token mode."))))))
81
82
;; ============================================================
83
;; Upload