feat: update user configuration handling to use user key prefix for improved clarity

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2026-01-14 14:11:53 -05:00
parent 32bae38ffd
commit 32172f2fce
5 changed files with 54 additions and 66 deletions

View File

@ -31,10 +31,12 @@ This plugin implements three capabilities to demonstrate the nd-host library:
Configure in the Navidrome UI (Settings → Plugins → discord-rich-presence):
| Key | Description | Example |
|------------|----------------------------------------------------------|--------------------------------|
| `clientid` | Your Discord application ID | `123456789012345678` |
| `users` | Comma-separated list of `username:token` pairs | `alice:token123,bob:token456` |
| Key | Description | Example |
|---------------|-------------------------------------------|--------------------------------|
| `clientid` | Your Discord application ID | `123456789012345678` |
| `user.<name>` | Discord token for the specified user | `user.alice` = `token123` |
Each user is configured as a separate key with the `user.` prefix.
### Getting Configuration Values
@ -43,9 +45,10 @@ Configure in the Navidrome UI (Settings → Plugins → discord-rich-presence):
2. **Discord Token**: This requires extracting your user token from Discord (not recommended for security reasons)
3. **Multiple Users**: Separate user mappings with commas:
3. **Multiple Users**: Add multiple user keys:
```properties
users = "user1:token1,user2:token2"
user.user1 = "token1"
user.user2 = "token2"
```
## Building

View File

@ -11,7 +11,8 @@
//! ```toml
//! [PluginConfig.discord-rich-presence-rs]
//! clientid = "YOUR_DISCORD_APPLICATION_ID"
//! users = "username1:discord_token1,username2:discord_token2"
//! "user.username1" = "discord_token1"
//! "user.username2" = "discord_token2"
//! ```
//!
//! **WARNING**: This plugin is for demonstration purposes only. Storing Discord tokens
@ -47,7 +48,7 @@ nd_pdk::register_websocket_close!(DiscordPlugin);
// ============================================================================
const CLIENT_ID_KEY: &str = "clientid";
const USERS_KEY: &str = "users";
const USER_KEY_PREFIX: &str = "user.";
const PAYLOAD_HEARTBEAT: &str = "heartbeat";
const PAYLOAD_CLEAR_ACTIVITY: &str = "clear-activity";
@ -68,15 +69,14 @@ fn get_config() -> Result<(String, std::collections::HashMap<String, String>), E
.filter(|s| !s.is_empty())
.ok_or_else(|| Error::msg("missing clientid in configuration"))?;
let users_config = config::get(USERS_KEY)?
.filter(|s| !s.is_empty())
.unwrap_or_default();
// Get all user keys with the "user." prefix
let user_keys = config::keys(USER_KEY_PREFIX)?;
let mut users = std::collections::HashMap::new();
for user in users_config.split(',') {
let parts: Vec<&str> = user.split(':').collect();
if parts.len() == 2 {
users.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
for key in user_keys {
let username = key.strip_prefix(USER_KEY_PREFIX).unwrap_or(&key);
if let Some(token) = config::get(&key)?.filter(|s| !s.is_empty()) {
users.insert(username.to_string(), token);
}
}

View File

@ -65,10 +65,12 @@ To work within this model the plugin stores no in-memory state. Connections are
Configure in the Navidrome UI (Settings → Plugins → discord-rich-presence):
| Key | Description | Example |
|------------|----------------------------------------------------------|--------------------------------|
| `clientid` | Your Discord application ID | `123456789012345678` |
| `users` | Comma-separated list of `username:token` pairs | `alice:token123,bob:token456` |
| Key | Description | Example |
|---------------|-------------------------------------------|--------------------------------|
| `clientid` | Your Discord application ID | `123456789012345678` |
| `user.<name>` | Discord token for the specified user | `user.alice` = `token123` |
Each user is configured as a separate key with the `user.` prefix.
## Building

View File

@ -24,8 +24,8 @@ import (
// Configuration keys
const (
clientIDKey = "clientid"
usersKey = "users"
clientIDKey = "clientid"
userKeyPrefix = "user."
)
// discordPlugin implements the scrobbler and scheduler interfaces.
@ -49,20 +49,27 @@ func getConfig() (clientID string, users map[string]string, err error) {
return "", nil, nil
}
cfgUsers, ok := pdk.GetConfig(usersKey)
if !ok || cfgUsers == "" {
// Get all user keys with the "user." prefix
userKeys := host.ConfigKeys(userKeyPrefix)
if len(userKeys) == 0 {
pdk.Log(pdk.LogWarn, "no users configured")
return clientID, nil, nil
}
users = make(map[string]string)
for _, user := range strings.Split(cfgUsers, ",") {
tuple := strings.Split(user, ":")
if len(tuple) != 2 {
return clientID, nil, fmt.Errorf("invalid user config: %s", user)
for _, key := range userKeys {
username := strings.TrimPrefix(key, userKeyPrefix)
token, exists := host.ConfigGet(key)
if exists && token != "" {
users[username] = token
}
users[strings.TrimSpace(tuple[0])] = strings.TrimSpace(tuple[1])
}
if len(users) == 0 {
pdk.Log(pdk.LogWarn, "no users configured")
return clientID, nil, nil
}
return clientID, users, nil
}

View File

@ -28,6 +28,8 @@ var _ = Describe("discordPlugin", func() {
pdk.ResetMock()
host.CacheMock.ExpectedCalls = nil
host.CacheMock.Calls = nil
host.ConfigMock.ExpectedCalls = nil
host.ConfigMock.Calls = nil
host.WebSocketMock.ExpectedCalls = nil
host.WebSocketMock.Calls = nil
host.SchedulerMock.ExpectedCalls = nil
@ -39,7 +41,9 @@ var _ = Describe("discordPlugin", func() {
Describe("getConfig", func() {
It("returns config values when properly set", func() {
pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true)
pdk.PDKMock.On("GetConfig", usersKey).Return("user1:token1,user2:token2", true)
host.ConfigMock.On("Keys", userKeyPrefix).Return([]string{"user.user1", "user.user2"})
host.ConfigMock.On("Get", "user.user1").Return("token1", true)
host.ConfigMock.On("Get", "user.user2").Return("token2", true)
pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe()
clientID, users, err := getConfig()
@ -62,7 +66,7 @@ var _ = Describe("discordPlugin", func() {
It("returns nil users when users not configured", func() {
pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true)
pdk.PDKMock.On("GetConfig", usersKey).Return("", false)
host.ConfigMock.On("Keys", userKeyPrefix).Return([]string{})
pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe()
clientID, users, err := getConfig()
@ -70,16 +74,6 @@ var _ = Describe("discordPlugin", func() {
Expect(clientID).To(Equal("test-client-id"))
Expect(users).To(BeNil())
})
It("returns error for invalid user format", func() {
pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true)
pdk.PDKMock.On("GetConfig", usersKey).Return("invalid-format", true)
pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe()
_, _, err := getConfig()
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid user config"))
})
})
Describe("IsAuthorized", func() {
@ -89,7 +83,8 @@ var _ = Describe("discordPlugin", func() {
It("returns true for authorized user", func() {
pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true)
pdk.PDKMock.On("GetConfig", usersKey).Return("testuser:token123", true)
host.ConfigMock.On("Keys", userKeyPrefix).Return([]string{"user.testuser"})
host.ConfigMock.On("Get", "user.testuser").Return("token123", true)
authorized, err := plugin.IsAuthorized(scrobbler.IsAuthorizedRequest{
Username: "testuser",
@ -100,7 +95,8 @@ var _ = Describe("discordPlugin", func() {
It("returns false for unauthorized user", func() {
pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true)
pdk.PDKMock.On("GetConfig", usersKey).Return("otheruser:token123", true)
host.ConfigMock.On("Keys", userKeyPrefix).Return([]string{"user.otheruser"})
host.ConfigMock.On("Get", "user.otheruser").Return("token123", true)
authorized, err := plugin.IsAuthorized(scrobbler.IsAuthorizedRequest{
Username: "testuser",
@ -108,16 +104,6 @@ var _ = Describe("discordPlugin", func() {
Expect(err).ToNot(HaveOccurred())
Expect(authorized).To(BeFalse())
})
It("returns error when config parsing fails", func() {
pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true)
pdk.PDKMock.On("GetConfig", usersKey).Return("invalid-format", true)
_, err := plugin.IsAuthorized(scrobbler.IsAuthorizedRequest{
Username: "testuser",
})
Expect(err).To(HaveOccurred())
})
})
Describe("NowPlaying", func() {
@ -125,21 +111,10 @@ var _ = Describe("discordPlugin", func() {
pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe()
})
It("returns error when config parsing fails", func() {
pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true)
pdk.PDKMock.On("GetConfig", usersKey).Return("invalid-format", true)
err := plugin.NowPlaying(scrobbler.NowPlayingRequest{
Username: "testuser",
Track: scrobbler.TrackInfo{Title: "Test Song"},
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to get config"))
})
It("returns not authorized error when user not in config", func() {
pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true)
pdk.PDKMock.On("GetConfig", usersKey).Return("otheruser:token", true)
host.ConfigMock.On("Keys", userKeyPrefix).Return([]string{"user.otheruser"})
host.ConfigMock.On("Get", "user.otheruser").Return("token", true)
err := plugin.NowPlaying(scrobbler.NowPlayingRequest{
Username: "testuser",
@ -151,7 +126,8 @@ var _ = Describe("discordPlugin", func() {
It("successfully sends now playing update", func() {
pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true)
pdk.PDKMock.On("GetConfig", usersKey).Return("testuser:test-token", true)
host.ConfigMock.On("Keys", userKeyPrefix).Return([]string{"user.testuser"})
host.ConfigMock.On("Get", "user.testuser").Return("test-token", true)
// Connect mocks (isConnected check via heartbeat)
host.CacheMock.On("GetInt", "discord.seq.testuser").Return(int64(0), false, errors.New("not found"))