Add Google Reader API implementation (experimental)

Co-authored-by: Sebastian Kempken <sebastian@kempken.io>
Co-authored-by: Gergan Penkov <gergan@gmail.com>
Co-authored-by: Dave Marquard <dave@marquard.org>
Co-authored-by: Moritz Fago <4459068+MoritzFago@users.noreply.github.com>
This commit is contained in:
Gergan Penkov 2022-01-03 04:45:12 +01:00 committed by GitHub
parent 2935aaef45
commit 4b6e46d9ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1923 additions and 36 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ miniflux-*
miniflux miniflux
*.rpm *.rpm
*.deb *.deb
.idea

View File

@ -563,4 +563,13 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql) _, err = tx.Exec(sql)
return err return err
}, },
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN googlereader_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN googlereader_username text default '';
ALTER TABLE integrations ADD COLUMN googlereader_password text default '';
`
_, err = tx.Exec(sql)
return err
},
} }

10
googlereader/doc.go Normal file
View File

@ -0,0 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
/*
Package googlereader implements Google Reader API endpoints.
*/
package googlereader // import "miniflux.app/googlereader"

1180
googlereader/handler.go Normal file

File diff suppressed because it is too large Load Diff

208
googlereader/middleware.go Normal file
View File

@ -0,0 +1,208 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package googlereader // import "miniflux.app/googlereader"
import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"net/http"
"strings"
"miniflux.app/http/request"
"miniflux.app/http/response"
"miniflux.app/http/response/json"
"miniflux.app/logger"
"miniflux.app/model"
"miniflux.app/storage"
)
type middleware struct {
store *storage.Storage
}
func newMiddleware(s *storage.Storage) *middleware {
return &middleware{s}
}
func (m *middleware) clientLogin(w http.ResponseWriter, r *http.Request) {
clientIP := request.ClientIP(r)
var username, password, output string
var integration *model.Integration
err := r.ParseForm()
if err != nil {
logger.Error("[Reader][Login] [ClientIP=%s] Could not parse form", clientIP)
json.Unauthorized(w, r)
return
}
username = r.Form.Get("Email")
password = r.Form.Get("Passwd")
output = r.Form.Get("output")
if username == "" || password == "" {
logger.Error("[Reader][Login] [ClientIP=%s] Empty username or password", clientIP)
json.Unauthorized(w, r)
return
}
if err = m.store.GoogleReaderUserCheckPassword(username, password); err != nil {
logger.Error("[Reader][Login] [ClientIP=%s] Invalid username or password: %s", clientIP, username)
json.Unauthorized(w, r)
return
}
logger.Info("[Reader][Login] [ClientIP=%s] User authenticated: %s", clientIP, username)
if integration, err = m.store.GoogleReaderUserGetIntegration(username); err != nil {
logger.Error("[Reader][Login] [ClientIP=%s] Could not load integration: %s", clientIP, username)
json.Unauthorized(w, r)
return
}
m.store.SetLastLogin(integration.UserID)
token := getAuthToken(integration.GoogleReaderUsername, integration.GoogleReaderPassword)
logger.Info("[Reader][Login] [ClientIP=%s] Created token: %s", clientIP, token)
result := login{SID: token, LSID: token, Auth: token}
if output == "json" {
json.OK(w, r, result)
return
}
builder := response.New(w, r)
builder.WithHeader("Content-Type", "text/plain; charset=UTF-8")
builder.WithBody(result.String())
builder.Write()
}
func (m *middleware) token(w http.ResponseWriter, r *http.Request) {
clientIP := request.ClientIP(r)
if !request.IsAuthenticated(r) {
logger.Error("[Reader][Token] [ClientIP=%s] User is not authenticated", clientIP)
json.Unauthorized(w, r)
return
}
token := request.GoolgeReaderToken(r)
if token == "" {
logger.Error("[Reader][Token] [ClientIP=%s] User does not have token: %s", clientIP, request.UserID(r))
json.Unauthorized(w, r)
return
}
logger.Info("[Reader][Token] [ClientIP=%s] token: %s", clientIP, token)
w.Header().Add("Content-Type", "text/plain; charset=UTF-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(token))
}
func (m *middleware) handleCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
clientIP := request.ClientIP(r)
var token string
if r.Method == http.MethodPost {
err := r.ParseForm()
if err != nil {
logger.Error("[Reader][Login] [ClientIP=%s] Could not parse form", clientIP)
Unauthorized(w, r)
return
}
token = r.Form.Get("T")
if token == "" {
logger.Error("[Reader][Auth] [ClientIP=%s] Post-Form T field is empty", clientIP)
Unauthorized(w, r)
return
}
} else {
authorization := r.Header.Get("Authorization")
if authorization == "" {
logger.Error("[Reader][Auth] [ClientIP=%s] No token provided", clientIP)
Unauthorized(w, r)
return
}
fields := strings.Fields(authorization)
if len(fields) != 2 {
logger.Error("[Reader][Auth] [ClientIP=%s] Authorization header does not have the expected structure GoogleLogin auth=xxxxxx - '%s'", clientIP, authorization)
Unauthorized(w, r)
return
}
if fields[0] != "GoogleLogin" {
logger.Error("[Reader][Auth] [ClientIP=%s] Authorization header does not begin with GoogleLogin - '%s'", clientIP, authorization)
Unauthorized(w, r)
return
}
auths := strings.Split(fields[1], "=")
if len(auths) != 2 {
logger.Error("[Reader][Auth] [ClientIP=%s] Authorization header does not have the expected structure GoogleLogin auth=xxxxxx - '%s'", clientIP, authorization)
Unauthorized(w, r)
return
}
if auths[0] != "auth" {
logger.Error("[Reader][Auth] [ClientIP=%s] Authorization header does not have the expected structure GoogleLogin auth=xxxxxx - '%s'", clientIP, authorization)
Unauthorized(w, r)
return
}
token = auths[1]
}
parts := strings.Split(token, "/")
if len(parts) != 2 {
logger.Error("[Reader][Auth] [ClientIP=%s] Auth token does not have the expected structure username/hash - '%s'", clientIP, token)
Unauthorized(w, r)
return
}
var integration *model.Integration
var user *model.User
var err error
if integration, err = m.store.GoogleReaderUserGetIntegration(parts[0]); err != nil {
logger.Error("[Reader][Auth] [ClientIP=%s] token: %s", clientIP, token)
logger.Error("[Reader][Auth] [ClientIP=%s] No user found with the given google reader username: %s", clientIP, parts[0])
Unauthorized(w, r)
return
}
expectedToken := getAuthToken(integration.GoogleReaderUsername, integration.GoogleReaderPassword)
if expectedToken != token {
logger.Error("[Reader][Auth] [ClientIP=%s] Token does not match: %s", clientIP, token)
Unauthorized(w, r)
return
}
if user, err = m.store.UserByID(integration.UserID); err != nil {
logger.Error("[Reader][Auth] [ClientIP=%s] No user found with the userID: %d", clientIP, integration.UserID)
Unauthorized(w, r)
return
}
m.store.SetLastLogin(integration.UserID)
ctx := r.Context()
ctx = context.WithValue(ctx, request.UserIDContextKey, user.ID)
ctx = context.WithValue(ctx, request.UserTimezoneContextKey, user.Timezone)
ctx = context.WithValue(ctx, request.IsAdminUserContextKey, user.IsAdmin)
ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
ctx = context.WithValue(ctx, request.GoogleReaderToken, token)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func getAuthToken(username, password string) string {
token := hex.EncodeToString(hmac.New(sha1.New, []byte(username+password)).Sum(nil))
token = username + "/" + token
return token
}

144
googlereader/response.go Normal file
View File

@ -0,0 +1,144 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package googlereader // import "miniflux.app/googlereader"
import (
"fmt"
"net/http"
"miniflux.app/http/response"
"miniflux.app/logger"
)
type login struct {
SID string `json:"SID,omitempty"`
LSID string `json:"LSID,omitempty"`
Auth string `json:"Auth,omitempty"`
}
func (l login) String() string {
return fmt.Sprintf("SID=%s\nLSID=%s\nAuth=%s\n", l.SID, l.LSID, l.Auth)
}
type userInfo struct {
UserID string `json:"userId"`
UserName string `json:"userName"`
UserProfileID string `json:"userProfileId"`
UserEmail string `json:"userEmail"`
}
type subscription struct {
ID string `json:"id"`
Title string `json:"title"`
Categories []subscriptionCategory `json:"categories"`
URL string `json:"url"`
HTMLURL string `json:"htmlUrl"`
IconURL string `json:"iconUrl"`
}
type quickAddResponse struct {
NumResults int64 `json:"numResults"`
Query string `json:"query,omitempty"`
StreamID string `json:"streamId,omitempty"`
StreamName string `json:"streamName,omitempty"`
}
type subscriptionCategory struct {
ID string `json:"id"`
Label string `json:"label,omitempty"`
Type string `json:"type,omitempty"`
}
type subscriptionsResponse struct {
Subscriptions []subscription `json:"subscriptions"`
}
type itemRef struct {
ID string `json:"id"`
DirectStreamIDs string `json:"directStreamIds,omitempty"`
TimestampUsec string `json:"timestampUsec,omitempty"`
}
type streamIDResponse struct {
ItemRefs []itemRef `json:"itemRefs"`
}
type tagsResponse struct {
Tags []subscriptionCategory `json:"tags"`
}
type streamContentItems struct {
Direction string `json:"direction"`
ID string `json:"id"`
Title string `json:"title"`
Self []contentHREF `json:"self"`
Alternate []contentHREFType `json:"alternate"`
Updated int64 `json:"updated"`
Items []contentItem `json:"items"`
Author string `json:"author"`
}
type contentItem struct {
ID string `json:"id"`
Categories []string `json:"categories"`
Title string `json:"title"`
CrawlTimeMsec string `json:"crawlTimeMsec"`
TimestampUsec string `json:"timestampUsec"`
Published int64 `json:"published"`
Updated int64 `json:"updated"`
Author string `json:"author"`
Alternate []contentHREFType `json:"alternate"`
Summary contentItemContent `json:"summary"`
Content contentItemContent `json:"content"`
Origin contentItemOrigin `json:"origin"`
Enclosure []contentItemEnclosure `json:"enclosure"`
Canonical []contentHREF `json:"canonical"`
}
type contentHREFType struct {
HREF string `json:"href"`
Type string `json:"type"`
}
type contentHREF struct {
HREF string `json:"href"`
}
type contentItemEnclosure struct {
URL string `json:"url"`
Type string `json:"type"`
}
type contentItemContent struct {
Direction string `json:"direction"`
Content string `json:"content"`
}
type contentItemOrigin struct {
StreamID string `json:"streamId"`
Title string `json:"title"`
HTMLUrl string `json:"htmlUrl"`
}
// Unauthorized sends a not authorized error to the client.
func Unauthorized(w http.ResponseWriter, r *http.Request) {
logger.Error("[HTTP:Unauthorized] %s", r.URL)
builder := response.New(w, r)
builder.WithStatus(http.StatusUnauthorized)
builder.WithHeader("Content-Type", "text/plain")
builder.WithHeader("X-Reader-Google-Bad-Token", "true")
builder.WithBody("Unauthorized")
builder.Write()
}
// OK sends a ok response to the client.
func OK(w http.ResponseWriter, r *http.Request) {
logger.Info("[HTTP:OK] %s", r.URL)
builder := response.New(w, r)
builder.WithStatus(http.StatusOK)
builder.WithHeader("Content-Type", "text/plain")
builder.WithBody("OK")
builder.Write()
}

View File

@ -25,8 +25,14 @@ const (
FlashErrorMessageContextKey FlashErrorMessageContextKey
PocketRequestTokenContextKey PocketRequestTokenContextKey
ClientIPContextKey ClientIPContextKey
GoogleReaderToken
) )
// GoolgeReaderToken returns the google reader token if it exists.
func GoolgeReaderToken(r *http.Request) string {
return getContextStringValue(r, GoogleReaderToken)
}
// IsAdminUser checks if the logged user is administrator. // IsAdminUser checks if the logged user is administrator.
func IsAdminUser(r *http.Request) bool { func IsAdminUser(r *http.Request) bool {
return getContextBoolValue(r, IsAdminUserContextKey) return getContextBoolValue(r, IsAdminUserContextKey)

View File

@ -223,6 +223,7 @@
"error.unlink_account_without_password": "Sie müssen ein Passwort festlegen, sonst können Sie sich nicht erneut anmelden.", "error.unlink_account_without_password": "Sie müssen ein Passwort festlegen, sonst können Sie sich nicht erneut anmelden.",
"error.duplicate_linked_account": "Es ist bereits jemand mit diesem Anbieter assoziiert!", "error.duplicate_linked_account": "Es ist bereits jemand mit diesem Anbieter assoziiert!",
"error.duplicate_fever_username": "Es existiert bereits jemand mit diesem Fever Benutzernamen!", "error.duplicate_fever_username": "Es existiert bereits jemand mit diesem Fever Benutzernamen!",
"error.duplicate_googlereader_username": "Es existiert bereits jemand mit diesem Google Reader Benutzernamen!",
"error.pocket_request_token": "Anfrage-Token konnte nicht von Pocket abgerufen werden!", "error.pocket_request_token": "Anfrage-Token konnte nicht von Pocket abgerufen werden!",
"error.pocket_access_token": "Zugriffstoken konnte nicht von Pocket abgerufen werden!", "error.pocket_access_token": "Zugriffstoken konnte nicht von Pocket abgerufen werden!",
"error.category_already_exists": "Diese Kategorie existiert bereits.", "error.category_already_exists": "Diese Kategorie existiert bereits.",
@ -308,6 +309,10 @@
"form.integration.fever_username": "Fever Benutzername", "form.integration.fever_username": "Fever Benutzername",
"form.integration.fever_password": "Fever Passwort", "form.integration.fever_password": "Fever Passwort",
"form.integration.fever_endpoint": "Fever API Endpunkt:", "form.integration.fever_endpoint": "Fever API Endpunkt:",
"form.integration.googlereader_activate": "Google Reader API aktivieren",
"form.integration.googlereader_username": "Google Reader Benutzername",
"form.integration.googlereader_password": "Google Reader Passwort",
"form.integration.googlereader_endpoint": "Google Reader API Endpunkt:",
"form.integration.pinboard_activate": "Artikel in Pinboard speichern", "form.integration.pinboard_activate": "Artikel in Pinboard speichern",
"form.integration.pinboard_token": "Pinboard API Token", "form.integration.pinboard_token": "Pinboard API Token",
"form.integration.pinboard_tags": "Pinboard Tags", "form.integration.pinboard_tags": "Pinboard Tags",

View File

@ -223,6 +223,7 @@
"error.unlink_account_without_password": "Πρέπει να ορίσετε έναν κωδικό πρόσβασης διαφορετικά δεν θα μπορείτε να συνδεθείτε ξανά.", "error.unlink_account_without_password": "Πρέπει να ορίσετε έναν κωδικό πρόσβασης διαφορετικά δεν θα μπορείτε να συνδεθείτε ξανά.",
"error.duplicate_linked_account": "Υπάρχει ήδη κάποιος που σχετίζεται με αυτόν τον πάροχο!", "error.duplicate_linked_account": "Υπάρχει ήδη κάποιος που σχετίζεται με αυτόν τον πάροχο!",
"error.duplicate_fever_username": "Υπάρχει ήδη κάποιος άλλος με το ίδιο όνομα χρήστη Fever!", "error.duplicate_fever_username": "Υπάρχει ήδη κάποιος άλλος με το ίδιο όνομα χρήστη Fever!",
"error.duplicate_googlereader_username": "Υπάρχει ήδη κάποιος άλλος με το ίδιο όνομα χρήστη Google Reader!",
"error.pocket_request_token": "Δεν είναι δυνατή η λήψη του request token από το Pocket!", "error.pocket_request_token": "Δεν είναι δυνατή η λήψη του request token από το Pocket!",
"error.pocket_access_token": "Δεν είναι δυνατή η λήψη του access token από το Pocket!", "error.pocket_access_token": "Δεν είναι δυνατή η λήψη του access token από το Pocket!",
"error.category_already_exists": "Αυτή η κατηγορία υπάρχει ήδη.", "error.category_already_exists": "Αυτή η κατηγορία υπάρχει ήδη.",
@ -308,6 +309,10 @@
"form.integration.fever_username": "Όνομα Χρήστη Fever", "form.integration.fever_username": "Όνομα Χρήστη Fever",
"form.integration.fever_password": "Κωδικός Πρόσβασης Fever", "form.integration.fever_password": "Κωδικός Πρόσβασης Fever",
"form.integration.fever_endpoint": "Τελικό σημείο Fever API:", "form.integration.fever_endpoint": "Τελικό σημείο Fever API:",
"form.integration.googlereader_activate": "Ενεργοποιήστε το Google Reader API",
"form.integration.googlereader_username": "Όνομα Χρήστη Google Reader",
"form.integration.googlereader_password": "Κωδικός Πρόσβασης Google Reader",
"form.integration.googlereader_endpoint": "Τελικό σημείο Google Reader API:",
"form.integration.pinboard_activate": "Αποθήκευση άρθρων στο Pinboard", "form.integration.pinboard_activate": "Αποθήκευση άρθρων στο Pinboard",
"form.integration.pinboard_token": "Pinboard API Token", "form.integration.pinboard_token": "Pinboard API Token",
"form.integration.pinboard_tags": "Ετικέτες Pinboard", "form.integration.pinboard_tags": "Ετικέτες Pinboard",

View File

@ -223,6 +223,7 @@
"error.unlink_account_without_password": "You must define a password otherwise you won't be able to login again.", "error.unlink_account_without_password": "You must define a password otherwise you won't be able to login again.",
"error.duplicate_linked_account": "There is already someone associated with this provider!", "error.duplicate_linked_account": "There is already someone associated with this provider!",
"error.duplicate_fever_username": "There is already someone else with the same Fever username!", "error.duplicate_fever_username": "There is already someone else with the same Fever username!",
"error.duplicate_googlereader_username": "There is already someone else with the same Google Reader username!",
"error.pocket_request_token": "Unable to fetch request token from Pocket!", "error.pocket_request_token": "Unable to fetch request token from Pocket!",
"error.pocket_access_token": "Unable to fetch access token from Pocket!", "error.pocket_access_token": "Unable to fetch access token from Pocket!",
"error.category_already_exists": "This category already exists.", "error.category_already_exists": "This category already exists.",
@ -308,6 +309,10 @@
"form.integration.fever_username": "Fever Username", "form.integration.fever_username": "Fever Username",
"form.integration.fever_password": "Fever Password", "form.integration.fever_password": "Fever Password",
"form.integration.fever_endpoint": "Fever API endpoint:", "form.integration.fever_endpoint": "Fever API endpoint:",
"form.integration.googlereader_activate": "Activate Google Reader API",
"form.integration.googlereader_username": "Google Reader Username",
"form.integration.googlereader_password": "Google Reader Password",
"form.integration.googlereader_endpoint": "Google Reader API endpoint:",
"form.integration.pinboard_activate": "Save articles to Pinboard", "form.integration.pinboard_activate": "Save articles to Pinboard",
"form.integration.pinboard_token": "Pinboard API Token", "form.integration.pinboard_token": "Pinboard API Token",
"form.integration.pinboard_tags": "Pinboard Tags", "form.integration.pinboard_tags": "Pinboard Tags",

View File

@ -223,6 +223,7 @@
"error.unlink_account_without_password": "Debe definir una contraseña, de lo contrario no podrá volver a iniciar sesión.", "error.unlink_account_without_password": "Debe definir una contraseña, de lo contrario no podrá volver a iniciar sesión.",
"error.duplicate_linked_account": "¡Ya hay alguien asociado a este servicio!", "error.duplicate_linked_account": "¡Ya hay alguien asociado a este servicio!",
"error.duplicate_fever_username": "¡Ya hay alguien con el mismo nombre de usuario de Fever!", "error.duplicate_fever_username": "¡Ya hay alguien con el mismo nombre de usuario de Fever!",
"error.duplicate_googlereader_username": "¡Ya hay alguien con el mismo nombre de usuario de Google Reader!",
"error.pocket_request_token": "Incapaz de obtener un token de solicitud de Pocket!", "error.pocket_request_token": "Incapaz de obtener un token de solicitud de Pocket!",
"error.pocket_access_token": "Incapaz de obtener un token de acceso de Pocket!", "error.pocket_access_token": "Incapaz de obtener un token de acceso de Pocket!",
"error.category_already_exists": "Esta categoría ya existe.", "error.category_already_exists": "Esta categoría ya existe.",
@ -308,6 +309,10 @@
"form.integration.fever_username": "Nombre de usuario de Fever", "form.integration.fever_username": "Nombre de usuario de Fever",
"form.integration.fever_password": "Contraseña de Fever", "form.integration.fever_password": "Contraseña de Fever",
"form.integration.fever_endpoint": "Extremo de API de Fever:", "form.integration.fever_endpoint": "Extremo de API de Fever:",
"form.integration.googlereader_activate": "Activar API de Google Reader",
"form.integration.googlereader_username": "Nombre de usuario de Google Reader",
"form.integration.googlereader_password": "Contraseña de Google Reader",
"form.integration.googlereader_endpoint": "Extremo de API de Google Reader:",
"form.integration.pinboard_activate": "Guardar artículos a Pinboard", "form.integration.pinboard_activate": "Guardar artículos a Pinboard",
"form.integration.pinboard_token": "Token de API de Pinboard", "form.integration.pinboard_token": "Token de API de Pinboard",
"form.integration.pinboard_tags": "Etiquetas de Pinboard", "form.integration.pinboard_tags": "Etiquetas de Pinboard",

View File

@ -223,6 +223,7 @@
"error.unlink_account_without_password": "Vous devez définir un mot de passe sinon vous ne pourrez plus vous connecter par la suite.", "error.unlink_account_without_password": "Vous devez définir un mot de passe sinon vous ne pourrez plus vous connecter par la suite.",
"error.duplicate_linked_account": "Il y a déjà quelqu'un d'associé avec ce provider !", "error.duplicate_linked_account": "Il y a déjà quelqu'un d'associé avec ce provider !",
"error.duplicate_fever_username": "Il y a déjà quelqu'un d'autre avec le même nom d'utilisateur Fever !", "error.duplicate_fever_username": "Il y a déjà quelqu'un d'autre avec le même nom d'utilisateur Fever !",
"error.duplicate_googlereader_username": "Il y a déjà quelqu'un d'autre avec le même nom d'utilisateur Google Reader !",
"error.pocket_request_token": "Impossible de récupérer le jeton d'accès depuis Pocket !", "error.pocket_request_token": "Impossible de récupérer le jeton d'accès depuis Pocket !",
"error.pocket_access_token": "Impossible de récupérer le jeton d'accès depuis Pocket !", "error.pocket_access_token": "Impossible de récupérer le jeton d'accès depuis Pocket !",
"error.category_already_exists": "Cette catégorie existe déjà.", "error.category_already_exists": "Cette catégorie existe déjà.",
@ -308,6 +309,10 @@
"form.integration.fever_username": "Nom d'utilisateur pour l'API de Fever", "form.integration.fever_username": "Nom d'utilisateur pour l'API de Fever",
"form.integration.fever_password": "Mot de passe pour l'API de Fever", "form.integration.fever_password": "Mot de passe pour l'API de Fever",
"form.integration.fever_endpoint": "Point de terminaison de l'API Fever :", "form.integration.fever_endpoint": "Point de terminaison de l'API Fever :",
"form.integration.googlereader_activate": "Activer l'API de Google Reader",
"form.integration.googlereader_username": "Nom d'utilisateur pour l'API de Google Reader",
"form.integration.googlereader_password": "Mot de passe pour l'API de Google Reader",
"form.integration.googlereader_endpoint": "Point de terminaison de l'API Google Reader:",
"form.integration.pinboard_activate": "Sauvegarder les articles vers Pinboard", "form.integration.pinboard_activate": "Sauvegarder les articles vers Pinboard",
"form.integration.pinboard_token": "Jeton de sécurité de l'API de Pinboard", "form.integration.pinboard_token": "Jeton de sécurité de l'API de Pinboard",
"form.integration.pinboard_tags": "Libellés de Pinboard", "form.integration.pinboard_tags": "Libellés de Pinboard",

View File

@ -223,6 +223,7 @@
"error.unlink_account_without_password": "Devi scegliere una password altrimenti la prossima volta non riuscirai ad accedere.", "error.unlink_account_without_password": "Devi scegliere una password altrimenti la prossima volta non riuscirai ad accedere.",
"error.duplicate_linked_account": "Esiste già un account configurato per questo servizio!", "error.duplicate_linked_account": "Esiste già un account configurato per questo servizio!",
"error.duplicate_fever_username": "Esiste già un account Fever con lo stesso nome utente!", "error.duplicate_fever_username": "Esiste già un account Fever con lo stesso nome utente!",
"error.duplicate_googlereader_username": "Esiste già un account Google Reader con lo stesso nome utente!",
"error.pocket_request_token": "Non sono riuscito ad ottenere il request token da Pocket!", "error.pocket_request_token": "Non sono riuscito ad ottenere il request token da Pocket!",
"error.pocket_access_token": "Non sono riuscito ad ottenere l'access token da Pocket!", "error.pocket_access_token": "Non sono riuscito ad ottenere l'access token da Pocket!",
"error.category_already_exists": "Questa categoria esiste già.", "error.category_already_exists": "Questa categoria esiste già.",
@ -308,6 +309,10 @@
"form.integration.fever_username": "Nome utente dell'account Fever", "form.integration.fever_username": "Nome utente dell'account Fever",
"form.integration.fever_password": "Password dell'account Fever", "form.integration.fever_password": "Password dell'account Fever",
"form.integration.fever_endpoint": "Endpoint dell'API di Fever:", "form.integration.fever_endpoint": "Endpoint dell'API di Fever:",
"form.integration.googlereader_activate": "Abilita l'API di Google Reader",
"form.integration.googlereader_username": "Nome utente dell'account Google Reader",
"form.integration.googlereader_password": "Password dell'account Google Reader",
"form.integration.googlereader_endpoint": "Endpoint dell'API di Google Reader:",
"form.integration.pinboard_activate": "Salva gli articoli su Pinboard", "form.integration.pinboard_activate": "Salva gli articoli su Pinboard",
"form.integration.pinboard_token": "Token dell'API di Pinboard", "form.integration.pinboard_token": "Token dell'API di Pinboard",
"form.integration.pinboard_tags": "Tag di Pinboard", "form.integration.pinboard_tags": "Tag di Pinboard",

View File

@ -223,6 +223,7 @@
"error.unlink_account_without_password": "パスワードを設定しなければ再びログインすることはできません。", "error.unlink_account_without_password": "パスワードを設定しなければ再びログインすることはできません。",
"error.duplicate_linked_account": "別なユーザーが既にこのサービスの同じユーザーとリンクしています。", "error.duplicate_linked_account": "別なユーザーが既にこのサービスの同じユーザーとリンクしています。",
"error.duplicate_fever_username": "既に同じ名前の Fever ユーザー名が使われています!", "error.duplicate_fever_username": "既に同じ名前の Fever ユーザー名が使われています!",
"error.duplicate_googlereader_username": "既に同じ名前の Google Reader ユーザー名が使われています!",
"error.pocket_request_token": "Pocket の request token が取得できません!", "error.pocket_request_token": "Pocket の request token が取得できません!",
"error.pocket_access_token": "Pocket の access token が取得できません!", "error.pocket_access_token": "Pocket の access token が取得できません!",
"error.category_already_exists": "このカテゴリは既に存在しています。", "error.category_already_exists": "このカテゴリは既に存在しています。",
@ -308,6 +309,10 @@
"form.integration.fever_username": "Fever の ユーザー名", "form.integration.fever_username": "Fever の ユーザー名",
"form.integration.fever_password": "Fever の パスワード", "form.integration.fever_password": "Fever の パスワード",
"form.integration.fever_endpoint": "Fever API endpoint:", "form.integration.fever_endpoint": "Fever API endpoint:",
"form.integration.googlereader_activate": "Google Reader API を有効にする",
"form.integration.googlereader_username": "Google Reader の ユーザー名",
"form.integration.googlereader_password": "Google Reader の パスワード",
"form.integration.googlereader_endpoint": "Google Reader API endpoint:",
"form.integration.pinboard_activate": "Pinboard に記事を保存する", "form.integration.pinboard_activate": "Pinboard に記事を保存する",
"form.integration.pinboard_token": "Pinboard の API Token", "form.integration.pinboard_token": "Pinboard の API Token",
"form.integration.pinboard_tags": "Pinboard の Tag", "form.integration.pinboard_tags": "Pinboard の Tag",

View File

@ -223,6 +223,7 @@
"error.unlink_account_without_password": "U moet een wachtwoord definiëren anders kunt u zich niet opnieuw aanmelden.", "error.unlink_account_without_password": "U moet een wachtwoord definiëren anders kunt u zich niet opnieuw aanmelden.",
"error.duplicate_linked_account": "Er is al iemand geregistreerd met deze provider!", "error.duplicate_linked_account": "Er is al iemand geregistreerd met deze provider!",
"error.duplicate_fever_username": "Er is al iemand met dezelfde Fever gebruikersnaam!", "error.duplicate_fever_username": "Er is al iemand met dezelfde Fever gebruikersnaam!",
"error.duplicate_googlereader_username": "Er is al iemand met dezelfde Google Reader gebruikersnaam!",
"error.pocket_request_token": "Kon geen aanvraagtoken ophalen van Pocket!", "error.pocket_request_token": "Kon geen aanvraagtoken ophalen van Pocket!",
"error.pocket_access_token": "Kon geen toegangstoken ophalen van Pocket!", "error.pocket_access_token": "Kon geen toegangstoken ophalen van Pocket!",
"error.category_already_exists": "Deze categorie bestaat al.", "error.category_already_exists": "Deze categorie bestaat al.",
@ -308,6 +309,10 @@
"form.integration.fever_username": "Fever gebruikersnaam", "form.integration.fever_username": "Fever gebruikersnaam",
"form.integration.fever_password": "Fever wachtwoord", "form.integration.fever_password": "Fever wachtwoord",
"form.integration.fever_endpoint": "Fever URL:", "form.integration.fever_endpoint": "Fever URL:",
"form.integration.googlereader_activate": "Activeer Google Reader API",
"form.integration.googlereader_username": "Google Reader gebruikersnaam",
"form.integration.googlereader_password": "Google Reader wachtwoord",
"form.integration.googlereader_endpoint": "Google Reader URL:",
"form.integration.pinboard_activate": "Artikelen opslaan naar Pinboard", "form.integration.pinboard_activate": "Artikelen opslaan naar Pinboard",
"form.integration.pinboard_token": "Pinboard API token", "form.integration.pinboard_token": "Pinboard API token",
"form.integration.pinboard_tags": "Pinboard tags", "form.integration.pinboard_tags": "Pinboard tags",

View File

@ -225,6 +225,7 @@
"error.unlink_account_without_password": "Musisz zdefiniować hasło, inaczej nie będziesz mógł się ponownie zalogować.", "error.unlink_account_without_password": "Musisz zdefiniować hasło, inaczej nie będziesz mógł się ponownie zalogować.",
"error.duplicate_linked_account": "Już ktoś jest powiązany z tym dostawcą!", "error.duplicate_linked_account": "Już ktoś jest powiązany z tym dostawcą!",
"error.duplicate_fever_username": "Już ktoś inny używa tej nazwy użytkownika Fever!", "error.duplicate_fever_username": "Już ktoś inny używa tej nazwy użytkownika Fever!",
"error.duplicate_googlereader_username": "Już ktoś inny używa tej nazwy użytkownika Google Reader!",
"error.pocket_request_token": "Nie można pobrać tokena żądania z Pocket!", "error.pocket_request_token": "Nie można pobrać tokena żądania z Pocket!",
"error.pocket_access_token": "Nie można pobrać tokena dostępu z Pocket!", "error.pocket_access_token": "Nie można pobrać tokena dostępu z Pocket!",
"error.category_already_exists": "Ta kategoria już istnieje.", "error.category_already_exists": "Ta kategoria już istnieje.",
@ -310,6 +311,10 @@
"form.integration.fever_username": "Login do Fever", "form.integration.fever_username": "Login do Fever",
"form.integration.fever_password": "Hasło do Fever", "form.integration.fever_password": "Hasło do Fever",
"form.integration.fever_endpoint": "Punkt końcowy API gorączka:", "form.integration.fever_endpoint": "Punkt końcowy API gorączka:",
"form.integration.googlereader_activate": "Aktywuj Google Reader API",
"form.integration.googlereader_username": "Login do Google Reader",
"form.integration.googlereader_password": "Hasło do Google Reader",
"form.integration.googlereader_endpoint": "Punkt końcowy API gorączka:",
"form.integration.pinboard_activate": "Zapisz artykuł w Pinboard", "form.integration.pinboard_activate": "Zapisz artykuł w Pinboard",
"form.integration.pinboard_token": "Token Pinboard API", "form.integration.pinboard_token": "Token Pinboard API",
"form.integration.pinboard_tags": "Pinboard Tags", "form.integration.pinboard_tags": "Pinboard Tags",

View File

@ -223,6 +223,7 @@
"error.unlink_account_without_password": "Você deve definir uma senha, senão não será possível efetuar a sessão novamente.", "error.unlink_account_without_password": "Você deve definir uma senha, senão não será possível efetuar a sessão novamente.",
"error.duplicate_linked_account": "Alguém já está vinculado a esse serviço!", "error.duplicate_linked_account": "Alguém já está vinculado a esse serviço!",
"error.duplicate_fever_username": "Alguém já está utilizando esse nome de usuário do Fever!", "error.duplicate_fever_username": "Alguém já está utilizando esse nome de usuário do Fever!",
"error.duplicate_googlereader_username": "Alguém já está utilizando esse nome de usuário do Google Reader!",
"error.pocket_request_token": "Não foi possível obter um pedido de token no Pocket!", "error.pocket_request_token": "Não foi possível obter um pedido de token no Pocket!",
"error.pocket_access_token": "Não foi possível obter um token de acesso no Pocket!", "error.pocket_access_token": "Não foi possível obter um token de acesso no Pocket!",
"error.category_already_exists": "Esta categoria já existe.", "error.category_already_exists": "Esta categoria já existe.",
@ -308,6 +309,10 @@
"form.integration.fever_username": "Nome de usuário do Fever", "form.integration.fever_username": "Nome de usuário do Fever",
"form.integration.fever_password": "Senha do Fever", "form.integration.fever_password": "Senha do Fever",
"form.integration.fever_endpoint": "Endpoint da API do Fever:", "form.integration.fever_endpoint": "Endpoint da API do Fever:",
"form.integration.googlereader_activate": "Ativar API do Google Reader",
"form.integration.googlereader_username": "Nome de usuário do Google Reader",
"form.integration.googlereader_password": "Senha do Google Reader",
"form.integration.googlereader_endpoint": "Endpoint da API do Google Reader:",
"form.integration.pinboard_activate": "Salvar itens no Pinboard", "form.integration.pinboard_activate": "Salvar itens no Pinboard",
"form.integration.pinboard_token": "Token de API do Pinboard", "form.integration.pinboard_token": "Token de API do Pinboard",
"form.integration.pinboard_tags": "Etiquetas (tags) do Pinboard", "form.integration.pinboard_tags": "Etiquetas (tags) do Pinboard",

View File

@ -225,6 +225,7 @@
"error.unlink_account_without_password": "Вы должны установить пароль, иначе вы не сможете войти снова.", "error.unlink_account_without_password": "Вы должны установить пароль, иначе вы не сможете войти снова.",
"error.duplicate_linked_account": "Уже есть кто-то, кто ассоциирован с этим аккаунтом!", "error.duplicate_linked_account": "Уже есть кто-то, кто ассоциирован с этим аккаунтом!",
"error.duplicate_fever_username": "Уже есть кто-то с таким же именем пользователя Fever!", "error.duplicate_fever_username": "Уже есть кто-то с таким же именем пользователя Fever!",
"error.duplicate_googlereader_username": "Уже есть кто-то с таким же именем пользователя Google Reader!",
"error.pocket_request_token": "Не удается извлечь request token из Pocket!", "error.pocket_request_token": "Не удается извлечь request token из Pocket!",
"error.pocket_access_token": "Не удается извлечь access token из Pocket!", "error.pocket_access_token": "Не удается извлечь access token из Pocket!",
"error.category_already_exists": "Эта категория уже существует.", "error.category_already_exists": "Эта категория уже существует.",
@ -310,6 +311,10 @@
"form.integration.fever_username": "Имя пользователя Fever", "form.integration.fever_username": "Имя пользователя Fever",
"form.integration.fever_password": "Пароль Fever", "form.integration.fever_password": "Пароль Fever",
"form.integration.fever_endpoint": "Конечная точка Fever API:", "form.integration.fever_endpoint": "Конечная точка Fever API:",
"form.integration.googlereader_activate": "Активировать Google Reader API",
"form.integration.googlereader_username": "Имя пользователя Google Reader",
"form.integration.googlereader_password": "Пароль Google Reader",
"form.integration.googlereader_endpoint": "Конечная точка Google Reader API:",
"form.integration.pinboard_activate": "Сохранять статьи в Pinboard", "form.integration.pinboard_activate": "Сохранять статьи в Pinboard",
"form.integration.pinboard_token": "Pinboard API Token", "form.integration.pinboard_token": "Pinboard API Token",
"form.integration.pinboard_tags": "Теги Pinboard", "form.integration.pinboard_tags": "Теги Pinboard",

View File

@ -223,6 +223,7 @@
"error.unlink_account_without_password": "Bir şifre belirlemelisiniz, aksi takdirde tekrar oturum açamazsınız.", "error.unlink_account_without_password": "Bir şifre belirlemelisiniz, aksi takdirde tekrar oturum açamazsınız.",
"error.duplicate_linked_account": "Bu sağlayıcıyla ilişkilendirilmiş biri zaten var!", "error.duplicate_linked_account": "Bu sağlayıcıyla ilişkilendirilmiş biri zaten var!",
"error.duplicate_fever_username": "Aynı Fever kullanıcı adına sahip başka biri zaten var!", "error.duplicate_fever_username": "Aynı Fever kullanıcı adına sahip başka biri zaten var!",
"error.duplicate_googlereader_username": "Aynı Google Reader kullanıcı adına sahip başka biri zaten var!",
"error.pocket_request_token": "Pocket'tan istek tokeni alınamıyor!", "error.pocket_request_token": "Pocket'tan istek tokeni alınamıyor!",
"error.pocket_access_token": "Pocket'tan erişim tokeni alınamıyor!", "error.pocket_access_token": "Pocket'tan erişim tokeni alınamıyor!",
"error.category_already_exists": "Bu kategori zaten mevcut.", "error.category_already_exists": "Bu kategori zaten mevcut.",
@ -308,6 +309,10 @@
"form.integration.fever_username": "Fever Kullanıcı Adı", "form.integration.fever_username": "Fever Kullanıcı Adı",
"form.integration.fever_password": "Fever Parolası", "form.integration.fever_password": "Fever Parolası",
"form.integration.fever_endpoint": "Fever API uç noktası:", "form.integration.fever_endpoint": "Fever API uç noktası:",
"form.integration.googlereader_activate": "Google Reader API'yi Etkinleştir",
"form.integration.googlereader_username": "Google Reader Kullanıcı Adı",
"form.integration.googlereader_password": "Google Reader Parolası",
"form.integration.googlereader_endpoint": "Google Reader API uç noktası:",
"form.integration.pinboard_activate": "Makaleleri Pinboard'a kaydet", "form.integration.pinboard_activate": "Makaleleri Pinboard'a kaydet",
"form.integration.pinboard_token": "Pinboard API Token", "form.integration.pinboard_token": "Pinboard API Token",
"form.integration.pinboard_tags": "Pinboard Etiketleri", "form.integration.pinboard_tags": "Pinboard Etiketleri",
@ -361,4 +366,4 @@
"%d yıl önce", "%d yıl önce",
"%d yıl önce" "%d yıl önce"
] ]
} }

View File

@ -221,6 +221,7 @@
"error.unlink_account_without_password": "您必须设置密码,否则您将无法再次登录。", "error.unlink_account_without_password": "您必须设置密码,否则您将无法再次登录。",
"error.duplicate_linked_account": "该 Provider 已被关联!", "error.duplicate_linked_account": "该 Provider 已被关联!",
"error.duplicate_fever_username": "Fever 用户名已被占用!", "error.duplicate_fever_username": "Fever 用户名已被占用!",
"error.duplicate_googlereader_username": "Google Reader 用户名已被占用!",
"error.pocket_request_token": "无法从 Pocket 获取请求令牌!", "error.pocket_request_token": "无法从 Pocket 获取请求令牌!",
"error.pocket_access_token": "无法从 Pocket 获取访问令牌!", "error.pocket_access_token": "无法从 Pocket 获取访问令牌!",
"error.category_already_exists": "分类已存在", "error.category_already_exists": "分类已存在",
@ -306,6 +307,10 @@
"form.integration.fever_username": "Fever 用户名", "form.integration.fever_username": "Fever 用户名",
"form.integration.fever_password": "Fever 密码", "form.integration.fever_password": "Fever 密码",
"form.integration.fever_endpoint": "Fever API 端点", "form.integration.fever_endpoint": "Fever API 端点",
"form.integration.googlereader_activate": "启用 Google Reader API",
"form.integration.googlereader_username": "Google Reader 用户名",
"form.integration.googlereader_password": "Google Reader 密码",
"form.integration.googlereader_endpoint": "Google Reader API 端点:",
"form.integration.pinboard_activate": "保存文章到 Pinboard", "form.integration.pinboard_activate": "保存文章到 Pinboard",
"form.integration.pinboard_token": "Pinboard API Token", "form.integration.pinboard_token": "Pinboard API Token",
"form.integration.pinboard_tags": "Pinboard 标签", "form.integration.pinboard_tags": "Pinboard 标签",

View File

@ -17,6 +17,9 @@ type Integration struct {
FeverEnabled bool FeverEnabled bool
FeverUsername string FeverUsername string
FeverToken string FeverToken string
GoogleReaderEnabled bool
GoogleReaderUsername string
GoogleReaderPassword string
WallabagEnabled bool WallabagEnabled bool
WallabagURL string WallabagURL string
WallabagClientID string WallabagClientID string

View File

@ -16,6 +16,7 @@ import (
"miniflux.app/api" "miniflux.app/api"
"miniflux.app/config" "miniflux.app/config"
"miniflux.app/fever" "miniflux.app/fever"
"miniflux.app/googlereader"
"miniflux.app/http/request" "miniflux.app/http/request"
"miniflux.app/logger" "miniflux.app/logger"
"miniflux.app/storage" "miniflux.app/storage"
@ -180,6 +181,7 @@ func setupHandler(store *storage.Storage, pool *worker.Pool) *mux.Router {
router.Use(middleware) router.Use(middleware)
fever.Serve(router, store) fever.Serve(router, store)
googlereader.Serve(router, store)
api.Serve(router, store, pool) api.Serve(router, store, pool)
ui.Serve(router, store, pool) ui.Serve(router, store, pool)

View File

@ -9,6 +9,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/lib/pq"
"miniflux.app/model" "miniflux.app/model"
) )
@ -215,3 +216,51 @@ func (s *Storage) RemoveCategory(userID, categoryID int64) error {
return nil return nil
} }
// delete the given categories, replacing those categories with the user's first
// category on affected feeds
func (s *Storage) RemoveAndReplaceCategoriesByName(userid int64, titles []string) error {
tx, err := s.db.Begin()
if err != nil {
return errors.New("unable to begin transaction")
}
titleParam := pq.Array(titles)
var count int
query := "SELECT count(*) FROM categories WHERE user_id = $1 and title != ANY($2)"
err = tx.QueryRow(query, userid, titleParam).Scan(&count)
if err != nil {
tx.Rollback()
return errors.New("unable to retrieve category count")
}
if count < 1 {
tx.Rollback()
return errors.New("at least 1 category must remain after deletion")
}
query = `
WITH d_cats AS (SELECT id FROM categories WHERE user_id = $1 AND title = ANY($2))
UPDATE feeds
SET category_id =
(SELECT id
FROM categories
WHERE user_id = $1 AND id NOT IN (SELECT id FROM d_cats)
ORDER BY title ASC
LIMIT 1)
WHERE user_id = $1 AND category_id IN (SELECT id FROM d_cats)
`
_, err = tx.Exec(query, userid, titleParam)
if err != nil {
tx.Rollback()
return fmt.Errorf("unable to replace categories: %v", err)
}
query = "DELETE FROM categories WHERE user_id = $1 AND title = ANY($2)"
_, err = tx.Exec(query, userid, titleParam)
if err != nil {
tx.Rollback()
return fmt.Errorf("unable to delete categories: %v", err)
}
tx.Commit()
return nil
}

View File

@ -371,6 +371,26 @@ func (s *Storage) SetEntriesStatusCount(userID int64, entryIDs []int64, status s
return visible, nil return visible, nil
} }
// SetEntriesBookmarked update the bookmarked state for the given list of entries.
func (s *Storage) SetEntriesBookmarkedState(userID int64, entryIDs []int64, starred bool) error {
query := `UPDATE entries SET starred=$1, changed_at=now() WHERE user_id=$2 AND id=ANY($3)`
result, err := s.db.Exec(query, starred, userID, pq.Array(entryIDs))
if err != nil {
return fmt.Errorf(`store: unable to update the bookmarked state %v: %v`, entryIDs, err)
}
count, err := result.RowsAffected()
if err != nil {
return fmt.Errorf(`store: unable to update these entries %v: %v`, entryIDs, err)
}
if count == 0 {
return errors.New(`store: nothing has been updated`)
}
return nil
}
// ToggleBookmark toggles entry bookmark value. // ToggleBookmark toggles entry bookmark value.
func (s *Storage) ToggleBookmark(userID int64, entryID int64) error { func (s *Storage) ToggleBookmark(userID int64, entryID int64) error {
query := `UPDATE entries SET starred = NOT starred, changed_at=now() WHERE user_id=$1 AND id=$2` query := `UPDATE entries SET starred = NOT starred, changed_at=now() WHERE user_id=$1 AND id=$2`

View File

@ -8,6 +8,7 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"golang.org/x/crypto/bcrypt"
"miniflux.app/model" "miniflux.app/model"
) )
@ -19,6 +20,14 @@ func (s *Storage) HasDuplicateFeverUsername(userID int64, feverUsername string)
return result return result
} }
// HasDuplicateGoogleReaderUsername checks if another user have the same googlereader username.
func (s *Storage) HasDuplicateGoogleReaderUsername(userID int64, googleReaderUsername string) bool {
query := `SELECT true FROM integrations WHERE user_id != $1 AND googlereader_username=$2`
var result bool
s.db.QueryRow(query, userID, googleReaderUsername).Scan(&result)
return result
}
// UserByFeverToken returns a user by using the Fever API token. // UserByFeverToken returns a user by using the Fever API token.
func (s *Storage) UserByFeverToken(token string) (*model.User, error) { func (s *Storage) UserByFeverToken(token string) (*model.User, error) {
query := ` query := `
@ -42,6 +51,57 @@ func (s *Storage) UserByFeverToken(token string) (*model.User, error) {
} }
} }
// GoogleReaderUserCheckPassword validates the hashed password.
func (s *Storage) GoogleReaderUserCheckPassword(username, password string) error {
var hash string
query := `
SELECT
googlereader_password
FROM integrations
WHERE
integrations.googlereader_enabled='t' AND integrations.googlereader_username=$1
`
err := s.db.QueryRow(query, username).Scan(&hash)
if err == sql.ErrNoRows {
return fmt.Errorf(`store: unable to find this user: %s`, username)
} else if err != nil {
return fmt.Errorf(`store: unable to fetch user: %v`, err)
}
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil {
return fmt.Errorf(`store: invalid password for "%s" (%v)`, username, err)
}
return nil
}
// GoogleReaderUserGetIntegration returns part of the google reader parts of the integration struct.
func (s *Storage) GoogleReaderUserGetIntegration(username string) (*model.Integration, error) {
var integration model.Integration
query := `
SELECT
user_id,
googlereader_enabled,
googlereader_username,
googlereader_password
FROM integrations
WHERE
integrations.googlereader_enabled='t' AND integrations.googlereader_username=$1
`
err := s.db.QueryRow(query, username).Scan(&integration.UserID, &integration.GoogleReaderEnabled, &integration.GoogleReaderUsername, &integration.GoogleReaderPassword)
if err == sql.ErrNoRows {
return &integration, fmt.Errorf(`store: unable to find this user: %s`, username)
} else if err != nil {
return &integration, fmt.Errorf(`store: unable to fetch user: %v`, err)
}
return &integration, nil
}
// Integration returns user integration settings. // Integration returns user integration settings.
func (s *Storage) Integration(userID int64) (*model.Integration, error) { func (s *Storage) Integration(userID int64) (*model.Integration, error) {
query := ` query := `
@ -57,6 +117,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
fever_enabled, fever_enabled,
fever_username, fever_username,
fever_token, fever_token,
googlereader_enabled,
googlereader_username,
googlereader_password,
wallabag_enabled, wallabag_enabled,
wallabag_url, wallabag_url,
wallabag_client_id, wallabag_client_id,
@ -90,6 +153,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
&integration.FeverEnabled, &integration.FeverEnabled,
&integration.FeverUsername, &integration.FeverUsername,
&integration.FeverToken, &integration.FeverToken,
&integration.GoogleReaderEnabled,
&integration.GoogleReaderUsername,
&integration.GoogleReaderPassword,
&integration.WallabagEnabled, &integration.WallabagEnabled,
&integration.WallabagURL, &integration.WallabagURL,
&integration.WallabagClientID, &integration.WallabagClientID,
@ -118,7 +184,13 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
// UpdateIntegration saves user integration settings. // UpdateIntegration saves user integration settings.
func (s *Storage) UpdateIntegration(integration *model.Integration) error { func (s *Storage) UpdateIntegration(integration *model.Integration) error {
query := ` var err error
if integration.GoogleReaderPassword != "" {
integration.GoogleReaderPassword, err = hashPassword(integration.GoogleReaderPassword)
if err != nil {
return err
}
query := `
UPDATE UPDATE
integrations integrations
SET SET
@ -144,41 +216,116 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
pocket_enabled=$20, pocket_enabled=$20,
pocket_access_token=$21, pocket_access_token=$21,
pocket_consumer_key=$22, pocket_consumer_key=$22,
telegram_bot_enabled=$23, googlereader_enabled=$23,
telegram_bot_token=$24, googlereader_username=$24,
telegram_bot_chat_id=$25 googlereader_password=$25,
telegram_bot_enabled=$26,
telegram_bot_token=$27,
telegram_bot_chat_id=$28
WHERE WHERE
user_id=$26 user_id=$29
` `
_, err := s.db.Exec( _, err = s.db.Exec(
query, query,
integration.PinboardEnabled, integration.PinboardEnabled,
integration.PinboardToken, integration.PinboardToken,
integration.PinboardTags, integration.PinboardTags,
integration.PinboardMarkAsUnread, integration.PinboardMarkAsUnread,
integration.InstapaperEnabled, integration.InstapaperEnabled,
integration.InstapaperUsername, integration.InstapaperUsername,
integration.InstapaperPassword, integration.InstapaperPassword,
integration.FeverEnabled, integration.FeverEnabled,
integration.FeverUsername, integration.FeverUsername,
integration.FeverToken, integration.FeverToken,
integration.WallabagEnabled, integration.WallabagEnabled,
integration.WallabagURL, integration.WallabagURL,
integration.WallabagClientID, integration.WallabagClientID,
integration.WallabagClientSecret, integration.WallabagClientSecret,
integration.WallabagUsername, integration.WallabagUsername,
integration.WallabagPassword, integration.WallabagPassword,
integration.NunuxKeeperEnabled, integration.NunuxKeeperEnabled,
integration.NunuxKeeperURL, integration.NunuxKeeperURL,
integration.NunuxKeeperAPIKey, integration.NunuxKeeperAPIKey,
integration.PocketEnabled, integration.PocketEnabled,
integration.PocketAccessToken, integration.PocketAccessToken,
integration.PocketConsumerKey, integration.PocketConsumerKey,
integration.TelegramBotEnabled, integration.GoogleReaderEnabled,
integration.TelegramBotToken, integration.GoogleReaderUsername,
integration.TelegramBotChatID, integration.GoogleReaderPassword,
integration.UserID, integration.TelegramBotEnabled,
) integration.TelegramBotToken,
integration.TelegramBotChatID,
integration.UserID,
)
} else {
query := `
UPDATE
integrations
SET
pinboard_enabled=$1,
pinboard_token=$2,
pinboard_tags=$3,
pinboard_mark_as_unread=$4,
instapaper_enabled=$5,
instapaper_username=$6,
instapaper_password=$7,
fever_enabled=$8,
fever_username=$9,
fever_token=$10,
wallabag_enabled=$11,
wallabag_url=$12,
wallabag_client_id=$13,
wallabag_client_secret=$14,
wallabag_username=$15,
wallabag_password=$16,
nunux_keeper_enabled=$17,
nunux_keeper_url=$18,
nunux_keeper_api_key=$19,
pocket_enabled=$20,
pocket_access_token=$21,
pocket_consumer_key=$22,
googlereader_enabled=$23,
googlereader_username=$24,
googlereader_password=$25,
telegram_bot_enabled=$26,
telegram_bot_token=$27,
telegram_bot_chat_id=$28
WHERE
user_id=$29
`
_, err = s.db.Exec(
query,
integration.PinboardEnabled,
integration.PinboardToken,
integration.PinboardTags,
integration.PinboardMarkAsUnread,
integration.InstapaperEnabled,
integration.InstapaperUsername,
integration.InstapaperPassword,
integration.FeverEnabled,
integration.FeverUsername,
integration.FeverToken,
integration.WallabagEnabled,
integration.WallabagURL,
integration.WallabagClientID,
integration.WallabagClientSecret,
integration.WallabagUsername,
integration.WallabagPassword,
integration.NunuxKeeperEnabled,
integration.NunuxKeeperURL,
integration.NunuxKeeperAPIKey,
integration.PocketEnabled,
integration.PocketAccessToken,
integration.PocketConsumerKey,
integration.GoogleReaderEnabled,
integration.GoogleReaderUsername,
integration.GoogleReaderPassword,
integration.TelegramBotEnabled,
integration.TelegramBotToken,
integration.TelegramBotChatID,
integration.UserID,
)
}
if err != nil { if err != nil {
return fmt.Errorf(`store: unable to update integration row: %v`, err) return fmt.Errorf(`store: unable to update integration row: %v`, err)

View File

@ -31,7 +31,27 @@
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button> <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div> </div>
</div> </div>
<h3>Google Reader</h3>
<div class="form-section">
<label>
<input type="checkbox" name="googlereader_enabled" value="1" {{ if .form.GoogleReaderEnabled }}checked{{ end }}> {{ t "form.integration.googlereader_activate" }}
</label>
<label for="form-googlereader-username">{{ t "form.integration.googlereader_username" }}</label>
<input type="text" name="googlereader_username" id="form-googlereader-username" value="{{ .form.GoogleReaderUsername }}" autocomplete="username" spellcheck="false">
<label for="form-googlereader-password">{{ t "form.integration.googlereader_password" }}</label>
<input type="password" name="googlereader_password" id="form-googlereader-password" value="{{ .form.GoogleReaderPassword }}" autocomplete="new-password">
<p>{{ t "form.integration.googlereader_endpoint" }} <strong>{{ rootURL }}{{ route "login" }}</strong></p>
<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div>
</div>
<!-- -->
<h3>Pinboard</h3> <h3>Pinboard</h3>
<div class="form-section"> <div class="form-section">
<label> <label>

View File

@ -22,6 +22,9 @@ type IntegrationForm struct {
FeverEnabled bool FeverEnabled bool
FeverUsername string FeverUsername string
FeverPassword string FeverPassword string
GoogleReaderEnabled bool
GoogleReaderUsername string
GoogleReaderPassword string
WallabagEnabled bool WallabagEnabled bool
WallabagURL string WallabagURL string
WallabagClientID string WallabagClientID string
@ -50,6 +53,8 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
integration.InstapaperPassword = i.InstapaperPassword integration.InstapaperPassword = i.InstapaperPassword
integration.FeverEnabled = i.FeverEnabled integration.FeverEnabled = i.FeverEnabled
integration.FeverUsername = i.FeverUsername integration.FeverUsername = i.FeverUsername
integration.GoogleReaderEnabled = i.GoogleReaderEnabled
integration.GoogleReaderUsername = i.GoogleReaderUsername
integration.WallabagEnabled = i.WallabagEnabled integration.WallabagEnabled = i.WallabagEnabled
integration.WallabagURL = i.WallabagURL integration.WallabagURL = i.WallabagURL
integration.WallabagClientID = i.WallabagClientID integration.WallabagClientID = i.WallabagClientID
@ -67,7 +72,7 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
integration.TelegramBotChatID = i.TelegramBotChatID integration.TelegramBotChatID = i.TelegramBotChatID
} }
// NewIntegrationForm returns a new AuthForm. // NewIntegrationForm returns a new IntegrationForm.
func NewIntegrationForm(r *http.Request) *IntegrationForm { func NewIntegrationForm(r *http.Request) *IntegrationForm {
return &IntegrationForm{ return &IntegrationForm{
PinboardEnabled: r.FormValue("pinboard_enabled") == "1", PinboardEnabled: r.FormValue("pinboard_enabled") == "1",
@ -80,6 +85,9 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
FeverEnabled: r.FormValue("fever_enabled") == "1", FeverEnabled: r.FormValue("fever_enabled") == "1",
FeverUsername: r.FormValue("fever_username"), FeverUsername: r.FormValue("fever_username"),
FeverPassword: r.FormValue("fever_password"), FeverPassword: r.FormValue("fever_password"),
GoogleReaderEnabled: r.FormValue("googlereader_enabled") == "1",
GoogleReaderUsername: r.FormValue("googlereader_username"),
GoogleReaderPassword: r.FormValue("googlereader_password"),
WallabagEnabled: r.FormValue("wallabag_enabled") == "1", WallabagEnabled: r.FormValue("wallabag_enabled") == "1",
WallabagURL: r.FormValue("wallabag_url"), WallabagURL: r.FormValue("wallabag_url"),
WallabagClientID: r.FormValue("wallabag_client_id"), WallabagClientID: r.FormValue("wallabag_client_id"),

View File

@ -38,6 +38,8 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
InstapaperPassword: integration.InstapaperPassword, InstapaperPassword: integration.InstapaperPassword,
FeverEnabled: integration.FeverEnabled, FeverEnabled: integration.FeverEnabled,
FeverUsername: integration.FeverUsername, FeverUsername: integration.FeverUsername,
GoogleReaderEnabled: integration.GoogleReaderEnabled,
GoogleReaderUsername: integration.GoogleReaderUsername,
WallabagEnabled: integration.WallabagEnabled, WallabagEnabled: integration.WallabagEnabled,
WallabagURL: integration.WallabagURL, WallabagURL: integration.WallabagURL,
WallabagClientID: integration.WallabagClientID, WallabagClientID: integration.WallabagClientID,

View File

@ -49,6 +49,19 @@ func (h *handler) updateIntegration(w http.ResponseWriter, r *http.Request) {
integration.FeverToken = "" integration.FeverToken = ""
} }
if integration.GoogleReaderUsername != "" && h.store.HasDuplicateGoogleReaderUsername(user.ID, integration.GoogleReaderUsername) {
sess.NewFlashErrorMessage(printer.Printf("error.duplicate_googlereader_username"))
html.Redirect(w, r, route.Path(h.router, "integrations"))
return
}
if integration.GoogleReaderEnabled {
if integrationForm.GoogleReaderPassword != "" {
integration.GoogleReaderPassword = integrationForm.GoogleReaderPassword
}
} else {
integration.GoogleReaderPassword = ""
}
err = h.store.UpdateIntegration(integration) err = h.store.UpdateIntegration(integration)
if err != nil { if err != nil {
html.ServerError(w, r, err) html.ServerError(w, r, err)