mirror of
https://github.com/writefreely/writefreely
synced 2024-11-24 01:23:04 +00:00
Support Web Monetized split content
Ref T770
This commit is contained in:
parent
9341784c0c
commit
e42ba392c6
18 changed files with 578 additions and 24 deletions
17
account.go
17
account.go
|
@ -199,6 +199,23 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr
|
|||
},
|
||||
}
|
||||
|
||||
var coll *Collection
|
||||
if signup.Monetization != "" {
|
||||
if coll == nil {
|
||||
coll, err = app.db.GetCollection(signup.Alias)
|
||||
if err != nil {
|
||||
log.Error("Unable to get new collection '%s' for monetization on signup: %v", signup.Alias, err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
err = app.db.SetCollectionAttribute(coll.ID, "monetization_pointer", signup.Monetization)
|
||||
if err != nil {
|
||||
log.Error("Unable to add monetization on signup: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
coll.Monetization = signup.Monetization
|
||||
}
|
||||
|
||||
var token string
|
||||
if reqJSON && !signup.Web {
|
||||
token, err = app.db.GetAccessToken(u.ID)
|
||||
|
|
|
@ -353,6 +353,17 @@ func (c *Collection) RenderMathJax() bool {
|
|||
return c.db.CollectionHasAttribute(c.ID, "render_mathjax")
|
||||
}
|
||||
|
||||
func (c *Collection) MonetizationURL() string {
|
||||
if c.Monetization == "" {
|
||||
return ""
|
||||
}
|
||||
return strings.Replace(c.Monetization, "$", "https://", 1)
|
||||
}
|
||||
|
||||
func (c CollectionPage) DisplayMonetization() string {
|
||||
return displayMonetization(c.Monetization, c.Alias)
|
||||
}
|
||||
|
||||
func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r)
|
||||
alias := r.FormValue("alias")
|
||||
|
|
16
database.go
16
database.go
|
@ -813,6 +813,7 @@ func (db *datastore) GetCollectionBy(condition string, value interface{}) (*Coll
|
|||
c.Signature = signature.String
|
||||
c.Format = format.String
|
||||
c.Public = c.IsPublic()
|
||||
c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer")
|
||||
|
||||
c.db = db
|
||||
|
||||
|
@ -1182,7 +1183,7 @@ func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, inclu
|
|||
}
|
||||
p.extractData()
|
||||
p.augmentContent(c)
|
||||
p.formatContent(cfg, c, includeFuture)
|
||||
p.formatContent(cfg, c, includeFuture, false)
|
||||
|
||||
posts = append(posts, p.processPost())
|
||||
}
|
||||
|
@ -1247,7 +1248,7 @@ func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag strin
|
|||
}
|
||||
p.extractData()
|
||||
p.augmentContent(c)
|
||||
p.formatContent(cfg, c, includeFuture)
|
||||
p.formatContent(cfg, c, includeFuture, false)
|
||||
|
||||
posts = append(posts, p.processPost())
|
||||
}
|
||||
|
@ -1652,6 +1653,14 @@ func (db *datastore) GetCollections(u *User, hostName string) (*[]Collection, er
|
|||
c.URL = c.CanonicalURL()
|
||||
c.Public = c.IsPublic()
|
||||
|
||||
/*
|
||||
// NOTE: future functionality
|
||||
if visibility != nil { // TODO: && visibility == CollPublic {
|
||||
// Add Monetization info when retrieving all public collections
|
||||
c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer")
|
||||
}
|
||||
*/
|
||||
|
||||
colls = append(colls, c)
|
||||
}
|
||||
err = rows.Err()
|
||||
|
@ -1698,6 +1707,9 @@ func (db *datastore) GetPublicCollections(hostName string) (*[]Collection, error
|
|||
c.URL = c.CanonicalURL()
|
||||
c.Public = c.IsPublic()
|
||||
|
||||
// Add Monetization information
|
||||
c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer")
|
||||
|
||||
colls = append(colls, c)
|
||||
}
|
||||
err = rows.Err()
|
||||
|
|
4
feed.go
4
feed.go
|
@ -97,6 +97,10 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
|||
|
||||
var title, permalink string
|
||||
for _, p := range *coll.Posts {
|
||||
// Add necessary path back to the web browser for Web Monetization if needed
|
||||
p.Collection = coll.CollectionObj // augmentReadingDestination requires a populated Collection field
|
||||
p.augmentReadingDestination()
|
||||
// Create the item for the feed
|
||||
title = p.PlainDisplayTitle()
|
||||
permalink = fmt.Sprintf("%s%s", baseUrl, p.Slug.String)
|
||||
feed.Items = append(feed.Items, &Item{
|
||||
|
|
52
handle.go
52
handle.go
|
@ -574,6 +574,38 @@ func (h *Handler) All(f handlerFunc) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func (h *Handler) PlainTextAPI(f handlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleTextError(w, r, func() error {
|
||||
// TODO: return correct "success" status
|
||||
status := 200
|
||||
start := time.Now()
|
||||
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
log.Error("%s:\n%s", e, debug.Stack())
|
||||
status = http.StatusInternalServerError
|
||||
w.WriteHeader(status)
|
||||
fmt.Fprintf(w, "Something didn't work quite right. The robots have alerted the humans.")
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host))
|
||||
}()
|
||||
|
||||
err := f(h.app.App(), w, r)
|
||||
if err != nil {
|
||||
if err, ok := err.(impart.HTTPError); ok {
|
||||
status = err.Status
|
||||
} else {
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}())
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) OAuth(f handlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleOAuthError(w, r, func() error {
|
||||
|
@ -842,6 +874,26 @@ func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error)
|
|||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
}
|
||||
|
||||
func (h *Handler) handleTextError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err, ok := err.(impart.HTTPError); ok {
|
||||
if err.Status >= 300 && err.Status < 400 {
|
||||
sendRedirect(w, err.Status, err.Message)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(err.Status)
|
||||
fmt.Fprintf(w, http.StatusText(err.Status))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "This is an unhelpful error message for a miscellaneous internal error.")
|
||||
}
|
||||
|
||||
func (h *Handler) handleOAuthError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
|
|
|
@ -393,6 +393,14 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
img {
|
||||
&.paid {
|
||||
height: 0.86em;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
nav#full-nav {
|
||||
margin: 0;
|
||||
|
||||
|
@ -743,6 +751,19 @@ input, button, select.inputform, textarea.inputform, a.btn {
|
|||
}
|
||||
}
|
||||
|
||||
.btn.cta.secondary, input[type=submit].secondary {
|
||||
background: transparent;
|
||||
color: @primary;
|
||||
&:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
.btn.cta.disabled {
|
||||
background-color: desaturate(@primary, 100%) !important;
|
||||
border-color: desaturate(@primary, 100%) !important;
|
||||
}
|
||||
|
||||
div.flat-select {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
|
|
@ -37,6 +37,25 @@ body#post article, pre, .hljs {
|
|||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
p.split {
|
||||
color: #6161FF;
|
||||
font-style: italic;
|
||||
font-size: 0.86em;
|
||||
}
|
||||
|
||||
#readmore-sell {
|
||||
padding: 1em 1em 2em;
|
||||
background-color: #fafafa;
|
||||
p.split {
|
||||
color: black;
|
||||
font-style: normal;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
.cta + .cta {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Post mixins */
|
||||
.article-code() {
|
||||
background-color: #f8f8f8;
|
||||
|
|
160
monetization.go
Normal file
160
monetization.go
Normal file
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright © 2020-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
* WriteFreely is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, included
|
||||
* in the LICENSE file in this source code package.
|
||||
*/
|
||||
|
||||
package writefreely
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/log"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func displayMonetization(monetization, alias string) string {
|
||||
if monetization == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
ptrURL, err := url.Parse(strings.Replace(monetization, "$", "https://", 1))
|
||||
if err == nil {
|
||||
if strings.HasSuffix(ptrURL.Host, ".xrptipbot.com") {
|
||||
// xrp tip bot doesn't support stream receipts, so return plain pointer
|
||||
return monetization
|
||||
}
|
||||
}
|
||||
|
||||
u := os.Getenv("PAYMENT_HOST")
|
||||
if u == "" {
|
||||
return "$webmonetization.org/api/receipts/" + url.PathEscape(monetization)
|
||||
}
|
||||
u += "/" + alias
|
||||
return u
|
||||
}
|
||||
|
||||
func handleSPSPEndpoint(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
idStr := r.FormValue("id")
|
||||
id, err := url.QueryUnescape(idStr)
|
||||
if err != nil {
|
||||
log.Error("Unable to unescape: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var c *Collection
|
||||
if strings.IndexRune(id, '.') > 0 && app.cfg.App.SingleUser {
|
||||
c, err = app.db.GetCollectionByID(1)
|
||||
} else {
|
||||
c, err = app.db.GetCollection(id)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pointer := c.Monetization
|
||||
if pointer == "" {
|
||||
err := impart.HTTPError{http.StatusNotFound, "No monetization pointer."}
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, pointer)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleGetSplitContent(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
var collID int64
|
||||
var collLookupID string
|
||||
var coll *Collection
|
||||
var err error
|
||||
vars := mux.Vars(r)
|
||||
if collAlias := vars["alias"]; collAlias != "" {
|
||||
// Fetch collection information, since an alias is provided
|
||||
coll, err = app.db.GetCollection(collAlias)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
collID = coll.ID
|
||||
collLookupID = coll.Alias
|
||||
}
|
||||
|
||||
p, err := app.db.GetPost(vars["post"], collID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
receipt := r.FormValue("receipt")
|
||||
if receipt == "" {
|
||||
return impart.HTTPError{http.StatusBadRequest, "No `receipt` given."}
|
||||
}
|
||||
err = verifyReceipt(receipt, collLookupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d := struct {
|
||||
Content string `json:"body"`
|
||||
HTMLContent string `json:"html_body"`
|
||||
}{}
|
||||
|
||||
if exc := strings.Index(p.Content, shortCodePaid); exc > -1 {
|
||||
baseURL := ""
|
||||
if coll != nil {
|
||||
baseURL = coll.CanonicalURL()
|
||||
}
|
||||
|
||||
d.Content = p.Content[exc+len(shortCodePaid):]
|
||||
d.HTMLContent = applyMarkdown([]byte(d.Content), baseURL, app.cfg)
|
||||
}
|
||||
|
||||
return impart.WriteSuccess(w, d, http.StatusOK)
|
||||
}
|
||||
|
||||
func verifyReceipt(receipt, id string) error {
|
||||
receiptsHost := os.Getenv("RECEIPTS_HOST")
|
||||
if receiptsHost == "" {
|
||||
receiptsHost = "https://webmonetization.org/api/receipts/verify?id=" + id
|
||||
} else {
|
||||
receiptsHost = fmt.Sprintf("%s/receipts?id=%s", receiptsHost, id)
|
||||
}
|
||||
|
||||
log.Info("Verifying receipt %s at %s", receipt, receiptsHost)
|
||||
r, err := http.NewRequest("POST", receiptsHost, bytes.NewBufferString(receipt))
|
||||
if err != nil {
|
||||
log.Error("Unable to create new request to %s: %s", receiptsHost, err)
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
log.Error("Unable to Do() request to %s: %s", receiptsHost, err)
|
||||
return err
|
||||
}
|
||||
if resp != nil && resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Error("Unable to read %s response body: %s", receiptsHost, err)
|
||||
return err
|
||||
}
|
||||
log.Info("Status : %s", resp.Status)
|
||||
log.Info("Response: %s", body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Error("Bad response from %s:\nStatus: %d\n%s", receiptsHost, resp.StatusCode, string(body))
|
||||
return impart.HTTPError{resp.StatusCode, string(body)}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -42,12 +42,46 @@ var (
|
|||
mentionReg = regexp.MustCompile(`@([A-Za-z0-9._%+-]+)(@[A-Za-z0-9.-]+\.[A-Za-z]+)\b`)
|
||||
)
|
||||
|
||||
func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) {
|
||||
func (p *Post) handlePremiumContent(c *Collection, isOwner, postPage bool, cfg *config.Config) {
|
||||
if c.Monetization != "" {
|
||||
// User has Web Monetization enabled, so split content if it exists
|
||||
spl := strings.Index(p.Content, shortCodePaid)
|
||||
p.IsPaid = spl > -1
|
||||
if postPage {
|
||||
// We're viewing the individual post
|
||||
if isOwner {
|
||||
p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`<p class="split">Your subscriber content begins here.</p>`+"\n\n", 1)
|
||||
} else {
|
||||
if spl > -1 {
|
||||
p.Content = p.Content[:spl+len(shortCodePaid)]
|
||||
p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`<p class="split">Continue reading with a <strong>Coil</strong> membership.</p>`+"\n\n", 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We've viewing the post on the collection landing
|
||||
if spl > -1 {
|
||||
baseURL := c.CanonicalURL()
|
||||
if isOwner {
|
||||
baseURL = "/" + c.Alias + "/"
|
||||
}
|
||||
|
||||
p.Content = p.Content[:spl+len(shortCodePaid)]
|
||||
p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:spl]), baseURL, cfg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool, isPostPage bool) {
|
||||
baseURL := c.CanonicalURL()
|
||||
// TODO: redundant
|
||||
if !isSingleUser {
|
||||
baseURL = "/" + c.Alias + "/"
|
||||
}
|
||||
|
||||
p.handlePremiumContent(c, isOwner, isPostPage, cfg)
|
||||
p.Content = strings.Replace(p.Content, "<!--paid-->", "<!--paid-->", 1)
|
||||
|
||||
p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String)))
|
||||
p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL, cfg))
|
||||
if exc := strings.Index(string(p.Content), "<!--more-->"); exc > -1 {
|
||||
|
@ -55,8 +89,8 @@ func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool) {
|
||||
p.Post.formatContent(cfg, &p.Collection.Collection, isOwner)
|
||||
func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool, isPostPage bool) {
|
||||
p.Post.formatContent(cfg, &p.Collection.Collection, isOwner, isPostPage)
|
||||
}
|
||||
|
||||
func (p *Post) augmentContent(c *Collection) {
|
||||
|
@ -78,6 +112,12 @@ func (p *PublicPost) augmentContent() {
|
|||
p.Post.augmentContent(&p.Collection.Collection)
|
||||
}
|
||||
|
||||
func (p *PublicPost) augmentReadingDestination() {
|
||||
if p.IsPaid {
|
||||
p.HTMLContent += template.HTML("\n\n" + `<p><a class="read-more" href="` + p.Collection.CanonicalURL() + p.Slug.String + `">` + localStr("Read more...", p.Language.String) + `</a> ($)</p>`)
|
||||
}
|
||||
}
|
||||
|
||||
func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string {
|
||||
return applyMarkdownSpecial(data, false, baseURL, cfg)
|
||||
}
|
||||
|
|
44
posts.go
44
posts.go
|
@ -48,6 +48,8 @@ const (
|
|||
postIDLen = 10
|
||||
|
||||
postMetaDateFormat = "2006-01-02 15:04:05"
|
||||
|
||||
shortCodePaid = "<!--paid-->"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -109,6 +111,7 @@ type (
|
|||
HTMLExcerpt template.HTML `db:"content" json:"-"`
|
||||
Tags []string `json:"tags"`
|
||||
Images []string `json:"images,omitempty"`
|
||||
IsPaid bool `json:"paid"`
|
||||
|
||||
OwnerName string `json:"owner,omitempty"`
|
||||
}
|
||||
|
@ -129,6 +132,20 @@ type (
|
|||
Collection *CollectionObj `json:"collection,omitempty"`
|
||||
}
|
||||
|
||||
CollectionPostPage struct {
|
||||
*PublicPost
|
||||
page.StaticPage
|
||||
IsOwner bool
|
||||
IsPinned bool
|
||||
IsCustomDomain bool
|
||||
Monetization string
|
||||
PinnedPosts *[]PublicPost
|
||||
IsFound bool
|
||||
IsAdmin bool
|
||||
CanInvite bool
|
||||
Silenced bool
|
||||
}
|
||||
|
||||
RawPost struct {
|
||||
Id, Slug string
|
||||
Title string
|
||||
|
@ -269,6 +286,14 @@ func (p *Post) HasTitleLink() bool {
|
|||
return hasLink
|
||||
}
|
||||
|
||||
func (c CollectionPostPage) DisplayMonetization() string {
|
||||
if c.Collection == nil {
|
||||
log.Info("CollectionPostPage.DisplayMonetization: c.Collection is nil")
|
||||
return ""
|
||||
}
|
||||
return displayMonetization(c.Monetization, c.Collection.Alias)
|
||||
}
|
||||
|
||||
func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
friendlyID := vars["post"]
|
||||
|
@ -1154,7 +1179,8 @@ func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object {
|
|||
o.Name = p.DisplayTitle()
|
||||
p.augmentContent()
|
||||
if p.HTMLContent == template.HTML("") {
|
||||
p.formatContent(cfg, false)
|
||||
p.formatContent(cfg, false, false)
|
||||
p.augmentReadingDestination()
|
||||
}
|
||||
o.Content = string(p.HTMLContent)
|
||||
if p.Language.Valid {
|
||||
|
@ -1502,20 +1528,8 @@ Are you sure it was ever here?`,
|
|||
p.extractData()
|
||||
p.Content = strings.Replace(p.Content, "<!--more-->", "", 1)
|
||||
// TODO: move this to function
|
||||
p.formatContent(app.cfg, cr.isCollOwner)
|
||||
tp := struct {
|
||||
*PublicPost
|
||||
page.StaticPage
|
||||
IsOwner bool
|
||||
IsPinned bool
|
||||
IsCustomDomain bool
|
||||
Monetization string
|
||||
PinnedPosts *[]PublicPost
|
||||
IsFound bool
|
||||
IsAdmin bool
|
||||
CanInvite bool
|
||||
Silenced bool
|
||||
}{
|
||||
p.formatContent(app.cfg, cr.isCollOwner, true)
|
||||
tp := CollectionPostPage{
|
||||
PublicPost: p,
|
||||
StaticPage: pageForReq(app, r),
|
||||
IsOwner: cr.isCollOwner,
|
||||
|
|
|
@ -134,6 +134,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
|||
// Handle collections
|
||||
write.HandleFunc("/api/collections", handler.All(newCollection)).Methods("POST")
|
||||
apiColls := write.PathPrefix("/api/collections/").Subrouter()
|
||||
apiColls.HandleFunc("/monetization-pointer", handler.PlainTextAPI(handleSPSPEndpoint)).Methods("GET")
|
||||
apiColls.HandleFunc("/"+host, handler.AllReader(fetchCollection)).Methods("GET")
|
||||
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.AllReader(fetchCollection)).Methods("GET")
|
||||
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.All(existingCollection)).Methods("POST", "DELETE")
|
||||
|
@ -141,6 +142,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
|||
apiColls.HandleFunc("/{alias}/posts", handler.All(newPost)).Methods("POST")
|
||||
apiColls.HandleFunc("/{alias}/posts/{post}", handler.AllReader(fetchPost)).Methods("GET")
|
||||
apiColls.HandleFunc("/{alias}/posts/{post:[a-zA-Z0-9]{10}}", handler.All(existingPost)).Methods("POST")
|
||||
apiColls.HandleFunc("/{alias}/posts/{post}/splitcontent", handler.AllReader(handleGetSplitContent)).Methods("GET", "POST")
|
||||
apiColls.HandleFunc("/{alias}/posts/{post}/{property}", handler.AllReader(fetchPostProperty)).Methods("GET")
|
||||
apiColls.HandleFunc("/{alias}/collect", handler.All(addPost)).Methods("POST")
|
||||
apiColls.HandleFunc("/{alias}/pin", handler.All(pinPost)).Methods("POST")
|
||||
|
|
78
static/img/paidarticle.svg
Normal file
78
static/img/paidarticle.svg
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="15mm"
|
||||
height="15mm"
|
||||
viewBox="0 0 15 15"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
sodipodi:docname="paidarticle.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="3.959798"
|
||||
inkscape:cx="32.178691"
|
||||
inkscape:cy="9.7652796"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="1280"
|
||||
inkscape:window-height="720"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="26"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-97.650089,-94.91132)">
|
||||
<circle
|
||||
style="opacity:0.83300003;fill:#72bf85;fill-opacity:1;stroke:none;stroke-width:1.14235425;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path819"
|
||||
cx="105.15009"
|
||||
cy="102.41132"
|
||||
r="7.5" />
|
||||
<g
|
||||
aria-label="$"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10.58333302px;line-height:125%;font-family:'Open Sans';-inkscape-font-specification:'Open Sans Bold';letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text817"
|
||||
transform="translate(0.15009808,-0.15009445)">
|
||||
<path
|
||||
d="m 107.59415,103.90759 q 0,0.82166 -0.59427,1.32292 -0.59428,0.49609 -1.66399,0.59428 v 1.05936 h -0.70796 v -1.03869 q -1.26091,-0.0258 -2.21175,-0.44442 v -1.36426 q 0.44958,0.22221 1.08003,0.39274 0.63562,0.17053 1.13172,0.20154 v -1.60197 l -0.34624,-0.13436 q -1.02319,-0.40307 -1.4521,-0.87333 -0.42375,-0.47542 -0.42375,-1.17305 0,-0.74931 0.58394,-1.229903 0.58912,-0.485759 1.63815,-0.589112 v -0.790649 h 0.70796 v 0.769979 q 1.18339,0.05168 2.13941,0.475423 l -0.48576,1.209232 q -0.80615,-0.33073 -1.65365,-0.40308 v 1.52445 q 1.00769,0.38758 1.43144,0.6718 0.42892,0.28422 0.62529,0.62528 0.20153,0.34107 0.20153,0.79582 z m -1.55546,0.0775 q 0,-0.21704 -0.1757,-0.3669 -0.1757,-0.14986 -0.5271,-0.31006 v 1.28675 q 0.7028,-0.11886 0.7028,-0.60979 z m -2.07739,-3.13675 q 0,0.22737 0.15503,0.37723 0.1602,0.1447 0.5116,0.29973 v -1.2144 q -0.66663,0.0982 -0.66663,0.53744 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Open Sans';-inkscape-font-specification:'Open Sans Bold';fill:#ffffff;fill-opacity:1;stroke-width:0.26458332px"
|
||||
id="path821"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
94
static/js/webmonetization.js
Normal file
94
static/js/webmonetization.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright © 2020-2021 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
* WriteFreely is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, included
|
||||
* in the LICENSE file in this source code package.
|
||||
*/
|
||||
|
||||
let unlockingSplitContent = false;
|
||||
let unlockedSplitContent = false;
|
||||
let pendingSplitContent = false;
|
||||
|
||||
function showWMPaywall($content, $split) {
|
||||
let $readmoreSell = document.createElement('div')
|
||||
$readmoreSell.id = 'readmore-sell';
|
||||
$content.insertAdjacentElement('beforeend', $readmoreSell);
|
||||
$readmoreSell.appendChild($split);
|
||||
$readmoreSell.insertAdjacentHTML("beforeend", '\n\n<p class="font sans">For <strong>$5 per month</strong>, you can read this and other great writing across our site and other websites that support Web Monetization.</p>')
|
||||
$readmoreSell.insertAdjacentHTML("beforeend", '\n\n<p class="font sans"><a href="https://coil.com/signup?ref=writefreely" class="btn cta" target="coil">Get started</a> <a href="https://coil.com/?ref=writefreely" class="btn cta secondary">Learn more</a></p>')
|
||||
}
|
||||
|
||||
function initMonetization() {
|
||||
let $content = document.querySelector('.e-content')
|
||||
let $post = document.getElementById('post-body')
|
||||
let $split = $post.querySelector('.split')
|
||||
if (document.monetization === undefined || $split == null) {
|
||||
if ($split) {
|
||||
showWMPaywall($content, $split)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
document.monetization.addEventListener('monetizationstop', function(event) {
|
||||
if (pendingSplitContent) {
|
||||
// We've seen the 'pending' activity, so we can assume things will work
|
||||
document.monetization.removeEventListener('monetizationstop', progressHandler)
|
||||
return
|
||||
}
|
||||
|
||||
// We're getting 'stop' without ever starting, so display the paywall.
|
||||
showWMPaywall($content, $split)
|
||||
});
|
||||
|
||||
document.monetization.addEventListener('monetizationpending', function (event) {
|
||||
pendingSplitContent = true
|
||||
})
|
||||
|
||||
let progressHandler = function(event) {
|
||||
if (unlockedSplitContent) {
|
||||
document.monetization.removeEventListener('monetizationprogress', progressHandler)
|
||||
return
|
||||
}
|
||||
if (!unlockingSplitContent && !unlockedSplitContent) {
|
||||
unlockingSplitContent = true
|
||||
getSplitContent(event.detail.receipt, function (status, data) {
|
||||
unlockingSplitContent = false
|
||||
if (status == 200) {
|
||||
$split.textContent = "Your subscriber perks start here."
|
||||
$split.insertAdjacentHTML("afterend", "\n\n"+data.data.html_body)
|
||||
} else {
|
||||
$split.textContent = "Something went wrong while unlocking subscriber content."
|
||||
}
|
||||
unlockedSplitContent = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getSplitContent(receipt, callback) {
|
||||
let params = "receipt="+encodeURIComponent(receipt)
|
||||
|
||||
let http = new XMLHttpRequest();
|
||||
http.open("POST", "/api/collections/" + window.collAlias + "/posts/" + window.postSlug + "/splitcontent", true);
|
||||
|
||||
// Send the proper header information along with the request
|
||||
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
|
||||
http.onreadystatechange = function () {
|
||||
if (http.readyState == 4) {
|
||||
callback(http.status, JSON.parse(http.responseText));
|
||||
}
|
||||
}
|
||||
http.send(params);
|
||||
}
|
||||
|
||||
document.monetization.addEventListener('monetizationstart', function() {
|
||||
if (!unlockedSplitContent) {
|
||||
$split.textContent = "Unlocking subscriber content..."
|
||||
}
|
||||
document.monetization.removeEventListener('monetizationstart', progressHandler)
|
||||
});
|
||||
document.monetization.addEventListener('monetizationprogress', progressHandler);
|
||||
}
|
|
@ -142,4 +142,13 @@ function unpinPost(e, postID) {
|
|||
})();
|
||||
} catch (e) { /* ¯\_(ツ)_/¯ */ }
|
||||
</script>
|
||||
|
||||
{{if and .Monetization (not .IsOwner)}}
|
||||
<script src="/js/webmonetization.js"></script>
|
||||
<script>
|
||||
window.collAlias = '{{.Collection.Alias}}'
|
||||
window.postSlug = '{{.Slug.String}}'
|
||||
initMonetization()
|
||||
</script>
|
||||
{{end}}
|
||||
</html>{{end}}
|
||||
|
|
|
@ -132,4 +132,13 @@ function unpinPost(e, postID) {
|
|||
})();
|
||||
} catch (e) { /* ¯\_(ツ)_/¯ */ }
|
||||
</script>
|
||||
|
||||
{{if and .Monetization (not .IsOwner)}}
|
||||
<script src="/js/webmonetization.js"></script>
|
||||
<script>
|
||||
window.collAlias = '{{.Collection.Alias}}'
|
||||
window.postSlug = '{{.Slug.String}}'
|
||||
initMonetization()
|
||||
</script>
|
||||
{{end}}
|
||||
</html>{{end}}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!-- Miscelaneous render related template parts we use multiple times -->
|
||||
{{define "collection-meta"}}
|
||||
{{if .Monetization -}}
|
||||
<meta name="monetization" content="{{.Monetization}}" />
|
||||
<meta name="monetization" content="{{.DisplayMonetization}}" />
|
||||
{{- end}}
|
||||
{{end}}
|
||||
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
{{ define "posts" }}
|
||||
{{ range $el := .Posts }}<article id="post-{{.ID}}" class="{{.Font}} h-entry" itemscope itemtype="http://schema.org/BlogPosting">
|
||||
{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}
|
||||
{{if .Title.String}}<h2 class="post-title" itemprop="name" class="p-name">{{if .HasTitleLink}}{{.HTMLTitle}} <a class="user hidden action" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{$.CanonicalURL}}{{.Slug.String}}{{end}}">view</a>{{else}}<a href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{$.CanonicalURL}}{{.Slug.String}}{{end}}" itemprop="url" class="u-url">{{.HTMLTitle}}</a>{{end}}
|
||||
{{if .Title.String}}<h2 class="post-title" itemprop="name" class="p-name">
|
||||
{{- if .HasTitleLink -}}
|
||||
{{.HTMLTitle}} <a class="user hidden action" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{$.CanonicalURL}}{{.Slug.String}}{{end}}">view{{if .IsPaid}} {{template "paid-badge" .}}{{end}}</a>
|
||||
{{- else -}}
|
||||
{{- if .IsPaid}}{{template "paid-badge" .}}{{end -}}
|
||||
<a href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{$.CanonicalURL}}{{.Slug.String}}{{end}}" itemprop="url" class="u-url">{{.HTMLTitle}}</a>
|
||||
{{- end}}
|
||||
{{if $.IsOwner}}
|
||||
<a class="user hidden action" href="/{{if not $.SingleUser}}{{$.Alias}}/{{end}}{{.Slug.String}}/edit">edit</a>
|
||||
{{if $.CanPin}}<a class="user hidden pin action" href="/{{$.Alias}}/{{.Slug.String}}/pin" onclick="pinPost(event, '{{.ID}}', '{{.Slug.String}}', '{{.PlainDisplayTitle}}')">pin</a>{{end}}
|
||||
|
@ -24,7 +30,10 @@
|
|||
{{if $.Format.ShowDates}}<time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}">{{if not .Title.String}}<a href="{{$.CanonicalURL}}{{.Slug.String}}" itemprop="url">{{end}}{{.DisplayDate}}{{if not .Title.String}}</a>{{end}}</time>{{end}}
|
||||
{{else}}
|
||||
<h2 class="post-title" itemprop="name">
|
||||
{{if $.Format.ShowDates}}<time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}"><a href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{$.CanonicalURL}}{{.Slug.String}}{{end}}" itemprop="url" class="u-url">{{.DisplayDate}}</a></time>{{end}}
|
||||
{{if $.Format.ShowDates -}}
|
||||
{{- if .IsPaid}}{{template "paid-badge" .}}{{end -}}
|
||||
<time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}"><a href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{$.CanonicalURL}}{{.Slug.String}}{{end}}" itemprop="url" class="u-url">{{.DisplayDate}}</a></time>
|
||||
{{- end}}
|
||||
{{if $.IsOwner}}
|
||||
{{if not $.Format.ShowDates}}<a class="user hidden action" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{$.CanonicalURL}}{{.Slug.String}}{{end}}">view</a>{{end}}
|
||||
<a class="user hidden action" href="/{{if not $.SingleUser}}{{$.Alias}}/{{end}}{{.Slug.String}}/edit">edit</a>
|
||||
|
@ -51,3 +60,5 @@
|
|||
|
||||
<a class="read-more" href="{{$.CanonicalURL}}{{.Slug.String}}">{{localstr "Read more..." .Language.String}}</a>{{else}}<div {{if .Language}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}" class="book e-content">{{if and (and (not $.IsOwner) (not $.Format.ShowDates)) (not .Title.String)}}<a class="hidden action" href="{{if $.IsOwner}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{$.CanonicalURL}}{{.Slug.String}}{{end}}">view</a>{{end}}{{.HTMLContent}}</div>{{end}}</article>{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{define "paid-badge"}}<img class="paid" alt="Paid article" src="/img/paidarticle.svg" /> {{end}}
|
3
users.go
3
users.go
|
@ -45,7 +45,8 @@ type (
|
|||
Signup bool `json:"signup" schema:"signup"`
|
||||
|
||||
// Feature fields
|
||||
Description string `json:"description" schema:"description"`
|
||||
Description string `json:"description" schema:"description"`
|
||||
Monetization string `json:"monetization" schema:"monetization"`
|
||||
}
|
||||
|
||||
// AuthUser contains information for a newly authenticated user (either
|
||||
|
|
Loading…
Reference in a new issue