1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2025-01-19 07:02:51 +00:00

Implement RSS, Atom and JSON feeds and add a new flag

This commit is contained in:
bouncepaw 2020-12-08 20:15:32 +05:00
parent 5269411ea2
commit 8810e9891d
11 changed files with 194 additions and 67 deletions

View File

@ -10,8 +10,9 @@ import (
) )
func init() { func init() {
flag.StringVar(&util.ServerPort, "port", "1737", "Port to serve the wiki at") flag.StringVar(&util.URL, "url", "http://0.0.0.0:$port", "URL at which your wiki can be found. Used to generate feeds")
flag.StringVar(&util.HomePage, "home", "home", "The home page") flag.StringVar(&util.ServerPort, "port", "1737", "Port to serve the wiki at using HTTP")
flag.StringVar(&util.HomePage, "home", "home", "The home page name")
flag.StringVar(&util.SiteTitle, "title", "🍄", "How to call your wiki in the navititle") flag.StringVar(&util.SiteTitle, "title", "🍄", "How to call your wiki in the navititle")
flag.StringVar(&util.UserTree, "user-tree", "u", "Hypha which is a superhypha of all user pages") flag.StringVar(&util.UserTree, "user-tree", "u", "Hypha which is a superhypha of all user pages")
flag.StringVar(&util.AuthMethod, "auth-method", "none", "What auth method to use. Variants: \"none\", \"fixed\"") flag.StringVar(&util.AuthMethod, "auth-method", "none", "What auth method to use. Variants: \"none\", \"fixed\"")
@ -34,6 +35,10 @@ func parseCliArgs() {
log.Fatal(err) log.Fatal(err)
} }
if util.URL == "http://0.0.0.0:$port" {
util.URL = "http://0.0.0.0:" + util.ServerPort
}
if !isCanonicalName(util.HomePage) { if !isCanonicalName(util.HomePage) {
log.Fatal("Error: you must use a proper name for the homepage") log.Fatal("Error: you must use a proper name for the homepage")
} }

1
go.mod
View File

@ -4,5 +4,6 @@ go 1.14
require ( require (
github.com/adrg/xdg v0.2.2 github.com/adrg/xdg v0.2.2
github.com/gorilla/feeds v1.1.1
github.com/valyala/quicktemplate v1.6.3 github.com/valyala/quicktemplate v1.6.3
) )

2
go.sum
View File

@ -2,6 +2,8 @@ github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo=
github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ= github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"log" "log"
"os/exec" "os/exec"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -13,6 +14,8 @@ import (
"github.com/bouncepaw/mycorrhiza/util" "github.com/bouncepaw/mycorrhiza/util"
) )
var renameMsgPattern = regexp.MustCompile(`^Rename (.*) to .*`)
// Start initializes git credentials. // Start initializes git credentials.
func Start(wikiDir string) { func Start(wikiDir string) {
_, err := gitsh("config", "user.name", "wikimind") _, err := gitsh("config", "user.name", "wikimind")
@ -27,10 +30,46 @@ func Start(wikiDir string) {
// Revision represents a revision, duh. Hash is usually short. Username is extracted from email. // Revision represents a revision, duh. Hash is usually short. Username is extracted from email.
type Revision struct { type Revision struct {
Hash string Hash string
Username string Username string
Time time.Time Time time.Time
Message string Message string
hyphaeAffectedBuf []string
}
// determine what hyphae were affected by this revision
func (rev *Revision) hyphaeAffected() (hyphae []string) {
if nil != rev.hyphaeAffectedBuf {
return rev.hyphaeAffectedBuf
}
hyphae = make([]string, 0)
var (
// List of files affected by this revision, one per line.
out, err = gitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash)
// set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most currently).
set = make(map[string]bool)
isNewName = func(hyphaName string) bool {
if _, present := set[hyphaName]; present {
return false
}
set[hyphaName] = true
return true
}
)
if err != nil {
return hyphae
}
for _, filename := range strings.Split(out.String(), "\n") {
if strings.IndexRune(filename, '.') >= 0 {
dotPos := strings.LastIndexByte(filename, '.')
hyphaName := string([]byte(filename)[0:dotPos]) // is it safe?
if isNewName(hyphaName) {
hyphae = append(hyphae, hyphaName)
}
}
}
rev.hyphaeAffectedBuf = hyphae
return hyphae
} }
// TimeString returns a human readable time representation. // TimeString returns a human readable time representation.
@ -40,42 +79,38 @@ func (rev Revision) TimeString() string {
// HyphaeLinks returns a comma-separated list of hyphae that were affected by this revision as HTML string. // HyphaeLinks returns a comma-separated list of hyphae that were affected by this revision as HTML string.
func (rev Revision) HyphaeLinks() (html string) { func (rev Revision) HyphaeLinks() (html string) {
// diff-tree --no-commit-id --name-only -r hyphae := rev.hyphaeAffected()
var ( for i, hyphaName := range hyphae {
// List of files affected by this revision, one per line. if i > 0 {
out, err = gitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash) html += `<span aria-hidden="true">, </span>`
// set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most).
set = make(map[string]bool)
isNewName = func(hyphaName string) bool {
if _, present := set[hyphaName]; present {
return false
} else {
set[hyphaName] = true
return true
}
}
)
if err != nil {
return ""
}
for _, filename := range strings.Split(out.String(), "\n") {
// If filename has an ampersand:
if strings.IndexRune(filename, '.') >= 0 {
// Remove ampersanded suffix from filename:
ampersandPos := strings.LastIndexByte(filename, '.')
hyphaName := string([]byte(filename)[0:ampersandPos]) // is it safe?
if isNewName(hyphaName) {
// Entries are separated by commas
if len(set) > 1 {
html += `<span aria-hidden="true">, </span>`
}
html += fmt.Sprintf(`<a href="/page/%[1]s">%[1]s</a>`, hyphaName)
}
} }
html += fmt.Sprintf(`<a href="/page/%[1]s">%[1]s</a>`, hyphaName)
} }
return html return html
} }
func (rev *Revision) descriptionForFeed() (html string) {
return fmt.Sprintf(
`<p>%s</p>
<p><b>Hyphae affected:</b> %s</p>`, rev.Message, rev.HyphaeLinks())
}
// Try and guess what link is the most important by looking at the message.
func (rev *Revision) bestLink() string {
var (
revs = rev.hyphaeAffected()
renameRes = renameMsgPattern.FindStringSubmatch(rev.Message)
)
switch {
case renameRes != nil:
return "/page/" + renameRes[1]
case len(revs) == 0:
return ""
default:
return "/page/" + revs[0]
}
}
func (rev Revision) RecentChangesEntry() (html string) { func (rev Revision) RecentChangesEntry() (html string) {
if user.AuthUsed && rev.Username != "anon" { if user.AuthUsed && rev.Username != "anon" {
return fmt.Sprintf(` return fmt.Sprintf(`

View File

@ -11,8 +11,56 @@ import (
"github.com/bouncepaw/mycorrhiza/templates" "github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/util" "github.com/bouncepaw/mycorrhiza/util"
"github.com/gorilla/feeds"
) )
func recentChangesFeed() *feeds.Feed {
feed := &feeds.Feed{
Title: "Recent changes",
Link: &feeds.Link{Href: util.URL},
Description: "List of 30 recent changes on the wiki",
Author: &feeds.Author{Name: "Wikimind", Email: "wikimind@mycorrhiza"},
Updated: time.Now(),
}
var (
out, err = gitsh(
"log", "--oneline", "--no-merges",
"--pretty=format:\"%h\t%ae\t%at\t%s\"",
"--max-count=30",
)
revs []Revision
)
if err == nil {
for _, line := range strings.Split(out.String(), "\n") {
revs = append(revs, parseRevisionLine(line))
}
}
for _, rev := range revs {
feed.Add(&feeds.Item{
Title: rev.Message,
Author: &feeds.Author{Name: rev.Username},
Id: rev.Hash,
Description: rev.descriptionForFeed(),
Created: rev.Time,
Updated: rev.Time,
Link: &feeds.Link{Href: util.URL + rev.bestLink()},
})
}
return feed
}
func RecentChangesRSS() (string, error) {
return recentChangesFeed().ToRss()
}
func RecentChangesAtom() (string, error) {
return recentChangesFeed().ToAtom()
}
func RecentChangesJSON() (string, error) {
return recentChangesFeed().ToJSON()
}
func RecentChanges(n int) string { func RecentChanges(n int) string {
var ( var (
out, err = gitsh( out, err = gitsh(

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
@ -14,6 +15,9 @@ import (
func init() { func init() {
http.HandleFunc("/history/", handlerHistory) http.HandleFunc("/history/", handlerHistory)
http.HandleFunc("/recent-changes/", handlerRecentChanges) http.HandleFunc("/recent-changes/", handlerRecentChanges)
http.HandleFunc("/recent-changes-rss", handlerRecentChangesRSS)
http.HandleFunc("/recent-changes-atom", handlerRecentChangesAtom)
http.HandleFunc("/recent-changes-json", handlerRecentChangesJSON)
} }
// handlerHistory lists all revisions of a hypha // handlerHistory lists all revisions of a hypha
@ -46,3 +50,28 @@ func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) {
http.Redirect(w, rq, "/recent-changes/20", http.StatusSeeOther) http.Redirect(w, rq, "/recent-changes/20", http.StatusSeeOther)
} }
} }
func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func() (string, error), name string) {
log.Println(rq.URL)
if content, err := f(); err != nil {
w.Header().Set("Content-Type", "text/plain;charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "An error while generating "+name+": "+err.Error())
} else {
w.Header().Set("Content-Type", "application/rss+xml")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, content)
}
}
func handlerRecentChangesRSS(w http.ResponseWriter, rq *http.Request) {
genericHandlerOfFeeds(w, rq, history.RecentChangesRSS, "RSS")
}
func handlerRecentChangesAtom(w http.ResponseWriter, rq *http.Request) {
genericHandlerOfFeeds(w, rq, history.RecentChangesAtom, "Atom")
}
func handlerRecentChangesJSON(w http.ResponseWriter, rq *http.Request) {
genericHandlerOfFeeds(w, rq, history.RecentChangesJSON, "JSON feed")
}

View File

@ -39,6 +39,7 @@ textarea {font-size:15px;}
.edit {height:100%;} .edit {height:100%;}
.edit-form {height:90%;} .edit-form {height:90%;}
.edit-form textarea {width:100%;height:90%;} .edit-form textarea {width:100%;height:90%;}
.icon {margin-right: .25rem; vertical-align: bottom; }
main h1:not(.navi-title) {font-size:1.7rem;} main h1:not(.navi-title) {font-size:1.7rem;}
blockquote {border-left: 4px black solid; margin-left: 0; padding-left: 1rem;} blockquote {border-left: 4px black solid; margin-left: 0; padding-left: 1rem;}

View File

@ -14,6 +14,7 @@ textarea {font-size:15px;}
.edit {height:100%;} .edit {height:100%;}
.edit-form {height:90%;} .edit-form {height:90%;}
.edit-form textarea {width:100%;height:90%;} .edit-form textarea {width:100%;height:90%;}
.icon {margin-right: .25rem; vertical-align: bottom; }
main h1:not(.navi-title) {font-size:1.7rem;} main h1:not(.navi-title) {font-size:1.7rem;}
blockquote {border-left: 4px black solid; margin-left: 0; padding-left: 1rem;} blockquote {border-left: 4px black solid; margin-left: 0; padding-left: 1rem;}

View File

@ -18,6 +18,8 @@
recent changes recent changes
</nav> </nav>
<p><img class="icon" width="20" height="20" src="https://upload.wikimedia.org/wikipedia/commons/4/46/Generic_Feed-icon.svg">Subscribe via <a href="/recent-changes-rss">RSS</a>, <a href="/recent-changes-atom">Atom</a> or <a href="/recent-changes-json">JSON feed</a>.</p>
{% comment %} {% comment %}
Here I am, willing to add some accesibility using ARIA. Turns out, Here I am, willing to add some accesibility using ARIA. Turns out,
role="feed" is not supported in any screen reader as of September role="feed" is not supported in any screen reader as of September

View File

@ -77,81 +77,83 @@ func StreamRecentChangesHTML(qw422016 *qt422016.Writer, changes []string, n int)
recent changes recent changes
</nav> </nav>
<p><img class="icon" width="20" height="20" src="https://upload.wikimedia.org/wikipedia/commons/4/46/Generic_Feed-icon.svg">Subscribe via <a href="/recent-changes-rss">RSS</a>, <a href="/recent-changes-atom">Atom</a> or <a href="/recent-changes-json">JSON feed</a>.</p>
`) `)
//line templates/recent_changes.qtpl:26 //line templates/recent_changes.qtpl:28
qw422016.N().S(` qw422016.N().S(`
<section class="recent-changes__list" role="feed"> <section class="recent-changes__list" role="feed">
`) `)
//line templates/recent_changes.qtpl:29 //line templates/recent_changes.qtpl:31
if len(changes) == 0 { if len(changes) == 0 {
//line templates/recent_changes.qtpl:29 //line templates/recent_changes.qtpl:31
qw422016.N().S(` qw422016.N().S(`
<p>Could not find any recent changes.</p> <p>Could not find any recent changes.</p>
`) `)
//line templates/recent_changes.qtpl:31 //line templates/recent_changes.qtpl:33
} else { } else {
//line templates/recent_changes.qtpl:31 //line templates/recent_changes.qtpl:33
qw422016.N().S(` qw422016.N().S(`
`) `)
//line templates/recent_changes.qtpl:32 //line templates/recent_changes.qtpl:34
for i, entry := range changes { for i, entry := range changes {
//line templates/recent_changes.qtpl:32 //line templates/recent_changes.qtpl:34
qw422016.N().S(` qw422016.N().S(`
<ul class="recent-changes__entry rc-entry" role="article" <ul class="recent-changes__entry rc-entry" role="article"
aria-setsize="`) aria-setsize="`)
//line templates/recent_changes.qtpl:34 //line templates/recent_changes.qtpl:36
qw422016.N().D(n) qw422016.N().D(n)
//line templates/recent_changes.qtpl:34 //line templates/recent_changes.qtpl:36
qw422016.N().S(`" aria-posinset="`) qw422016.N().S(`" aria-posinset="`)
//line templates/recent_changes.qtpl:34 //line templates/recent_changes.qtpl:36
qw422016.N().D(i) qw422016.N().D(i)
//line templates/recent_changes.qtpl:34 //line templates/recent_changes.qtpl:36
qw422016.N().S(`"> qw422016.N().S(`">
`) `)
//line templates/recent_changes.qtpl:35 //line templates/recent_changes.qtpl:37
qw422016.N().S(entry) qw422016.N().S(entry)
//line templates/recent_changes.qtpl:35 //line templates/recent_changes.qtpl:37
qw422016.N().S(` qw422016.N().S(`
</ul> </ul>
`) `)
//line templates/recent_changes.qtpl:37 //line templates/recent_changes.qtpl:39
} }
//line templates/recent_changes.qtpl:37 //line templates/recent_changes.qtpl:39
qw422016.N().S(` qw422016.N().S(`
`) `)
//line templates/recent_changes.qtpl:38 //line templates/recent_changes.qtpl:40
} }
//line templates/recent_changes.qtpl:38 //line templates/recent_changes.qtpl:40
qw422016.N().S(` qw422016.N().S(`
</section> </section>
</main> </main>
`) `)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
} }
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
func WriteRecentChangesHTML(qq422016 qtio422016.Writer, changes []string, n int) { func WriteRecentChangesHTML(qq422016 qtio422016.Writer, changes []string, n int) {
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
StreamRecentChangesHTML(qw422016, changes, n) StreamRecentChangesHTML(qw422016, changes, n)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
} }
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
func RecentChangesHTML(changes []string, n int) string { func RecentChangesHTML(changes []string, n int) string {
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
WriteRecentChangesHTML(qb422016, changes, n) WriteRecentChangesHTML(qb422016, changes, n)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
return qs422016 return qs422016
//line templates/recent_changes.qtpl:41 //line templates/recent_changes.qtpl:43
} }

View File

@ -8,6 +8,7 @@ import (
) )
var ( var (
URL string
ServerPort string ServerPort string
HomePage string HomePage string
SiteTitle string SiteTitle string