diff --git a/client/core.go b/client/core.go index 2958ce02..5f5bbdf6 100644 --- a/client/core.go +++ b/client/core.go @@ -133,6 +133,7 @@ type Entry struct { Date time.Time `json:"published_at"` Content string `json:"content"` Author string `json:"author"` + ShareCode string `json:"share_code"` Starred bool `json:"starred"` Enclosures Enclosures `json:"enclosures,omitempty"` Feed *Feed `json:"feed,omitempty"` diff --git a/crypto/crypto.go b/crypto/crypto.go index fa236ab4..c06cd307 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -8,6 +8,7 @@ import ( "crypto/rand" "crypto/sha256" "encoding/base64" + "encoding/hex" "fmt" ) @@ -36,3 +37,8 @@ func GenerateRandomBytes(size int) []byte { func GenerateRandomString(size int) string { return base64.URLEncoding.EncodeToString(GenerateRandomBytes(size)) } + +// GenerateRandomStringHex returns a random hexadecimal string. +func GenerateRandomStringHex(size int) string { + return hex.EncodeToString(GenerateRandomBytes(size)) +} diff --git a/database/migration.go b/database/migration.go index fff815bc..c9abf0e8 100644 --- a/database/migration.go +++ b/database/migration.go @@ -12,7 +12,7 @@ import ( "miniflux.app/logger" ) -const schemaVersion = 27 +const schemaVersion = 28 // Migrate executes database migrations. func Migrate(db *sql.DB) { diff --git a/database/sql.go b/database/sql.go index d79a4529..cd3f418c 100644 --- a/database/sql.go +++ b/database/sql.go @@ -167,6 +167,9 @@ alter table entries alter column changed_at set not null; primary key(id), unique (user_id, description) ); +`, + "schema_version_28": `alter table entries add column share_code text not null default ''; +create unique index entries_share_code_idx on entries using btree(share_code) where share_code <> ''; `, "schema_version_3": `create table tokens ( id text not null, @@ -223,6 +226,7 @@ var SqlMapChecksums = map[string]string{ "schema_version_25": "5262d2d4c88d637b6603a1fcd4f68ad257bd59bd1adf89c58a18ee87b12050d7", "schema_version_26": "64f14add40691f18f514ac0eed10cd9b19c83a35e5c3d8e0bce667e0ceca9094", "schema_version_27": "4235396b37fd7f52ff6f7526416042bb1649701233e2d99f0bcd583834a0a967", + "schema_version_28": "a64b5ba0b37fe3f209617b7d0e4dd05018d2b8362d2c9c528ba8cce19b77e326", "schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12", "schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9", "schema_version_5": "46397e2f5f2c82116786127e9f6a403e975b14d2ca7b652a48cd1ba843e6a27c", diff --git a/database/sql/schema_version_28.sql b/database/sql/schema_version_28.sql new file mode 100644 index 00000000..fed34cfa --- /dev/null +++ b/database/sql/schema_version_28.sql @@ -0,0 +1,2 @@ +alter table entries add column share_code text not null default ''; +create unique index entries_share_code_idx on entries using btree(share_code) where share_code <> ''; diff --git a/locale/translations.go b/locale/translations.go index 2acbc1ab..0691a1a2 100644 --- a/locale/translations.go +++ b/locale/translations.go @@ -76,6 +76,8 @@ var translations = map[string]string{ "entry.original.label": "Original-Artikel", "entry.comments.label": "Kommentare", "entry.comments.title": "Kommentare anzeigen", + "entry.share.label": "Teilen", + "entry.share.title": "Diesen Artikel teilen", "page.unread.title": "Ungelesen", "page.starred.title": "Lesezeichen", "page.categories.title": "Kategorien", @@ -402,6 +404,8 @@ var translations = map[string]string{ "entry.original.label": "Original", "entry.comments.label": "Comments", "entry.comments.title": "View Comments", + "entry.share.label": "Share", + "entry.share.title": "Share this article", "page.unread.title": "Unread", "page.starred.title": "Starred", "page.categories.title": "Categories", @@ -708,6 +712,8 @@ var translations = map[string]string{ "entry.original.label": "Original", "entry.comments.label": "Comentarios", "entry.comments.title": "Ver comentarios", + "entry.share.label": "Comparta", + "entry.share.title": "Comparta este articulo", "page.unread.title": "No leídos", "page.starred.title": "Marcadores", "page.categories.title": "Categorias", @@ -1014,6 +1020,8 @@ var translations = map[string]string{ "entry.original.label": "Original", "entry.comments.label": "Commentaires", "entry.comments.title": "Voir les commentaires", + "entry.share.label": "Partager", + "entry.share.title": "Partager cet article", "page.unread.title": "Non lus", "page.starred.title": "Favoris", "page.categories.title": "Catégories", @@ -1340,6 +1348,8 @@ var translations = map[string]string{ "entry.original.label": "Contenuto originale", "entry.comments.label": "Commenti", "entry.comments.title": "Mostra i commenti", + "entry.share.label": "Condividi", + "entry.share.title": "Condividi questo articolo", "page.unread.title": "Da leggere", "page.starred.title": "Preferiti", "page.categories.title": "Categorie", @@ -1646,6 +1656,8 @@ var translations = map[string]string{ "entry.original.label": "オリジナル", "entry.comments.label": "コメント", "entry.comments.title": "コメントを見る", + "entry.share.label": "共有", + "entry.share.title": "この記事を共有する", "page.unread.title": "未読", "page.starred.title": "星付き", "page.categories.title": "カテゴリ", @@ -1952,6 +1964,8 @@ var translations = map[string]string{ "entry.original.label": "Origineel", "entry.comments.label": "Comments", "entry.comments.title": "Bekijk de reacties", + "entry.share.label": "Deel", + "entry.share.title": "Deel dit artikel", "page.unread.title": "Ongelezen", "page.starred.title": "Favorieten", "page.categories.title": "Categorieën", @@ -2276,6 +2290,8 @@ var translations = map[string]string{ "entry.original.label": "Oryginalny artykuł", "entry.comments.label": "Komentarze", "entry.comments.title": "Zobacz komentarze", + "entry.share.label": "Podzielić się", + "entry.share.title": "Podzielić się ten artykuł", "page.unread.title": "Nieprzeczytane", "page.starred.title": "Oznaczone gwiazdką", "page.categories.title": "Kategorie", @@ -2608,6 +2624,8 @@ var translations = map[string]string{ "entry.original.label": "Оригинал", "entry.comments.label": "Комментарии", "entry.comments.title": "Показать комментарии", + "entry.share.label": "поделиться", + "entry.share.title": "поделиться эту статью", "page.unread.title": "Непрочитанное", "page.starred.title": "Избранное", "page.categories.title": "Категории", @@ -2922,6 +2940,8 @@ var translations = map[string]string{ "entry.original.label": "原始内容", "entry.comments.label": "评论", "entry.comments.title": "查看评论", + "entry.share.label": "分享", + "entry.share.title": "分享这篇文章", "page.unread.title": "未读", "page.starred.title": "星标", "page.categories.title": "分类", @@ -3169,14 +3189,14 @@ var translations = map[string]string{ } var translationsChecksums = map[string]string{ - "de_DE": "cc826a57cf4bf789df38db4f50626ad8c1c2b84ce34075c2c04de3d1f0dcd2d5", - "en_US": "f7e6db53cdbc2c0d959ac231dbacf0ef4d0ed81248944c4a4f8b83ef000f5349", - "es_ES": "cc727f62eef3a6cba51b65253d70a50161af35bf9c5366281b7984b2fc189961", - "fr_FR": "d3d1a4bf9aa8e4e24bae2f117507dcfc3cf00660a73b44a6c42356e8dbab8ae8", - "it_IT": "5ded991f2c70ec2268e6053bd84a77cf4136ebaea42013d3e79d594f38abb1b3", - "ja_JP": "110d7a7b1c888282b031de340e3318a62cdd62076b05a7fb49759f554c6dbe76", - "nl_NL": "a934ab4b1eff85580425a5859c31fcb227ae8926deba74df4e42b5d4feb67826", - "pl_PL": "6e80c36788723b9a7ff3f372e13a55c68d153727ec0abb56663cadbf6d6e1d9f", - "ru_RU": "d56f9e31f63731d23ce1ea2a8a4cb019f3ab282b23a1f494c47061daea523587", - "zh_CN": "4a5ca40790fceab88257f6742dc05294b79142bee8aad6fc87fbd479d1941292", + "de_DE": "7360a69e038d71e00f64c03891401cd517779687d46a907688f4a9a7b6205146", + "en_US": "92dda79899a673652a43fd8d61c893749713af09909ca03ac6fea06ac617d361", + "es_ES": "813b8cd42907dfbc19ff51f3367e0dbb013d373b013d7854df512e846652ff21", + "fr_FR": "279c52bbf682949cf8782e7e81f2bf5cfd300cebf577d51ce9436d44aaaf6323", + "it_IT": "5e8408e9aee142e1bd7e73f2a91ae96bc9ad0ab61c20416ad9e93b6fe505e8a9", + "ja_JP": "508025c0c7e7f57195ae011c4499ab58a85d043c828565c1740df879fb2376c1", + "nl_NL": "e621a5e7408928624a060a832d9fc36b74026221bd7b07894a4cce267be3cdd1", + "pl_PL": "2383c1a9be451557fe601f346e30bb165a88d9c00d17909a9c747d64864a423d", + "ru_RU": "d7ad59bbd7a150af9d476c4c3034eb85762de7381e2925d75e373584ed45c725", + "zh_CN": "e5f169a3c83c9bd7a41e9737e001e58fec243eee7aa23a71d37bfa8e05d92860", } diff --git a/locale/translations/de_DE.json b/locale/translations/de_DE.json index 07543b6f..bf34e833 100644 --- a/locale/translations/de_DE.json +++ b/locale/translations/de_DE.json @@ -71,6 +71,8 @@ "entry.original.label": "Original-Artikel", "entry.comments.label": "Kommentare", "entry.comments.title": "Kommentare anzeigen", + "entry.share.label": "Teilen", + "entry.share.title": "Diesen Artikel teilen", "page.unread.title": "Ungelesen", "page.starred.title": "Lesezeichen", "page.categories.title": "Kategorien", diff --git a/locale/translations/en_US.json b/locale/translations/en_US.json index c3dc3b34..7b7ea63e 100644 --- a/locale/translations/en_US.json +++ b/locale/translations/en_US.json @@ -71,6 +71,8 @@ "entry.original.label": "Original", "entry.comments.label": "Comments", "entry.comments.title": "View Comments", + "entry.share.label": "Share", + "entry.share.title": "Share this article", "page.unread.title": "Unread", "page.starred.title": "Starred", "page.categories.title": "Categories", diff --git a/locale/translations/es_ES.json b/locale/translations/es_ES.json index d1545e5c..dae98986 100644 --- a/locale/translations/es_ES.json +++ b/locale/translations/es_ES.json @@ -71,6 +71,8 @@ "entry.original.label": "Original", "entry.comments.label": "Comentarios", "entry.comments.title": "Ver comentarios", + "entry.share.label": "Comparta", + "entry.share.title": "Comparta este articulo", "page.unread.title": "No leídos", "page.starred.title": "Marcadores", "page.categories.title": "Categorias", diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json index f01c9e35..e75ad613 100644 --- a/locale/translations/fr_FR.json +++ b/locale/translations/fr_FR.json @@ -71,6 +71,8 @@ "entry.original.label": "Original", "entry.comments.label": "Commentaires", "entry.comments.title": "Voir les commentaires", + "entry.share.label": "Partager", + "entry.share.title": "Partager cet article", "page.unread.title": "Non lus", "page.starred.title": "Favoris", "page.categories.title": "Catégories", diff --git a/locale/translations/it_IT.json b/locale/translations/it_IT.json index 0ba57c8c..e869454a 100644 --- a/locale/translations/it_IT.json +++ b/locale/translations/it_IT.json @@ -71,6 +71,8 @@ "entry.original.label": "Contenuto originale", "entry.comments.label": "Commenti", "entry.comments.title": "Mostra i commenti", + "entry.share.label": "Condividi", + "entry.share.title": "Condividi questo articolo", "page.unread.title": "Da leggere", "page.starred.title": "Preferiti", "page.categories.title": "Categorie", diff --git a/locale/translations/ja_JP.json b/locale/translations/ja_JP.json index 4834b188..b7a6ae38 100644 --- a/locale/translations/ja_JP.json +++ b/locale/translations/ja_JP.json @@ -71,6 +71,8 @@ "entry.original.label": "オリジナル", "entry.comments.label": "コメント", "entry.comments.title": "コメントを見る", + "entry.share.label": "共有", + "entry.share.title": "この記事を共有する", "page.unread.title": "未読", "page.starred.title": "星付き", "page.categories.title": "カテゴリ", diff --git a/locale/translations/nl_NL.json b/locale/translations/nl_NL.json index 76dc686b..871271bb 100644 --- a/locale/translations/nl_NL.json +++ b/locale/translations/nl_NL.json @@ -71,6 +71,8 @@ "entry.original.label": "Origineel", "entry.comments.label": "Comments", "entry.comments.title": "Bekijk de reacties", + "entry.share.label": "Deel", + "entry.share.title": "Deel dit artikel", "page.unread.title": "Ongelezen", "page.starred.title": "Favorieten", "page.categories.title": "Categorieën", diff --git a/locale/translations/pl_PL.json b/locale/translations/pl_PL.json index 7c842f5e..62740eb1 100644 --- a/locale/translations/pl_PL.json +++ b/locale/translations/pl_PL.json @@ -71,6 +71,8 @@ "entry.original.label": "Oryginalny artykuł", "entry.comments.label": "Komentarze", "entry.comments.title": "Zobacz komentarze", + "entry.share.label": "Podzielić się", + "entry.share.title": "Podzielić się ten artykuł", "page.unread.title": "Nieprzeczytane", "page.starred.title": "Oznaczone gwiazdką", "page.categories.title": "Kategorie", diff --git a/locale/translations/ru_RU.json b/locale/translations/ru_RU.json index b415a193..e1332011 100644 --- a/locale/translations/ru_RU.json +++ b/locale/translations/ru_RU.json @@ -71,6 +71,8 @@ "entry.original.label": "Оригинал", "entry.comments.label": "Комментарии", "entry.comments.title": "Показать комментарии", + "entry.share.label": "поделиться", + "entry.share.title": "поделиться эту статью", "page.unread.title": "Непрочитанное", "page.starred.title": "Избранное", "page.categories.title": "Категории", diff --git a/locale/translations/zh_CN.json b/locale/translations/zh_CN.json index 44675d1e..1117a90b 100644 --- a/locale/translations/zh_CN.json +++ b/locale/translations/zh_CN.json @@ -71,6 +71,8 @@ "entry.original.label": "原始内容", "entry.comments.label": "评论", "entry.comments.title": "查看评论", + "entry.share.label": "分享", + "entry.share.title": "分享这篇文章", "page.unread.title": "未读", "page.starred.title": "星标", "page.categories.title": "分类", diff --git a/model/entry.go b/model/entry.go index 0a7a5f7b..54dfe03f 100644 --- a/model/entry.go +++ b/model/entry.go @@ -31,6 +31,7 @@ type Entry struct { Date time.Time `json:"published_at"` Content string `json:"content"` Author string `json:"author"` + ShareCode string `json:"share_code"` Starred bool `json:"starred"` Enclosures EnclosureList `json:"enclosures,omitempty"` Feed *Feed `json:"feed,omitempty"` diff --git a/storage/entry.go b/storage/entry.go index 277eb7a3..f0fa64bc 100644 --- a/storage/entry.go +++ b/storage/entry.go @@ -9,6 +9,7 @@ import ( "fmt" "time" + "miniflux.app/crypto" "miniflux.app/logger" "miniflux.app/model" @@ -351,3 +352,35 @@ func (s *Storage) EntryURLExists(feedID int64, entryURL string) bool { s.db.QueryRow(query, feedID, entryURL).Scan(&result) return result } + +// GetEntryShareCode returns the share code of the provided entry. +// It generates a new one if not already defined. +func (s *Storage) GetEntryShareCode(userID int64, entryID int64) (shareCode string, err error) { + query := `SELECT share_code FROM entries WHERE user_id=$1 AND id=$2` + err = s.db.QueryRow(query, userID, entryID).Scan(&shareCode) + + if err != nil || shareCode != "" { + return + } + + shareCode = crypto.GenerateRandomStringHex(16) + + query = `UPDATE entries SET share_code = $1 WHERE user_id=$2 AND id=$3` + result, err := s.db.Exec(query, shareCode, userID, entryID) + if err != nil { + err = fmt.Errorf(`store: unable to set share_code for entry #%d: %v`, entryID, err) + return + } + + count, err := result.RowsAffected() + if err != nil { + err = fmt.Errorf(`store: unable to set share_code for entry #%d: %v`, entryID, err) + return + } + + if count == 0 { + err = errors.New(`store: nothing has been updated`) + } + + return +} diff --git a/storage/entry_query_builder.go b/storage/entry_query_builder.go index ebc98a12..cf547a22 100644 --- a/storage/entry_query_builder.go +++ b/storage/entry_query_builder.go @@ -128,6 +128,13 @@ func (e *EntryQueryBuilder) WithoutStatus(status string) *EntryQueryBuilder { return e } +// WithShareCode set the entry hash. +func (e *EntryQueryBuilder) WithShareCode(shareCode string) *EntryQueryBuilder { + e.conditions = append(e.conditions, fmt.Sprintf("e.share_code = $%d", len(e.args)+1)) + e.args = append(e.args, shareCode) + return e +} + // WithOrder set the sorting order. func (e *EntryQueryBuilder) WithOrder(order string) *EntryQueryBuilder { e.order = order @@ -198,6 +205,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) { e.url, e.comments_url, e.author, + e.share_code, e.content, e.status, e.starred, @@ -255,6 +263,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) { &entry.URL, &entry.CommentsURL, &entry.Author, + &entry.ShareCode, &entry.Content, &entry.Status, &entry.Starred, @@ -358,3 +367,10 @@ func NewEntryQueryBuilder(store *Storage, userID int64) *EntryQueryBuilder { conditions: []string{"e.user_id = $1"}, } } + +// NewAnonymousQueryBuilder returns a new EntryQueryBuilder suitable for anonymous users. +func NewAnonymousQueryBuilder(store *Storage) *EntryQueryBuilder { + return &EntryQueryBuilder{ + store: store, + } +} diff --git a/template/html/entry.html b/template/html/entry.html index f9872d9f..3ff2cdb5 100644 --- a/template/html/entry.html +++ b/template/html/entry.html @@ -6,6 +6,7 @@