This commit is contained in:
parent
bd279a8808
commit
d9211fa0d5
5 changed files with 368 additions and 23 deletions
60
dict.go
60
dict.go
|
@ -1,24 +1,40 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
type dict struct {
|
type dict struct {
|
||||||
hostname string
|
hostname string
|
||||||
languageEn string
|
languageEn string
|
||||||
searchStr string
|
searchStr string
|
||||||
newSentence string
|
newSentence string
|
||||||
|
mentions string
|
||||||
|
newMention string
|
||||||
|
thanksForMention string
|
||||||
|
backToPage string
|
||||||
}
|
}
|
||||||
|
|
||||||
var EN = dict{
|
var EN = dict{
|
||||||
hostname: "en.arns.lt",
|
hostname: "en.arns.lt",
|
||||||
languageEn: "English",
|
languageEn: "English",
|
||||||
searchStr: "Input searching tag, created date or word in description",
|
searchStr: "Input searching tag, created date or word in description",
|
||||||
newSentence: "New sentence",
|
newSentence: "New sentence",
|
||||||
|
mentions: "Mentions",
|
||||||
|
newMention: "New gemini mention",
|
||||||
|
thanksForMention: "Thanks for the mention",
|
||||||
|
backToPage: "Back to page",
|
||||||
}
|
}
|
||||||
|
|
||||||
var SGS = dict{
|
var SGS = dict{
|
||||||
hostname: "en.arns.lt",
|
hostname: "en.arns.lt",
|
||||||
languageEn: "Samogitian",
|
languageEn: "Samogitian",
|
||||||
searchStr: "Ivesk ėiškuoma žīma, sokūrėma data arba žuodi ėš aprašīma",
|
searchStr: "Ivesk ėiškuoma žīma, sokūrėma data arba žuodi ėš aprašīma",
|
||||||
newSentence: "Naujė ėšmintės",
|
newSentence: "Naujė ėšmintės",
|
||||||
|
mentions: "Pamėnavuojėmā",
|
||||||
|
newMention: "Naus gemini mėnavuojėms",
|
||||||
|
thanksForMention: "Diekou ož pamėnavuojėma",
|
||||||
|
backToPage: "Atgal i poslapi",
|
||||||
}
|
}
|
||||||
|
|
||||||
func Dict() dict {
|
func Dict() dict {
|
||||||
|
@ -29,3 +45,23 @@ func Dict() dict {
|
||||||
|
|
||||||
return dictMap[defaultLang]
|
return dictMap[defaultLang]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func T(key string) string {
|
||||||
|
dict := map[string]string{}
|
||||||
|
|
||||||
|
d := Dict()
|
||||||
|
r := reflect.ValueOf(&d).Elem()
|
||||||
|
rt := r.Type()
|
||||||
|
for i := 0; i < rt.NumField(); i++ {
|
||||||
|
field := rt.Field(i)
|
||||||
|
rv := reflect.ValueOf(&d)
|
||||||
|
value := reflect.Indirect(rv).FieldByName(field.Name)
|
||||||
|
dict[field.Name] = value.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := dict[key]; ok {
|
||||||
|
return value
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
133
main.go
133
main.go
|
@ -163,6 +163,10 @@ func process(_ context.Context, w gemini.ResponseWriter, r *gemini.Request) {
|
||||||
renderSearch(w, r, client)
|
renderSearch(w, r, client)
|
||||||
case regexp.MustCompile(`^/admin/sentence$`).MatchString(r.URL.Path):
|
case regexp.MustCompile(`^/admin/sentence$`).MatchString(r.URL.Path):
|
||||||
renderAdminSentence(w, r, client)
|
renderAdminSentence(w, r, client)
|
||||||
|
case regexp.MustCompile(`^/admin/mentions/review/[\d\w]+/[\d\w]+$`).MatchString(r.URL.Path):
|
||||||
|
adminReviewMention(w, r)
|
||||||
|
case regexp.MustCompile(`^/admin/mentions/remove/[\d\w]+/[\d\w]+$`).MatchString(r.URL.Path):
|
||||||
|
adminRemoveMention(w, r)
|
||||||
case regexp.MustCompile(`^/t/?$`).MatchString(r.URL.Path):
|
case regexp.MustCompile(`^/t/?$`).MatchString(r.URL.Path):
|
||||||
renderTags(w, client)
|
renderTags(w, client)
|
||||||
case regexp.MustCompile(`^/f/?$`).MatchString(r.URL.Path):
|
case regexp.MustCompile(`^/f/?$`).MatchString(r.URL.Path):
|
||||||
|
@ -183,6 +187,8 @@ func process(_ context.Context, w gemini.ResponseWriter, r *gemini.Request) {
|
||||||
downloadAttachment(w, r, client)
|
downloadAttachment(w, r, client)
|
||||||
case regexp.MustCompile(`^/r/.+`).MatchString(r.URL.Path):
|
case regexp.MustCompile(`^/r/.+`).MatchString(r.URL.Path):
|
||||||
redirectAction(w, r, client)
|
redirectAction(w, r, client)
|
||||||
|
case regexp.MustCompile(`^/mention/[\d\w]+`).MatchString(r.URL.Path):
|
||||||
|
processMention(w, r)
|
||||||
default:
|
default:
|
||||||
w.WriteHeader(gemini.StatusNotFound, "Out of space")
|
w.WriteHeader(gemini.StatusNotFound, "Out of space")
|
||||||
}
|
}
|
||||||
|
@ -254,12 +260,6 @@ func renderIndex(w gemini.ResponseWriter, client TreeManagerClient, r *gemini.Re
|
||||||
db.Save("page_counter._"+defaultLang, "1")
|
db.Save("page_counter._"+defaultLang, "1")
|
||||||
}
|
}
|
||||||
|
|
||||||
adminCert, found := db.Get("admin_cert")
|
|
||||||
if !found {
|
|
||||||
adminCert = "xxx"
|
|
||||||
db.Save("admin_cert", "xxx")
|
|
||||||
}
|
|
||||||
|
|
||||||
sentence := ""
|
sentence := ""
|
||||||
sentenceCount, _ := db.Length("sentences_" + defaultLang)
|
sentenceCount, _ := db.Length("sentences_" + defaultLang)
|
||||||
if sentenceCount > 0 {
|
if sentenceCount > 0 {
|
||||||
|
@ -276,7 +276,7 @@ func renderIndex(w gemini.ResponseWriter, client TreeManagerClient, r *gemini.Re
|
||||||
"lastFiles": GetLastFiles(tree.Files),
|
"lastFiles": GetLastFiles(tree.Files),
|
||||||
"lastRebuild": lastRebuild,
|
"lastRebuild": lastRebuild,
|
||||||
"pageCounter": pageCounter,
|
"pageCounter": pageCounter,
|
||||||
"admin": adminCert == getCertFingerprint(r),
|
"admin": isAdmin(r),
|
||||||
"sentence": sentence,
|
"sentence": sentence,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -406,8 +406,7 @@ func renderAdminSentence(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
adminCert, found := db.Get("admin_cert")
|
if !isAdmin(r) {
|
||||||
if !found || adminCert != getCertFingerprint(r) {
|
|
||||||
w.WriteHeader(gemini.StatusNotFound, "Out of space")
|
w.WriteHeader(gemini.StatusNotFound, "Out of space")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -418,7 +417,7 @@ func renderAdminSentence(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, found = db.Get("sentences_" + defaultLang)
|
_, found := db.Get("sentences_" + defaultLang)
|
||||||
if !found {
|
if !found {
|
||||||
db.CreateNode("sentences_" + defaultLang)
|
db.CreateNode("sentences_" + defaultLang)
|
||||||
_, _ = db.Get("sentences_" + defaultLang)
|
_, _ = db.Get("sentences_" + defaultLang)
|
||||||
|
@ -434,6 +433,62 @@ func renderAdminSentence(
|
||||||
w.WriteHeader(gemini.StatusPermanentRedirect, "/")
|
w.WriteHeader(gemini.StatusPermanentRedirect, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func adminReviewMention(
|
||||||
|
w gemini.ResponseWriter,
|
||||||
|
r *gemini.Request,
|
||||||
|
) {
|
||||||
|
if !isAdmin(r) {
|
||||||
|
w.WriteHeader(gemini.StatusNotFound, "Out of space")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q, err := gemini.QueryUnescape(r.URL.RawQuery)
|
||||||
|
if err != nil || q == "" {
|
||||||
|
w.WriteHeader(gemini.StatusInput, "Patvėrtink so 't'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
urlParts := strings.Split(r.URL.Path, "/")
|
||||||
|
fmt.Println(urlParts)
|
||||||
|
pageId := urlParts[4]
|
||||||
|
id := urlParts[5]
|
||||||
|
|
||||||
|
if q != "t" {
|
||||||
|
w.WriteHeader(gemini.StatusPermanentRedirect, "/r/"+pageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkReviewed(pageId, id)
|
||||||
|
w.WriteHeader(gemini.StatusPermanentRedirect, "/r/"+pageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminRemoveMention(
|
||||||
|
w gemini.ResponseWriter,
|
||||||
|
r *gemini.Request,
|
||||||
|
) {
|
||||||
|
if !isAdmin(r) {
|
||||||
|
w.WriteHeader(gemini.StatusNotFound, "Out of space")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q, err := gemini.QueryUnescape(r.URL.RawQuery)
|
||||||
|
if err != nil || q == "" {
|
||||||
|
w.WriteHeader(gemini.StatusInput, "Patvėrtink so 't'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
urlParts := strings.Split(r.URL.Path, "/")
|
||||||
|
fmt.Println(urlParts)
|
||||||
|
pageId := urlParts[4]
|
||||||
|
id := urlParts[5]
|
||||||
|
|
||||||
|
if q != "t" {
|
||||||
|
w.WriteHeader(gemini.StatusPermanentRedirect, "/r/"+pageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveMention(pageId, id)
|
||||||
|
w.WriteHeader(gemini.StatusPermanentRedirect, "/r/"+pageId)
|
||||||
|
}
|
||||||
|
|
||||||
func renderSearch(
|
func renderSearch(
|
||||||
w gemini.ResponseWriter,
|
w gemini.ResponseWriter,
|
||||||
r *gemini.Request,
|
r *gemini.Request,
|
||||||
|
@ -547,7 +602,15 @@ func renderFile(w gemini.ResponseWriter, r *gemini.Request, client TreeManagerCl
|
||||||
w.SetMediaType("text/gemini")
|
w.SetMediaType("text/gemini")
|
||||||
tpl := pongo2.Must(pongo2.FromFile("templates/page.gmi"))
|
tpl := pongo2.Must(pongo2.FromFile("templates/page.gmi"))
|
||||||
page, err := tpl.Execute(
|
page, err := tpl.Execute(
|
||||||
pongo2.Context{"lang": defaultLang, "file": file, "pageCounter": pageCounter},
|
pongo2.Context{
|
||||||
|
"lang": defaultLang,
|
||||||
|
"file": file,
|
||||||
|
"pageCounter": pageCounter,
|
||||||
|
"T": T,
|
||||||
|
"admin": isAdmin(r),
|
||||||
|
"mentions": GetMentionsForPage(id, STATUS_MENTION_REVIEWED),
|
||||||
|
"adminWaitingMentions": GetMentionsForPage(id, STATUS_MENTION_NOT_REVIEWED),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("template failed: %v", err)
|
log.Fatalf("template failed: %v", err)
|
||||||
|
@ -561,6 +624,38 @@ func renderFile(w gemini.ResponseWriter, r *gemini.Request, client TreeManagerCl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func processMention(w gemini.ResponseWriter, r *gemini.Request) {
|
||||||
|
urlParts := strings.Split(r.URL.Path, "/")
|
||||||
|
id := urlParts[2]
|
||||||
|
|
||||||
|
q, err := gemini.QueryUnescape(r.URL.RawQuery)
|
||||||
|
if err != nil || q == "" {
|
||||||
|
w.WriteHeader(gemini.StatusInput, Dict().newMention)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = ProcessMention(q, id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
w.WriteHeader(gemini.StatusCGIError, "Internal server error: unprocessable mention")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.SetMediaType("text/gemini")
|
||||||
|
tpl := pongo2.Must(pongo2.FromFile("templates/mention.gmi"))
|
||||||
|
page, err := tpl.Execute(pongo2.Context{"lang": defaultLang, "id": id, "q": q, "T": T})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("template failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write([]byte(page))
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func renderAllFiles(w gemini.ResponseWriter, client TreeManagerClient) {
|
func renderAllFiles(w gemini.ResponseWriter, client TreeManagerClient) {
|
||||||
path := ""
|
path := ""
|
||||||
tr := TreeRequest{Path: &path, Filter: getLangFilters()}
|
tr := TreeRequest{Path: &path, Filter: getLangFilters()}
|
||||||
|
@ -649,3 +744,19 @@ func getCertFingerprint(r *gemini.Request) string {
|
||||||
|
|
||||||
return hex.EncodeToString(sum[:])
|
return hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAdmin(r *gemini.Request) bool {
|
||||||
|
db, err := zordfsdb.InitDB("./db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
adminCert, found := db.Get("admin_cert")
|
||||||
|
if !found {
|
||||||
|
adminCert = "xxx"
|
||||||
|
db.Save("admin_cert", "xxx")
|
||||||
|
}
|
||||||
|
|
||||||
|
return getCertFingerprint(r) == adminCert
|
||||||
|
}
|
||||||
|
|
175
mention.go
Normal file
175
mention.go
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
// Trying to implement gemini mentions from https://codeberg.org/bacardi55/gemini-mentions-rfc
|
||||||
|
// Thanks to @bacardi55 and his example https://git.sr.ht/~bacardi55/ggm (this is forked from there)
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"g.arns.lt/zordsdavini/zordfsdb"
|
||||||
|
"git.sr.ht/~adnano/go-gemini"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
STATUS_MENTION_NOT_REVIEWED = "0"
|
||||||
|
STATUS_MENTION_REVIEWED = "1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Nota: Gemini servers might have max execution time for cgi scripts.
|
||||||
|
// Eg: Gemserv has a 5s maximum policy before killing the request.
|
||||||
|
const maxRequestTime = 4
|
||||||
|
|
||||||
|
func ProcessMention(remoteUrl string, pageId string) (bool, error) {
|
||||||
|
u, err := validateUrl(remoteUrl)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("[gemini-mentions] Url is not valid.")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := fetchGeminiPage(u)
|
||||||
|
if err != nil || response.Status.Class() != gemini.StatusSuccess {
|
||||||
|
return false, fmt.Errorf("[gemini-mentions] Error retrieving content from %s: %s", u, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if respCert := response.TLS().PeerCertificates; len(respCert) > 0 &&
|
||||||
|
time.Now().After(respCert[0].NotAfter) {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"[gemini-mentions] Ignored url (invalid certificate for capsule): %s",
|
||||||
|
u,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all good, let's find gemini mentions link inside.
|
||||||
|
var content []byte
|
||||||
|
content, err = io.ReadAll(response.Body)
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("[gemini-mentions] Couldn't retrieve the provided URL content")
|
||||||
|
}
|
||||||
|
links := findMentionLinks(string(content), hostname)
|
||||||
|
if len(links) < 1 {
|
||||||
|
return false, fmt.Errorf("[gemini-mentions] Recieved a link with no mention: %s", u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// log to db
|
||||||
|
db, err := zordfsdb.InitDB("./db")
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("[gemini-mentions] no db: %s", err)
|
||||||
|
}
|
||||||
|
_, found := db.GetNode("mentions")
|
||||||
|
if !found {
|
||||||
|
db.CreateNode("mentions")
|
||||||
|
}
|
||||||
|
_, found = db.GetNode("mentions." + pageId)
|
||||||
|
if !found {
|
||||||
|
db.CreateNode("mentions." + pageId)
|
||||||
|
db.CreateNode("mentions." + pageId + ".mentions")
|
||||||
|
}
|
||||||
|
for _, link := range links {
|
||||||
|
id, err := db.AddObject("mentions." + pageId + ".mentions")
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("[gemini-mentions] couldn't add object: %s", err)
|
||||||
|
}
|
||||||
|
db.Save("mentions."+pageId+".mentions."+id+".url", extractUrl(link))
|
||||||
|
db.Save("mentions."+pageId+".mentions."+id+".reviewed", STATUS_MENTION_NOT_REVIEWED)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: send notification to misfin
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateUrl(remoteUrl string) (string, error) {
|
||||||
|
remote, e := url.QueryUnescape(remoteUrl)
|
||||||
|
if e != nil {
|
||||||
|
return "", fmt.Errorf("Provided URL is not a good URL: %s", e)
|
||||||
|
}
|
||||||
|
remote = strings.Replace(remote, "..", "", -1)
|
||||||
|
|
||||||
|
u, err := url.Parse(remote)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Provided URL is not a good URL: %s", err)
|
||||||
|
} else if u.Scheme != "gemini" && u.Scheme != "" {
|
||||||
|
return "", fmt.Errorf("Only gemini url are supported for now.")
|
||||||
|
} else {
|
||||||
|
return "gemini://" + u.Host + u.Path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchGeminiPage(remoteUrl string) (*gemini.Response, error) {
|
||||||
|
gemclient := &gemini.Client{}
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), time.Duration(maxRequestTime)*time.Second)
|
||||||
|
response, err := gemclient.Get(ctx, remoteUrl)
|
||||||
|
if err != nil {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if respCert := response.TLS().PeerCertificates; len(respCert) > 0 &&
|
||||||
|
time.Now().After(respCert[0].NotAfter) {
|
||||||
|
return response, fmt.Errorf("Certificate is unvalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findMentionLinks(content string, capsuleRootAddress string) []string {
|
||||||
|
exp := "(?im)^(=>)[ ]?gemini://" + capsuleRootAddress + "[^ ]+([ ](.*))?$"
|
||||||
|
re := regexp.MustCompile(exp)
|
||||||
|
return re.FindAllString(content, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractUrl(url string) string {
|
||||||
|
exp := "(?im)gemini://[^ ]+"
|
||||||
|
re := regexp.MustCompile(exp)
|
||||||
|
return re.FindString(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMentionsForPage(pageId string, reviewStatus string) map[string]string {
|
||||||
|
mentions := make(map[string]string)
|
||||||
|
db, err := zordfsdb.InitDB("./db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return mentions
|
||||||
|
}
|
||||||
|
|
||||||
|
_, found := db.Get("mentions." + pageId + ".mentions")
|
||||||
|
if !found {
|
||||||
|
return mentions
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range db.Keys("mentions." + pageId + ".mentions") {
|
||||||
|
reviewed, _ := db.Get("mentions." + pageId + ".mentions." + id + ".reviewed")
|
||||||
|
if reviewed == reviewStatus {
|
||||||
|
url, _ := db.Get("mentions." + pageId + ".mentions." + id + ".url")
|
||||||
|
mentions[id] = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mentions
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarkReviewed(pageId string, id string) {
|
||||||
|
db, err := zordfsdb.InitDB("./db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Save("mentions."+pageId+".mentions."+id+".reviewed", STATUS_MENTION_REVIEWED)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveMention(pageId string, id string) {
|
||||||
|
db, err := zordfsdb.InitDB("./db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Del("mentions." + pageId + ".mentions." + id)
|
||||||
|
}
|
7
templates/mention.gmi
Normal file
7
templates/mention.gmi
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% import "macros.tpl" home %}
|
||||||
|
# {{ T('newMention') }}
|
||||||
|
|
||||||
|
{{ T('thanksForMention') }}: {{ q }}
|
||||||
|
|
||||||
|
=> /r/{{id}} {{ T('backToPage') }}
|
||||||
|
{{ home(lang) }}
|
|
@ -14,8 +14,24 @@ Language: {{ file.File.Lang }}
|
||||||
|
|
||||||
{{ category_url(file.File.CategoryPath, file.File.Category|last, 0) }}
|
{{ category_url(file.File.CategoryPath, file.File.Category|last, 0) }}
|
||||||
|
|
||||||
|
### {{ T('mentions') }}
|
||||||
|
{% if mentions %}
|
||||||
|
{% for id, mention in mentions %}=> {{ mention}}
|
||||||
|
{% endfor %} {% endif %}
|
||||||
|
=> /mention/{{file.File.Id}} {{ T('newMention') }}
|
||||||
|
|
||||||
{{ home(lang) }}
|
{{ home(lang) }}
|
||||||
|
|
||||||
```
|
```
|
||||||
Page counter: {{pageCounter}}
|
Page counter: {{pageCounter}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
{% if admin and adminWaitingMentions %}
|
||||||
|
### Keravuotuojė sekcėjė
|
||||||
|
{% for id, mention in adminWaitingMentions %}
|
||||||
|
=> {{ mention }}
|
||||||
|
=> /admin/mentions/review/{{file.File.Id}}/{{id}} ✓ Patvėrtintė
|
||||||
|
=> /admin/mentions/remove/{{file.File.Id}}/{{id}} 🗙 Pašalintė
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
Loading…
Add table
Reference in a new issue