Skip to content

Commit

Permalink
Merge pull request #185 from Southclaws/mentions
Browse files Browse the repository at this point in the history
add mentions in content and notify members when mentioned in threads
  • Loading branch information
Southclaws authored Sep 12, 2024
2 parents 44cad02 + ffec502 commit 3c953e2
Show file tree
Hide file tree
Showing 78 changed files with 4,872 additions and 369 deletions.
2 changes: 1 addition & 1 deletion api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3069,7 +3069,7 @@ components:
The kind of event that triggered the notification.
Identical to the `notification.Event` enumerated type.
type: string
enum: [thread_reply, post_like, follow]
enum: [thread_reply, post_like, follow, profile_mention]

NotificationStatus:
type: string
Expand Down
5 changes: 2 additions & 3 deletions app/resources/account/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import (
"github.com/Southclaws/fault"
"github.com/Southclaws/fault/ftag"
"github.com/Southclaws/opt"
"github.com/Southclaws/storyden/app/resources/datagraph"
"github.com/rs/xid"

"github.com/Southclaws/storyden/app/resources/content"
)

var errSuspended = fault.Wrap(fault.New("suspended"), ftag.With(ftag.PermissionDenied))
Expand All @@ -22,7 +21,7 @@ type Account struct {
ID AccountID
Handle string
Name string
Bio content.Rich
Bio datagraph.Content
Admin bool
Followers int
Following int
Expand Down
4 changes: 2 additions & 2 deletions app/resources/account/account_writer/account_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"go.uber.org/fx"

"github.com/Southclaws/storyden/app/resources/account"
"github.com/Southclaws/storyden/app/resources/content"
"github.com/Southclaws/storyden/app/resources/datagraph"
"github.com/Southclaws/storyden/internal/ent"
"github.com/Southclaws/storyden/internal/ent/schema"
)
Expand Down Expand Up @@ -47,7 +47,7 @@ func WithName(name string) Option {
}
}

func WithBio(v content.Rich) Option {
func WithBio(v datagraph.Content) Option {
return func(a *account.Account) {
a.Bio = v
}
Expand Down
4 changes: 2 additions & 2 deletions app/resources/account/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/Southclaws/fault"
"github.com/Southclaws/opt"

"github.com/Southclaws/storyden/app/resources/content"
"github.com/Southclaws/storyden/app/resources/datagraph"
"github.com/Southclaws/storyden/internal/ent"
"github.com/Southclaws/storyden/internal/ent/schema"
)
Expand All @@ -18,7 +18,7 @@ func MapAccount(a *ent.Account) (*Account, error) {
return a.Service
})

bio, err := content.NewRichText(a.Bio)
bio, err := datagraph.NewRichText(a.Bio)
if err != nil {
return nil, err
}
Expand Down
7 changes: 4 additions & 3 deletions app/resources/account/notification/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ package notification
type eventEnum string

const (
eventThreadReply eventEnum = "thread_reply"
eventPostLike eventEnum = "post_like"
eventFollow eventEnum = "follow"
eventThreadReply eventEnum = "thread_reply"
eventPostLike eventEnum = "post_like"
eventFollow eventEnum = "follow"
eventProfileMention eventEnum = "profile_mention"
)
9 changes: 6 additions & 3 deletions app/resources/account/notification/notification_enum_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions app/resources/asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,22 @@ import (
"github.com/Southclaws/opt"
"github.com/rs/xid"

"github.com/Southclaws/storyden/app/resources/account"
"github.com/Southclaws/storyden/internal/ent"
)

var errInvalidFormat = fault.New("invalid format")

type Repository interface {
Add(ctx context.Context,
owner account.AccountID,
owner xid.ID,
filename Filename,
size int,
) (*Asset, error)

Get(ctx context.Context, id Filename) (*Asset, error)
GetByID(ctx context.Context, id AssetID) (*Asset, error)

Remove(ctx context.Context, owner account.AccountID, id Filename) error
Remove(ctx context.Context, owner xid.ID, id Filename) error
}

type AssetID = xid.ID
Expand Down
5 changes: 2 additions & 3 deletions app/resources/asset/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/Southclaws/fault/ftag"
"github.com/rs/xid"

"github.com/Southclaws/storyden/app/resources/account"
"github.com/Southclaws/storyden/internal/ent"
"github.com/Southclaws/storyden/internal/ent/asset"
)
Expand All @@ -22,7 +21,7 @@ func New(db *ent.Client) Repository {
}

func (d *database) Add(ctx context.Context,
accountID account.AccountID,
accountID xid.ID,
filename Filename,
size int,
) (*Asset, error) {
Expand Down Expand Up @@ -68,7 +67,7 @@ func (d *database) GetByID(ctx context.Context, id AssetID) (*Asset, error) {
return FromModel(asset), nil
}

func (d *database) Remove(ctx context.Context, accountID account.AccountID, id Filename) error {
func (d *database) Remove(ctx context.Context, accountID xid.ID, id Filename) error {
q := d.db.Asset.
Delete().Where(
asset.Filename(id.name),
Expand Down
133 changes: 102 additions & 31 deletions app/resources/content/content.go → app/resources/datagraph/content.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
package content
package datagraph

import (
"bytes"
"fmt"
"io"
"math"
"net/url"
"regexp"
"strings"
"unicode"

"github.com/Southclaws/fault"
"github.com/cixtor/readability"
"github.com/microcosm-cc/bluemonday"
"github.com/samber/lo"
"go.uber.org/zap"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)

var policy = bluemonday.UGCPolicy()
// RefScheme is used as a scheme for URIs to reference resources.
// These can be used in content to refer to profiles, posts, nodes, etc.
const RefScheme = "sdr"

var policy = func() *bluemonday.Policy {
p := bluemonday.UGCPolicy()

p.AllowURLSchemes(
"mailto",
"http",
"https",
RefScheme,
)

p.AllowDataAttributes()

return p
}()

var spaces = regexp.MustCompile(`\s+`)

// MaxSummaryLength is the maximum length of the short summary text
const MaxSummaryLength = 128
Expand All @@ -24,14 +47,15 @@ const MaxSummaryLength = 128
// string but on the read path, it'll get turned into this either way.
const EmptyState = `<body></body>`

type Rich struct {
type Content struct {
html *html.Node
short string
links []string
sdrs RefList
}

func (r Rich) HTML() string {
if r.html == nil {
func (r Content) HTML() string {
if r.html == nil || r.html.FirstChild == nil {
return EmptyState
}

Expand All @@ -45,34 +69,72 @@ func (r Rich) HTML() string {
return w.String()
}

func (r Rich) HTMLTree() *html.Node {
func (r Content) HTMLTree() *html.Node {
return r.html
}

func (r Rich) Short() string {
func (r Content) Short() string {
return r.short
}

func (r Rich) Links() []string {
func (r Content) Links() []string {
return r.links
}

func (r Content) References() RefList {
return r.sdrs
}

type options struct {
baseURL string
}
type option func(*options)

// NewRichText will pull out any meaningful structured information from markdown
// document this includes a summary of the text and all link URLs for hydrating.
func NewRichText(raw string) (Rich, error) {
sanitised := policy.Sanitize(raw)
htmlTree, err := html.Parse(strings.NewReader(sanitised))
func NewRichText(raw string) (Content, error) {
return NewRichTextFromReader(strings.NewReader(raw))
}

func NewRichTextFromReader(r io.Reader, opts ...option) (Content, error) {
o := options{baseURL: "ignore:"}
for _, opt := range opts {
opt(&o)
}

buf, err := io.ReadAll(r)
if err != nil {
return Content{}, fault.Wrap(err)
}

sanitised := policy.SanitizeBytes(buf)

htmlTree, err := html.Parse(bytes.NewReader(sanitised))
if err != nil {
return Content{}, fault.Wrap(err)
}

result, err := readability.New().Parse(bytes.NewReader(sanitised), o.baseURL)
if err != nil {
return Rich{}, fault.Wrap(err)
return Content{}, fault.Wrap(err)
}

return NewRichTextFromHTML(htmlTree)
short := getSummary(result)

bodyTree, links, refs := extractReferences(htmlTree)

return Content{
html: bodyTree,
short: short,
links: links,
sdrs: refs,
}, nil
}

func NewRichTextFromHTML(htmlTree *html.Node) (Rich, error) {
func extractReferences(htmlTree *html.Node) (*html.Node, []string, RefList) {
bodyTree := &html.Node{}
textonly := strings.Builder{}
links := []string{}
sdrs := []url.URL{}

if htmlTree.DataAtom == atom.Body {
bodyTree = htmlTree
Expand All @@ -89,18 +151,14 @@ func NewRichTextFromHTML(htmlTree *html.Node) (Rich, error) {

if hasHref {
if parsed, err := url.Parse(href.Val); err == nil {
links = append(links, parsed.String())
switch parsed.Scheme {
case "http", "https":
links = append(links, parsed.String())
case RefScheme:
sdrs = append(sdrs, *parsed)
}
}
}

case atom.P:
if n.Type == html.TextNode && len(n.Data) > 0 {

oneline := strings.ReplaceAll(n.Data, "\n", " ")
textonly.Write([]byte(oneline))
textonly.WriteByte(' ')
return
}
}
}

Expand All @@ -115,7 +173,24 @@ func NewRichTextFromHTML(htmlTree *html.Node) (Rich, error) {
}
walk(htmlTree)

paragraphs := []rune(strings.TrimSpace(textonly.String()))
var refs RefList
for _, v := range sdrs {
r, err := NewRefFromSDR(v)
if err != nil {
zap.L().Warn("invalid SDR in content", zap.Error(err))
continue
}
refs = append(refs, r)
}

return bodyTree, links, refs
}

func getSummary(article readability.Article) string {
trimmed := strings.TrimSpace(article.TextContent)
collapsed := spaces.ReplaceAllString(trimmed, " ")

paragraphs := []rune(collapsed)
end := int(math.Min(float64(len(paragraphs)-1), MaxSummaryLength))

var short string
Expand Down Expand Up @@ -147,9 +222,5 @@ func NewRichTextFromHTML(htmlTree *html.Node) (Rich, error) {
short = string(paragraphs)
}

return Rich{
html: bodyTree,
short: short,
links: links,
}, nil
return short
}
Loading

0 comments on commit 3c953e2

Please sign in to comment.