Files
PinePods-nix/PinePods-0.8.2/gpodder-api/internal/api/settings.go
2026-03-03 10:57:43 -05:00

992 lines
29 KiB
Go

package api
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
"unicode/utf8"
"pinepods/gpodder-api/internal/db"
"pinepods/gpodder-api/internal/models"
"github.com/gin-gonic/gin"
)
// Constants for settings
const (
MAX_SETTING_KEY_LENGTH = 255
MAX_SETTING_VALUE_LENGTH = 8192
MAX_SETTINGS_PER_REQUEST = 50
)
// Known settings that trigger behavior
var knownSettings = map[string]map[string]bool{
"account": {
"public_profile": true,
"store_user_agent": true,
"public_subscriptions": true,
"color_theme": true,
"default_subscribe_all": true,
},
"episode": {
"is_favorite": true,
"played": true,
"current_position": true,
},
"podcast": {
"public_subscription": true,
"auto_download": true,
"episode_sort": true,
},
"device": {
"auto_update": true,
"update_interval": true,
"wifi_only_downloads": true,
"max_episodes_per_feed": true,
},
}
// Validation interfaces and functions
// ValueValidator defines interface for validating settings values
type ValueValidator interface {
Validate(value interface{}) bool
}
// BooleanValidator validates boolean values
type BooleanValidator struct{}
func (v BooleanValidator) Validate(value interface{}) bool {
_, ok := value.(bool)
return ok
}
// IntValidator validates integer values
type IntValidator struct {
Min int
Max int
}
func (v IntValidator) Validate(value interface{}) bool {
num, ok := value.(float64) // JSON numbers are parsed as float64
if !ok {
return false
}
// Check if it's a whole number
if num != float64(int(num)) {
return false
}
// Check range if specified
intVal := int(num)
if v.Min != 0 || v.Max != 0 {
if intVal < v.Min || (v.Max != 0 && intVal > v.Max) {
return false
}
}
return true
}
// StringValidator validates string values
type StringValidator struct {
AllowedValues []string
MaxLength int
}
func (v StringValidator) Validate(value interface{}) bool {
str, ok := value.(string)
if !ok {
return false
}
// Check maximum length if specified
if v.MaxLength > 0 && utf8.RuneCountInString(str) > v.MaxLength {
return false
}
// Check allowed values if specified
if len(v.AllowedValues) > 0 {
for _, allowed := range v.AllowedValues {
if str == allowed {
return true
}
}
return false
}
return true
}
// validation rules for specific settings
var settingValidators = map[string]map[string]ValueValidator{
"account": {
"public_profile": BooleanValidator{},
"store_user_agent": BooleanValidator{},
"public_subscriptions": BooleanValidator{},
"default_subscribe_all": BooleanValidator{},
"color_theme": StringValidator{AllowedValues: []string{"light", "dark", "system"}, MaxLength: 10},
},
"episode": {
"is_favorite": BooleanValidator{},
"played": BooleanValidator{},
"current_position": IntValidator{Min: 0},
},
"podcast": {
"public_subscription": BooleanValidator{},
"auto_download": BooleanValidator{},
"episode_sort": StringValidator{AllowedValues: []string{"newest_first", "oldest_first", "title"}, MaxLength: 20},
},
"device": {
"auto_update": BooleanValidator{},
"update_interval": IntValidator{Min: 10, Max: 1440}, // 10 minutes to 24 hours
"wifi_only_downloads": BooleanValidator{},
"max_episodes_per_feed": IntValidator{Min: 1, Max: 1000},
},
}
// validateSettingValue validates a setting value based on its scope and key
func validateSettingValue(scope, key string, value interface{}) (bool, string) {
// Maximum setting value length check
jsonValue, err := json.Marshal(value)
if err != nil {
return false, "Failed to serialize setting value"
}
if len(jsonValue) > MAX_SETTING_VALUE_LENGTH {
return false, "Setting value exceeds maximum length"
}
// Check if we have a specific validator for this setting
if validators, ok := settingValidators[scope]; ok {
if validator, ok := validators[key]; ok {
if !validator.Validate(value) {
return false, "Setting value failed validation for the specified scope and key"
}
}
}
return true, ""
}
// getSettings handles GET /api/2/settings/{username}/{scope}.json
func getSettings(database *db.Database) gin.HandlerFunc {
return func(c *gin.Context) {
// Get user ID from context (set by AuthMiddleware)
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// Get scope from URL
scope := c.Param("scope")
if scope == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Scope is required"})
return
}
// Validate scope
if scope != "account" && scope != "device" && scope != "podcast" && scope != "episode" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid scope. Valid values are: account, device, podcast, episode",
})
return
}
// Get optional query parameters
deviceID := c.Query("device")
podcastURL := c.Query("podcast")
episodeURL := c.Query("episode")
// Validate parameters based on scope
if scope == "device" && deviceID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Device ID is required for device scope"})
return
}
if scope == "podcast" && podcastURL == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Podcast URL is required for podcast scope"})
return
}
if scope == "episode" && (podcastURL == "" || episodeURL == "") {
c.JSON(http.StatusBadRequest, gin.H{"error": "Podcast URL and Episode URL are required for episode scope"})
return
}
// Build query based on scope
var query string
var args []interface{}
switch scope {
case "account":
if database.IsPostgreSQLDB() {
query = `
SELECT SettingKey, SettingValue
FROM "GpodderSyncSettings"
WHERE UserID = $1 AND Scope = $2
AND DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL
`
args = append(args, userID, scope)
} else {
query = `
SELECT SettingKey, SettingValue
FROM GpodderSyncSettings
WHERE UserID = ? AND Scope = ?
AND DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL
`
args = append(args, userID, scope)
}
case "device":
// Get device ID from name
var deviceIDInt int
var deviceQuery string
if database.IsPostgreSQLDB() {
deviceQuery = `
SELECT DeviceID FROM "GpodderDevices"
WHERE UserID = $1 AND DeviceName = $2 AND IsActive = true
`
} else {
deviceQuery = `
SELECT DeviceID FROM GpodderDevices
WHERE UserID = ? AND DeviceName = ? AND IsActive = true
`
}
err := database.QueryRow(deviceQuery, userID, deviceID).Scan(&deviceIDInt)
if err != nil {
if err == sql.ErrNoRows {
log.Printf("Device not found: %s", deviceID)
c.JSON(http.StatusNotFound, gin.H{"error": "Device not found or not active"})
} else {
log.Printf("Error getting device ID: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get device"})
}
return
}
if database.IsPostgreSQLDB() {
query = `
SELECT SettingKey, SettingValue
FROM "GpodderSyncSettings"
WHERE UserID = $1 AND Scope = $2 AND DeviceID = $3
AND PodcastURL IS NULL AND EpisodeURL IS NULL
`
args = append(args, userID, scope, deviceIDInt)
} else {
query = `
SELECT SettingKey, SettingValue
FROM GpodderSyncSettings
WHERE UserID = ? AND Scope = ? AND DeviceID = ?
AND PodcastURL IS NULL AND EpisodeURL IS NULL
`
args = append(args, userID, scope, deviceIDInt)
}
case "podcast":
// Validate podcast URL
if !isValidURL(podcastURL) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid podcast URL"})
return
}
if database.IsPostgreSQLDB() {
query = `
SELECT SettingKey, SettingValue
FROM "GpodderSyncSettings"
WHERE UserID = $1 AND Scope = $2 AND PodcastURL = $3
AND DeviceID IS NULL AND EpisodeURL IS NULL
`
args = append(args, userID, scope, podcastURL)
} else {
query = `
SELECT SettingKey, SettingValue
FROM GpodderSyncSettings
WHERE UserID = ? AND Scope = ? AND PodcastURL = ?
AND DeviceID IS NULL AND EpisodeURL IS NULL
`
args = append(args, userID, scope, podcastURL)
}
case "episode":
// Validate URLs
if !isValidURL(podcastURL) || !isValidURL(episodeURL) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid podcast or episode URL"})
return
}
if database.IsPostgreSQLDB() {
query = `
SELECT SettingKey, SettingValue
FROM "GpodderSyncSettings"
WHERE UserID = $1 AND Scope = $2 AND PodcastURL = $3 AND EpisodeURL = $4
AND DeviceID IS NULL
`
args = append(args, userID, scope, podcastURL, episodeURL)
} else {
query = `
SELECT SettingKey, SettingValue
FROM GpodderSyncSettings
WHERE UserID = ? AND Scope = ? AND PodcastURL = ? AND EpisodeURL = ?
AND DeviceID IS NULL
`
args = append(args, userID, scope, podcastURL, episodeURL)
}
}
// Query settings
rows, err := database.Query(query, args...)
if err != nil {
log.Printf("Error querying settings: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get settings"})
return
}
defer rows.Close()
// Build settings map
settings := make(map[string]interface{})
for rows.Next() {
var key, value string
if err := rows.Scan(&key, &value); err != nil {
log.Printf("Error scanning setting row: %v", err)
continue
}
// Try to unmarshal as JSON, fallback to string if not valid JSON
var jsonValue interface{}
if err := json.Unmarshal([]byte(value), &jsonValue); err != nil {
// Not valid JSON, use as string
settings[key] = value
} else {
// Valid JSON, use parsed value
settings[key] = jsonValue
}
}
if err := rows.Err(); err != nil {
log.Printf("Error iterating setting rows: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get settings"})
return
}
// Return settings
c.JSON(http.StatusOK, settings)
}
}
// isValidURL performs basic URL validation
func isValidURL(urlStr string) bool {
// Check if empty
if urlStr == "" {
return false
}
// Must start with http:// or https://
if !strings.HasPrefix(strings.ToLower(urlStr), "http://") &&
!strings.HasPrefix(strings.ToLower(urlStr), "https://") {
return false
}
// Basic length check
if len(urlStr) < 10 || len(urlStr) > 2048 {
return false
}
return true
}
// saveSettings handles POST /api/2/settings/{username}/{scope}.json
func saveSettings(database *db.Database) gin.HandlerFunc {
return func(c *gin.Context) {
// Get user ID from context (set by AuthMiddleware)
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// Get scope from URL
scope := c.Param("scope")
if scope == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Scope is required"})
return
}
// Validate scope
if scope != "account" && scope != "device" && scope != "podcast" && scope != "episode" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid scope. Valid values are: account, device, podcast, episode",
})
return
}
// Get optional query parameters
deviceName := c.Query("device")
podcastURL := c.Query("podcast")
episodeURL := c.Query("episode")
// Validate parameters based on scope
if scope == "device" && deviceName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Device ID is required for device scope"})
return
}
if scope == "podcast" && podcastURL == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Podcast URL is required for podcast scope"})
return
}
if scope == "episode" && (podcastURL == "" || episodeURL == "") {
c.JSON(http.StatusBadRequest, gin.H{"error": "Podcast URL and Episode URL are required for episode scope"})
return
}
// Validate URLs
if scope == "podcast" && !isValidURL(podcastURL) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid podcast URL"})
return
}
if scope == "episode" && (!isValidURL(podcastURL) || !isValidURL(episodeURL)) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid podcast or episode URL"})
return
}
// Parse request body
var req models.SettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("Error parsing request: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: expected a JSON object with 'set' and 'remove' properties"})
return
}
// Validate request size
if len(req.Set) > MAX_SETTINGS_PER_REQUEST || len(req.Remove) > MAX_SETTINGS_PER_REQUEST {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Too many settings in request. Maximum allowed: %d", MAX_SETTINGS_PER_REQUEST),
})
return
}
// Process device ID if needed
var deviceID *int
if scope == "device" {
var deviceIDInt int
var deviceQuery string
if database.IsPostgreSQLDB() {
deviceQuery = `
SELECT DeviceID FROM "GpodderDevices"
WHERE UserID = $1 AND DeviceName = $2 AND IsActive = true
`
} else {
deviceQuery = `
SELECT DeviceID FROM GpodderDevices
WHERE UserID = ? AND DeviceName = ? AND IsActive = true
`
}
err := database.QueryRow(deviceQuery, userID, deviceName).Scan(&deviceIDInt)
if err != nil {
if err == sql.ErrNoRows {
// Create the device if it doesn't exist
if database.IsPostgreSQLDB() {
deviceQuery = `
INSERT INTO "GpodderDevices" (UserID, DeviceName, DeviceType, IsActive, LastSync)
VALUES ($1, $2, 'other', true, $3)
RETURNING DeviceID
`
err = database.QueryRow(deviceQuery, userID, deviceName, time.Now()).Scan(&deviceIDInt)
} else {
deviceQuery = `
INSERT INTO GpodderDevices (UserID, DeviceName, DeviceType, IsActive, LastSync)
VALUES (?, ?, 'other', true, ?)
`
result, err := database.Exec(deviceQuery, userID, deviceName, "other", time.Now())
if err != nil {
log.Printf("Error creating device: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create device"})
return
}
lastID, err := result.LastInsertId()
if err != nil {
log.Printf("Error getting last insert ID: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create device"})
return
}
deviceIDInt = int(lastID)
}
if err != nil {
log.Printf("Error creating device: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create device"})
return
}
} else {
log.Printf("Error getting device ID: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get device"})
return
}
}
deviceID = &deviceIDInt
}
// Begin transaction
tx, err := database.Begin()
if err != nil {
log.Printf("Error beginning transaction: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save settings"})
return
}
defer func() {
if err != nil {
tx.Rollback()
return
}
}()
// Process settings to set
for key, value := range req.Set {
// Validate key
if len(key) == 0 || len(key) > MAX_SETTING_KEY_LENGTH {
log.Printf("Invalid setting key length: %s", key)
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Invalid setting key: must be between 1 and %d characters", MAX_SETTING_KEY_LENGTH),
})
return
}
// Allow only letters, numbers, underscores and hyphens
if !isValidSettingKey(key) {
log.Printf("Invalid setting key: %s", key)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid setting key: must contain only letters, numbers, underscores and hyphens",
})
return
}
// Validate value
valid, errMsg := validateSettingValue(scope, key, value)
if !valid {
log.Printf("Invalid setting value for key %s: %s", key, errMsg)
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Invalid value for key '%s': %s", key, errMsg),
})
return
}
// Convert value to JSON string
jsonValue, err := json.Marshal(value)
if err != nil {
log.Printf("Error marshaling value to JSON: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid value for key: " + key})
return
}
// Build query based on scope
var query string
var args []interface{}
switch scope {
case "account":
if database.IsPostgreSQLDB() {
query = `
INSERT INTO "GpodderSyncSettings" (UserID, Scope, SettingKey, SettingValue, LastUpdated)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (UserID, Scope, SettingKey)
WHERE DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL
DO UPDATE SET SettingValue = $4, LastUpdated = $5
`
args = append(args, userID, scope, key, string(jsonValue), time.Now())
} else {
query = `
INSERT INTO GpodderSyncSettings (UserID, Scope, SettingKey, SettingValue, LastUpdated)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE SettingValue = VALUES(SettingValue), LastUpdated = VALUES(LastUpdated)
`
args = append(args, userID, scope, key, string(jsonValue), time.Now())
}
case "device":
if database.IsPostgreSQLDB() {
query = `
INSERT INTO "GpodderSyncSettings" (UserID, Scope, DeviceID, SettingKey, SettingValue, LastUpdated)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (UserID, Scope, SettingKey, DeviceID)
WHERE PodcastURL IS NULL AND EpisodeURL IS NULL
DO UPDATE SET SettingValue = $5, LastUpdated = $6
`
args = append(args, userID, scope, deviceID, key, string(jsonValue), time.Now())
} else {
query = `
INSERT INTO GpodderSyncSettings (UserID, Scope, DeviceID, SettingKey, SettingValue, LastUpdated)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE SettingValue = VALUES(SettingValue), LastUpdated = VALUES(LastUpdated)
`
args = append(args, userID, scope, deviceID, key, string(jsonValue), time.Now())
}
case "podcast":
if database.IsPostgreSQLDB() {
query = `
INSERT INTO "GpodderSyncSettings" (UserID, Scope, PodcastURL, SettingKey, SettingValue, LastUpdated)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (UserID, Scope, SettingKey, PodcastURL)
WHERE DeviceID IS NULL AND EpisodeURL IS NULL
DO UPDATE SET SettingValue = $5, LastUpdated = $6
`
args = append(args, userID, scope, podcastURL, key, string(jsonValue), time.Now())
} else {
query = `
INSERT INTO GpodderSyncSettings (UserID, Scope, PodcastURL, SettingKey, SettingValue, LastUpdated)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE SettingValue = VALUES(SettingValue), LastUpdated = VALUES(LastUpdated)
`
args = append(args, userID, scope, podcastURL, key, string(jsonValue), time.Now())
}
case "episode":
if database.IsPostgreSQLDB() {
query = `
INSERT INTO "GpodderSyncSettings" (UserID, Scope, PodcastURL, EpisodeURL, SettingKey, SettingValue, LastUpdated)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (UserID, Scope, SettingKey, PodcastURL, EpisodeURL)
WHERE DeviceID IS NULL
DO UPDATE SET SettingValue = $6, LastUpdated = $7
`
args = append(args, userID, scope, podcastURL, episodeURL, key, string(jsonValue), time.Now())
} else {
query = `
INSERT INTO GpodderSyncSettings (UserID, Scope, PodcastURL, EpisodeURL, SettingKey, SettingValue, LastUpdated)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE SettingValue = VALUES(SettingValue), LastUpdated = VALUES(LastUpdated)
`
args = append(args, userID, scope, podcastURL, episodeURL, key, string(jsonValue), time.Now())
}
}
// Execute query
_, err = tx.Exec(query, args...)
if err != nil {
log.Printf("Error setting value for key %s: %v", key, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save settings"})
return
}
}
// Process settings to remove
for _, key := range req.Remove {
// Validate key
if len(key) == 0 || len(key) > MAX_SETTING_KEY_LENGTH {
log.Printf("Invalid setting key length: %s", key)
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Invalid setting key: must be between 1 and %d characters", MAX_SETTING_KEY_LENGTH),
})
return
}
// Allow only letters, numbers, underscores and hyphens
if !isValidSettingKey(key) {
log.Printf("Invalid setting key: %s", key)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid setting key: must contain only letters, numbers, underscores and hyphens",
})
return
}
// Build query based on scope
var query string
var args []interface{}
switch scope {
case "account":
if database.IsPostgreSQLDB() {
query = `
DELETE FROM "GpodderSyncSettings"
WHERE UserID = $1 AND Scope = $2 AND SettingKey = $3
AND DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL
`
args = append(args, userID, scope, key)
} else {
query = `
DELETE FROM GpodderSyncSettings
WHERE UserID = ? AND Scope = ? AND SettingKey = ?
AND DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL
`
args = append(args, userID, scope, key)
}
case "device":
if database.IsPostgreSQLDB() {
query = `
DELETE FROM "GpodderSyncSettings"
WHERE UserID = $1 AND Scope = $2 AND DeviceID = $3 AND SettingKey = $4
AND PodcastURL IS NULL AND EpisodeURL IS NULL
`
args = append(args, userID, scope, deviceID, key)
} else {
query = `
DELETE FROM GpodderSyncSettings
WHERE UserID = ? AND Scope = ? AND DeviceID = ? AND SettingKey = ?
AND PodcastURL IS NULL AND EpisodeURL IS NULL
`
args = append(args, userID, scope, deviceID, key)
}
case "podcast":
if database.IsPostgreSQLDB() {
query = `
DELETE FROM "GpodderSyncSettings"
WHERE UserID = $1 AND Scope = $2 AND PodcastURL = $3 AND SettingKey = $4
AND DeviceID IS NULL AND EpisodeURL IS NULL
`
args = append(args, userID, scope, podcastURL, key)
} else {
query = `
DELETE FROM GpodderSyncSettings
WHERE UserID = ? AND Scope = ? AND PodcastURL = ? AND SettingKey = ?
AND DeviceID IS NULL AND EpisodeURL IS NULL
`
args = append(args, userID, scope, podcastURL, key)
}
case "episode":
if database.IsPostgreSQLDB() {
query = `
DELETE FROM "GpodderSyncSettings"
WHERE UserID = $1 AND Scope = $2 AND PodcastURL = $3 AND EpisodeURL = $4 AND SettingKey = $5
AND DeviceID IS NULL
`
args = append(args, userID, scope, podcastURL, episodeURL, key)
} else {
query = `
DELETE FROM GpodderSyncSettings
WHERE UserID = ? AND Scope = ? AND PodcastURL = ? AND EpisodeURL = ? AND SettingKey = ?
AND DeviceID IS NULL
`
args = append(args, userID, scope, podcastURL, episodeURL, key)
}
}
// Execute query
_, err = tx.Exec(query, args...)
if err != nil {
log.Printf("Error removing key %s: %v", key, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save settings"})
return
}
}
// Commit transaction
if err = tx.Commit(); err != nil {
log.Printf("Error committing transaction: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save settings"})
return
}
// Query all settings for the updated response
var queryAll string
var argsAll []interface{}
switch scope {
case "account":
if database.IsPostgreSQLDB() {
queryAll = `
SELECT SettingKey, SettingValue
FROM "GpodderSyncSettings"
WHERE UserID = $1 AND Scope = $2
AND DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL
`
argsAll = append(argsAll, userID, scope)
} else {
queryAll = `
SELECT SettingKey, SettingValue
FROM GpodderSyncSettings
WHERE UserID = ? AND Scope = ?
AND DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL
`
argsAll = append(argsAll, userID, scope)
}
case "device":
if database.IsPostgreSQLDB() {
queryAll = `
SELECT SettingKey, SettingValue
FROM "GpodderSyncSettings"
WHERE UserID = $1 AND Scope = $2 AND DeviceID = $3
AND PodcastURL IS NULL AND EpisodeURL IS NULL
`
argsAll = append(argsAll, userID, scope, deviceID)
} else {
queryAll = `
SELECT SettingKey, SettingValue
FROM GpodderSyncSettings
WHERE UserID = ? AND Scope = ? AND DeviceID = ?
AND PodcastURL IS NULL AND EpisodeURL IS NULL
`
argsAll = append(argsAll, userID, scope, deviceID)
}
case "podcast":
if database.IsPostgreSQLDB() {
queryAll = `
SELECT SettingKey, SettingValue
FROM "GpodderSyncSettings"
WHERE UserID = $1 AND Scope = $2 AND PodcastURL = $3
AND DeviceID IS NULL AND EpisodeURL IS NULL
`
argsAll = append(argsAll, userID, scope, podcastURL)
} else {
queryAll = `
SELECT SettingKey, SettingValue
FROM GpodderSyncSettings
WHERE UserID = ? AND Scope = ? AND PodcastURL = ?
AND DeviceID IS NULL AND EpisodeURL IS NULL
`
argsAll = append(argsAll, userID, scope, podcastURL)
}
case "episode":
if database.IsPostgreSQLDB() {
queryAll = `
SELECT SettingKey, SettingValue
FROM "GpodderSyncSettings"
WHERE UserID = $1 AND Scope = $2 AND PodcastURL = $3 AND EpisodeURL = $4
AND DeviceID IS NULL
`
argsAll = append(argsAll, userID, scope, podcastURL, episodeURL)
} else {
queryAll = `
SELECT SettingKey, SettingValue
FROM GpodderSyncSettings
WHERE UserID = ? AND Scope = ? AND PodcastURL = ? AND EpisodeURL = ?
AND DeviceID IS NULL
`
argsAll = append(argsAll, userID, scope, podcastURL, episodeURL)
}
}
// Query all settings
rows, err := database.Query(queryAll, argsAll...)
if err != nil {
log.Printf("Error querying all settings: %v", err)
c.JSON(http.StatusOK, gin.H{}) // Return empty object in case of error
return
}
defer rows.Close()
// Build settings map
settings := make(map[string]interface{})
for rows.Next() {
var key, value string
if err := rows.Scan(&key, &value); err != nil {
log.Printf("Error scanning setting row: %v", err)
continue
}
// Try to unmarshal as JSON, fallback to string if not valid JSON
var jsonValue interface{}
if err := json.Unmarshal([]byte(value), &jsonValue); err != nil {
// Not valid JSON, use as string
settings[key] = value
} else {
// Valid JSON, use parsed value
settings[key] = jsonValue
}
}
if err := rows.Err(); err != nil {
log.Printf("Error iterating setting rows: %v", err)
c.JSON(http.StatusOK, gin.H{}) // Return empty object in case of error
return
}
// Return updated settings
c.JSON(http.StatusOK, settings)
}
}
// isValidSettingKey checks if the key contains only valid characters
func isValidSettingKey(key string) bool {
for _, r := range key {
if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && (r < '0' || r > '9') && r != '_' && r != '-' {
return false
}
}
return true
}
// toggleGpodderAPI is a Pinepods-specific extension to enable/disable the gpodder API for a user
func toggleGpodderAPI(database *db.PostgresDB) gin.HandlerFunc {
return func(c *gin.Context) {
// Get user ID from context (set by AuthMiddleware)
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// Parse request body
var req struct {
Enable bool `json:"enable"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
// Set Pod_Sync_Type based on enable flag
var podSyncType string
if req.Enable {
// Check if external gpodder sync is already enabled
var currentSyncType string
err := database.QueryRow(`
SELECT Pod_Sync_Type FROM "Users" WHERE UserID = $1
`, userID).Scan(&currentSyncType)
if err != nil {
log.Printf("Error getting current sync type: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to toggle gpodder API"})
return
}
if currentSyncType == "external" {
podSyncType = "both"
} else {
podSyncType = "gpodder"
}
} else {
// Check if external gpodder sync is enabled
var currentSyncType string
err := database.QueryRow(`
SELECT Pod_Sync_Type FROM "Users" WHERE UserID = $1
`, userID).Scan(&currentSyncType)
if err != nil {
log.Printf("Error getting current sync type: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to toggle gpodder API"})
return
}
if currentSyncType == "both" {
podSyncType = "external"
} else {
podSyncType = "None"
}
}
// Update user's Pod_Sync_Type
_, err := database.Exec(`
UPDATE "Users" SET Pod_Sync_Type = $1 WHERE UserID = $2
`, podSyncType, userID)
if err != nil {
log.Printf("Error updating Pod_Sync_Type: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to toggle gpodder API"})
return
}
// Return success response
c.JSON(http.StatusOK, gin.H{
"enabled": req.Enable,
"sync_type": podSyncType,
})
}
}