Add support for time tags from Discord
This commit is contained in:
116
formatter_tag.go
116
formatter_tag.go
@@ -18,9 +18,11 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
@@ -83,6 +85,41 @@ func (n *astDiscordChannelMention) String() string {
|
||||
return fmt.Sprintf("<#%d>", n.id)
|
||||
}
|
||||
|
||||
type discordTimestampStyle rune
|
||||
|
||||
func (dts discordTimestampStyle) Format() string {
|
||||
switch dts {
|
||||
case 't':
|
||||
return "15:04 MST"
|
||||
case 'T':
|
||||
return "15:04:05 MST"
|
||||
case 'd':
|
||||
return "2006-01-02 MST"
|
||||
case 'D':
|
||||
return "2 January 2006 MST"
|
||||
case 'F':
|
||||
return "Monday, 2 January 2006 15:04 MST"
|
||||
case 'f':
|
||||
fallthrough
|
||||
default:
|
||||
return "2 January 2006 15:04 MST"
|
||||
}
|
||||
}
|
||||
|
||||
type astDiscordTimestamp struct {
|
||||
astDiscordTag
|
||||
|
||||
timestamp int64
|
||||
style discordTimestampStyle
|
||||
}
|
||||
|
||||
func (n *astDiscordTimestamp) String() string {
|
||||
if n.style == 'f' {
|
||||
return fmt.Sprintf("<t:%d>", n.timestamp)
|
||||
}
|
||||
return fmt.Sprintf("<t:%d:%c>", n.timestamp, n.style)
|
||||
}
|
||||
|
||||
type astDiscordCustomEmoji struct {
|
||||
astDiscordTag
|
||||
name string
|
||||
@@ -98,7 +135,8 @@ func (n *astDiscordCustomEmoji) String() string {
|
||||
|
||||
type discordTagParser struct{}
|
||||
|
||||
var discordTagRegex = regexp.MustCompile(`<(a?:\w+:|@[!&]?|#)(\d+)(?::(\d+):(.+?))?>`)
|
||||
// Regex to match everything in https://discord.com/developers/docs/reference#message-formatting
|
||||
var discordTagRegex = regexp.MustCompile(`<(a?:\w+:|@[!&]?|#|t:)(\d+)(?::([tTdDfFR])|(\d+):(.+?))?>`)
|
||||
var defaultDiscordTagParser = &discordTagParser{}
|
||||
|
||||
func (s *discordTagParser) Trigger() []byte {
|
||||
@@ -131,11 +169,23 @@ func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.C
|
||||
case tagName == "#":
|
||||
var guildID int64
|
||||
var channelName string
|
||||
if len(match[3]) > 0 && len(match[4]) > 0 {
|
||||
guildID, _ = strconv.ParseInt(string(match[3]), 10, 64)
|
||||
channelName = string(match[4])
|
||||
if len(match[4]) > 0 && len(match[5]) > 0 {
|
||||
guildID, _ = strconv.ParseInt(string(match[4]), 10, 64)
|
||||
channelName = string(match[5])
|
||||
}
|
||||
return &astDiscordChannelMention{astDiscordTag: tag, guildID: guildID, name: channelName}
|
||||
case tagName == "t:":
|
||||
var style discordTimestampStyle
|
||||
if len(match[3]) == 0 {
|
||||
style = 'f'
|
||||
} else {
|
||||
style = discordTimestampStyle(match[3][0])
|
||||
}
|
||||
return &astDiscordTimestamp{
|
||||
astDiscordTag: tag,
|
||||
timestamp: id,
|
||||
style: style,
|
||||
}
|
||||
case strings.HasPrefix(tagName, ":"):
|
||||
return &astDiscordCustomEmoji{name: tagName, astDiscordTag: tag}
|
||||
case strings.HasPrefix(tagName, "a:"):
|
||||
@@ -157,6 +207,51 @@ func (r *discordTagHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegi
|
||||
reg.Register(astKindDiscordTag, r.renderDiscordMention)
|
||||
}
|
||||
|
||||
func relativeTimeFormat(ts time.Time) string {
|
||||
now := time.Now()
|
||||
if ts.Year() >= 2262 {
|
||||
return "date out of range for relative format"
|
||||
}
|
||||
duration := ts.Sub(now)
|
||||
word := "in %s"
|
||||
if duration < 0 {
|
||||
duration = -duration
|
||||
word = "%s ago"
|
||||
}
|
||||
var count int
|
||||
var unit string
|
||||
switch {
|
||||
case duration < time.Second:
|
||||
count = int(duration.Milliseconds())
|
||||
unit = "millisecond"
|
||||
case duration < time.Minute:
|
||||
count = int(math.Round(duration.Seconds()))
|
||||
unit = "second"
|
||||
case duration < time.Hour:
|
||||
count = int(math.Round(duration.Minutes()))
|
||||
unit = "minute"
|
||||
case duration < 24*time.Hour:
|
||||
count = int(math.Round(duration.Hours()))
|
||||
unit = "hour"
|
||||
case duration < 30*24*time.Hour:
|
||||
count = int(math.Round(duration.Hours() / 24))
|
||||
unit = "day"
|
||||
case duration < 365*24*time.Hour:
|
||||
count = int(math.Round(duration.Hours() / 24 / 30))
|
||||
unit = "month"
|
||||
default:
|
||||
count = int(math.Round(duration.Hours() / 24 / 365))
|
||||
unit = "year"
|
||||
}
|
||||
var diff string
|
||||
if count == 1 {
|
||||
diff = fmt.Sprintf("a %s", unit)
|
||||
} else {
|
||||
diff = fmt.Sprintf("%d %ss", count, unit)
|
||||
}
|
||||
return fmt.Sprintf(word, diff)
|
||||
}
|
||||
|
||||
func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source []byte, n ast.Node, entering bool) (status ast.WalkStatus, err error) {
|
||||
status = ast.WalkContinue
|
||||
if !entering {
|
||||
@@ -188,6 +283,19 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
|
||||
_, _ = fmt.Fprintf(w, `<img data-mx-emoticon src="%[1]s" alt="%[2]s" title="%[2]s" height="32"/>`, reactionMXC.String(), node.name)
|
||||
return
|
||||
}
|
||||
case *astDiscordTimestamp:
|
||||
ts := time.Unix(node.timestamp, 0).UTC()
|
||||
var formatted string
|
||||
if node.style == 'R' {
|
||||
formatted = relativeTimeFormat(ts)
|
||||
} else {
|
||||
formatted = ts.Format(node.style.Format())
|
||||
}
|
||||
// https://github.com/matrix-org/matrix-spec-proposals/pull/3160
|
||||
const fullDatetimeFormat = "2006-01-02T15:04:05.000-0700"
|
||||
fullRFC := ts.Format(fullDatetimeFormat)
|
||||
fullHumanReadable := ts.Format(discordTimestampStyle('F').Format())
|
||||
_, _ = fmt.Fprintf(w, `<time title="%s" datetime="%s"><strong>%s</strong></time>`, fullHumanReadable, fullRFC, formatted)
|
||||
}
|
||||
stringifiable, ok := n.(mautrix.Stringifiable)
|
||||
if ok {
|
||||
|
||||
Reference in New Issue
Block a user