Compare commits

...

8 Commits

Author SHA1 Message Date
r e50f12b615 Restrict instance domain in single_instance mode 2023-09-18 10:07:54 +00:00
r ad38855261 Set timeout and response size limit for the http client 2023-09-18 04:05:20 +00:00
r 60ccc9686a fluoride: Allow submitting the form with Ctrl+Enter 2023-09-09 08:14:16 +00:00
r 60392e61c7 Disable access log by default
Access logs aren't really useful during normal operation. Add a new flag
-v to enable the verbose logging mode, which is still useful during the
development.

Also remove the log_file config because it's no longer useful.
2023-09-09 06:38:52 +00:00
r 8eec93e028 Trim leading and trailing white space from selectable text 2023-09-08 18:11:07 +00:00
r 461908e031 Load CSS on the root page
This applies the background color to the root page and avoids flicker
during the initial page load.
2023-09-08 18:10:29 +00:00
r 426e9ad14f Fix display name and title on mute page 2023-09-08 14:46:40 +00:00
r 8a26dd1908 Fix userlist margin 2023-09-08 14:38:51 +00:00
19 changed files with 103 additions and 64 deletions

View File

@ -31,9 +31,6 @@ static_directory=static
# Empty value will disable the format selection in frontend. # Empty value will disable the format selection in frontend.
post_formats=PlainText:text/plain,HTML:text/html,Markdown:text/markdown,BBCode:text/bbcode post_formats=PlainText:text/plain,HTML:text/html,Markdown:text/markdown,BBCode:text/bbcode
# Log file. Will log to stdout if value is empty.
# log_file=log
# In single instance mode, bloat will not ask for instance domain name and # In single instance mode, bloat will not ask for instance domain name and
# user will be directly redirected to login form. User login from other # user will be directly redirected to login form. User login from other
# instances is not allowed in this mode. # instances is not allowed in this mode.

View File

@ -20,7 +20,6 @@ type config struct {
TemplatesPath string TemplatesPath string
CustomCSS string CustomCSS string
PostFormats []model.PostFormat PostFormats []model.PostFormat
LogFile string
} }
func (c *config) IsValid() bool { func (c *config) IsValid() bool {
@ -97,7 +96,7 @@ func Parse(r io.Reader) (c *config, err error) {
} }
c.PostFormats = formats c.PostFormats = formats
case "log_file": case "log_file":
c.LogFile = val // ignore
default: default:
return nil, errors.New("invalid config key " + key) return nil, errors.New("invalid config key " + key)
} }

18
main.go
View File

@ -26,6 +26,7 @@ func errExit(err error) {
func main() { func main() {
configFile := flag.String("f", "", "config file") configFile := flag.String("f", "", "config file")
verbose := flag.Bool("v", false, "verbose mode")
flag.Parse() flag.Parse()
if len(*configFile) > 0 { if len(*configFile) > 0 {
@ -52,25 +53,12 @@ func main() {
customCSS = "/static/" + customCSS customCSS = "/static/" + customCSS
} }
var logger *log.Logger
if len(config.LogFile) < 1 {
logger = log.New(os.Stdout, "", log.LstdFlags)
} else {
lf, err := os.OpenFile(config.LogFile,
os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
errExit(err)
}
defer lf.Close()
logger = log.New(lf, "", log.LstdFlags)
}
s := service.NewService(config.ClientName, config.ClientScope, s := service.NewService(config.ClientName, config.ClientScope,
config.ClientWebsite, customCSS, config.SingleInstance, config.ClientWebsite, customCSS, config.SingleInstance,
config.PostFormats, renderer) config.PostFormats, renderer)
handler := service.NewHandler(s, logger, config.StaticDirectory) handler := service.NewHandler(s, *verbose, config.StaticDirectory)
logger.Println("listening on", config.ListenAddress) log.Println("listening on", config.ListenAddress)
err = http.ListenAndServe(config.ListenAddress, handler) err = http.ListenAndServe(config.ListenAddress, handler)
if err != nil { if err != nil {
errExit(err) errExit(err)

View File

@ -11,7 +11,6 @@ import (
// AppConfig is a setting for registering applications. // AppConfig is a setting for registering applications.
type AppConfig struct { type AppConfig struct {
http.Client
Server string Server string
ClientName string ClientName string
@ -62,7 +61,7 @@ func RegisterApp(ctx context.Context, appConfig *AppConfig) (*Application, error
} }
req = req.WithContext(ctx) req = req.WithContext(ctx)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := appConfig.Do(req) resp, err := httpClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }

41
mastodon/http.go Normal file
View File

@ -0,0 +1,41 @@
package mastodon
import (
"fmt"
"io"
"net/http"
"time"
)
type lr struct {
io.ReadCloser
r *http.Request
}
func (r *lr) Read(p []byte) (n int, err error) {
n, err = r.ReadCloser.Read(p)
// override the generic error returned by the MaxBytesReader
if _, ok := err.(*http.MaxBytesError); ok {
err = fmt.Errorf("%s \"%s\": response body too large", r.r.Method, r.r.URL)
}
return
}
type transport struct {
t http.RoundTripper
}
func (t *transport) RoundTrip(r *http.Request) (*http.Response, error) {
resp, err := t.t.RoundTrip(r)
if resp != nil && resp.Body != nil {
resp.Body = &lr{http.MaxBytesReader(nil, resp.Body, 8<<20), r}
}
return resp, err
}
var httpClient = &http.Client{
Transport: &transport{
t: http.DefaultTransport,
},
Timeout: 30 * time.Second,
}

View File

@ -168,12 +168,13 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in
} }
} }
return json.NewDecoder(resp.Body).Decode(&res) return json.NewDecoder(resp.Body).Decode(&res)
} }
// NewClient return new mastodon API client. // NewClient return new mastodon API client.
func NewClient(config *Config) *Client { func NewClient(config *Config) *Client {
return &Client{ return &Client{
Client: http.DefaultClient, Client: httpClient,
config: config, config: config,
} }
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -68,7 +69,7 @@ func (c *client) redirect(url string) {
c.w.WriteHeader(http.StatusFound) c.w.WriteHeader(http.StatusFound)
} }
func (c *client) authenticate(t int) (err error) { func (c *client) authenticate(t int, instance string) (err error) {
csrf := c.r.FormValue("csrf_token") csrf := c.r.FormValue("csrf_token")
ref := c.r.URL.RequestURI() ref := c.r.URL.RequestURI()
defer func() { defer func() {
@ -98,6 +99,9 @@ func (c *client) authenticate(t int) (err error) {
return err return err
} }
c.s = sess c.s = sess
if len(instance) > 0 && c.s.Instance != instance {
return errors.New("invalid instance")
}
c.Client = mastodon.NewClient(&mastodon.Config{ c.Client = mastodon.NewClient(&mastodon.Config{
Server: "https://" + c.s.Instance, Server: "https://" + c.s.Instance,
ClientID: c.s.ClientID, ClientID: c.s.ClientID,

View File

@ -683,7 +683,7 @@ func (s *service) MutePage(c *client, id string) (err error) {
if err != nil { if err != nil {
return return
} }
cdata := s.cdata(c, "Mute"+user.DisplayName+" @"+user.Acct, 0, 0, "") cdata := s.cdata(c, "Mute "+user.DisplayName+" @"+user.Acct, 0, 0, "")
data := &renderer.UserData{ data := &renderer.UserData{
User: user, User: user,
CommonData: cdata, CommonData: cdata,

View File

@ -23,7 +23,7 @@ const (
CSRF CSRF
) )
func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler { func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
r := mux.NewRouter() r := mux.NewRouter()
writeError := func(c *client, err error, t int, retry bool) { writeError := func(c *client, err error, t int, retry bool) {
@ -48,10 +48,12 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
r: req, r: req,
} }
defer func(begin time.Time) { if verbose {
logger.Printf("path=%s, err=%v, took=%v\n", defer func(begin time.Time) {
req.URL.Path, err, time.Since(begin)) log.Printf("path=%s, err=%v, took=%v\n",
}(time.Now()) req.URL.Path, err, time.Since(begin))
}(time.Now())
}
var ct string var ct string
switch rt { switch rt {
@ -62,7 +64,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
} }
c.w.Header().Add("Content-Type", ct) c.w.Header().Add("Content-Type", ct)
err = c.authenticate(at) err = c.authenticate(at, s.instance)
if err != nil { if err != nil {
writeError(c, err, rt, req.Method == http.MethodGet) writeError(c, err, rt, req.Method == http.MethodGet)
return return
@ -77,7 +79,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
} }
rootPage := handle(func(c *client) error { rootPage := handle(func(c *client) error {
err := c.authenticate(SESSION) err := c.authenticate(SESSION, "")
if err != nil { if err != nil {
if err == errInvalidSession { if err == errInvalidSession {
c.redirect("/signin") c.redirect("/signin")

View File

@ -285,6 +285,12 @@ function onPaste(e) {
fp.files = dt.files; fp.files = dt.files;
} }
function onKeydown(e) {
if (e.key == 'Enter' && e.ctrlKey) {
document.querySelector(".post-form").submit();
}
}
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
checkCSRFToken(); checkCSRFToken();
checkAntiDopamineMode(); checkAntiDopamineMode();
@ -325,8 +331,10 @@ document.addEventListener("DOMContentLoaded", function() {
} }
var pf = document.querySelector(".post-form") var pf = document.querySelector(".post-form")
if (pf) if (pf) {
pf.addEventListener("paste", onPaste); pf.addEventListener("paste", onPaste);
pf.addEventListener("keydown", onKeydown);
}
}); });
// @license-end // @license-end

View File

@ -1,4 +1,4 @@
body { frame, body {
background-color: #d2d2d2; background-color: #d2d2d2;
} }
@ -436,7 +436,8 @@ img.emoji {
.user-list-item { .user-list-item {
overflow: auto; overflow: auto;
margin: 0 0 12px 0; margin: 0 0 4px 0;
padding: 4px;
display: flex; display: flex;
align-items: center; align-items: center;
} }

View File

@ -1,6 +1,6 @@
{{with .Data}} {{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}} {{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
<div class="page-title"> Mute {{.User.Acct}} </div> <div class="page-title"> Mute {{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}} @{{.User.Acct}} </div>
<form action="/mute/{{.User.ID}}" method="POST"> <form action="/mute/{{.User.ID}}" method="POST">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">

View File

@ -8,9 +8,9 @@
</div> </div>
<div class="user-info-details-container"> <div class="user-info-details-container">
<div class="user-info-details-name"> <div class="user-info-details-name">
<bdi class="status-dname"> {{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}} </bdi> <bdi class="status-dname">{{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}}</bdi>
<a class="nav-link" href="/user/{{.User.ID}}" accesskey="0" title="User profile (0)"> <a class="nav-link" href="/user/{{.User.ID}}" accesskey="0" title="User profile (0)">
<span class="status-uname"> @{{.User.Acct}} </span> <span class="status-uname">@{{.User.Acct}}</span>
</a> </a>
</div> </div>
<div class="user-info-details-nav"> <div class="user-info-details-nav">

View File

@ -28,13 +28,13 @@
</div> </div>
<div class="notification-follow"> <div class="notification-follow">
<div class="notification-info-text"> <div class="notification-info-text">
<bdi class="status-dname"> {{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}} </bdi> <bdi class="status-dname">{{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}}</bdi>
<span class="notification-text"> followed you - <span class="notification-text"> followed you -
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time> <time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
</span> </span>
</div> </div>
<div> <div>
<a href="/user/{{.Account.ID}}"> <span class="status-uname"> @{{.Account.Acct}} </span> </a> <a href="/user/{{.Account.ID}}"> <span class="status-uname">@{{.Account.Acct}}</span> </a>
</div> </div>
</div> </div>
</div> </div>
@ -48,13 +48,13 @@
</div> </div>
<div class="notification-follow"> <div class="notification-follow">
<div class="notification-info-text"> <div class="notification-info-text">
<bdi class="status-dname"> {{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}} </bdi> <bdi class="status-dname">{{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}}</bdi>
<span class="notification-text"> wants to follow you - <span class="notification-text"> wants to follow you -
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time> <time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
</span> </span>
</div> </div>
<div> <div>
<a href="/user/{{.Account.ID}}"> <span class="status-uname"> @{{.Account.Acct}} </span> </a> <a href="/user/{{.Account.ID}}"><span class="status-uname">@{{.Account.Acct}}</span></a>
</div> </div>
<form class="d-inline" action="/accept/{{.Account.ID}}" method="post" target="_self"> <form class="d-inline" action="/accept/{{.Account.ID}}" method="post" target="_self">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
@ -79,7 +79,7 @@
<img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="48" /> <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="48" />
</a> </a>
<a href="/user/{{.Account.ID}}"> <a href="/user/{{.Account.ID}}">
<span class="status-uname"> @{{.Account.Acct}} </span> <span class="status-uname">@{{.Account.Acct}}</span>
</a> </a>
<span class="notification-text"> retweeted your post - <span class="notification-text"> retweeted your post -
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time> <time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
@ -93,7 +93,7 @@
<img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="48" /> <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="48" />
</a> </a>
<a href="/user/{{.Account.ID}}"> <a href="/user/{{.Account.ID}}">
<span class="status-uname"> @{{.Account.Acct}} </span> <span class="status-uname">@{{.Account.Acct}}</span>
</a> </a>
<span class="notification-text"> liked your post - <span class="notification-text"> liked your post -
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time> <time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
@ -107,7 +107,7 @@
<img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="48" /> <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="48" />
</a> </a>
<a href="/user/{{.Account.ID}}"> <a href="/user/{{.Account.ID}}">
<span class="status-uname"> @{{.Account.Acct}} </span> <span class="status-uname">@{{.Account.Acct}}</span>
</a> </a>
<span class="notification-text"> {{.Type}} - <span class="notification-text"> {{.Type}} -
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time> <time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>

View File

@ -9,9 +9,9 @@
</div> </div>
<div class="user-list-name"> <div class="user-list-name">
<div> <div>
<div class="status-dname"> {{EmojiFilter (HTML .DisplayName) .Emojis | Raw}} </div> <div class="status-dname">{{EmojiFilter (HTML .DisplayName) .Emojis | Raw}}</div>
<a class="img-link" href="/user/{{.ID}}"> <a class="img-link" href="/user/{{.ID}}">
<div class="status-uname"> @{{.Acct}} </div> <div class="status-uname">{{.Acct}}</div>
</a> </a>
</div> </div>
<form class="d-inline" action="/accept/{{.ID}}" method="post" target="_self"> <form class="d-inline" action="/accept/{{.ID}}" method="post" target="_self">

View File

@ -4,14 +4,15 @@
<head> <head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<link rel="icon" type="image/png" href="/static/favicon.png"> <link rel="icon" type="image/png" href="/static/favicon.png">
<link rel="stylesheet" href="/static/style.css">
<title>{{.Title}}</title> <title>{{.Title}}</title>
</head> </head>
<frameset cols="424px,*"> <frameset cols="424px,*">
<frameset rows="316px,*"> <frameset rows="316px,*">
<frame name="nav" src="/nav"> <frame name="nav" src="/nav" {{if $.Ctx.DarkMode}}class="dark"{{end}}>
<frame name="notification" src="/notifications"> <frame name="notification" src="/notifications" {{if $.Ctx.DarkMode}}class="dark"{{end}}>
</frameset> </frameset>
<frame name="main" src="/timeline/home"> <frame name="main" src="/timeline/home" {{if $.Ctx.DarkMode}}class="dark"{{end}}>
</frameset> </frameset>
</html> </html>
{{end}} {{end}}

View File

@ -5,9 +5,9 @@
<a class="img-link" href="/user/{{.Account.ID}}"> <a class="img-link" href="/user/{{.Account.ID}}">
<img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="24" /> <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="24" />
</a> </a>
<bdi class="status-dname"> {{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}} </bdi> <bdi class="status-dname">{{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}}</bdi>
<a href="/user/{{.Account.ID}}"> <a href="/user/{{.Account.ID}}">
<span class="status-uname"> @{{.Account.Acct}} </span> <span class="status-uname">@{{.Account.Acct}}</span>
</a> </a>
retweeted retweeted
</div> </div>
@ -23,9 +23,9 @@
</div> </div>
<div class="status"> <div class="status">
<div class="status-name"> <div class="status-name">
<bdi class="status-dname"> {{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}} </bdi> <bdi class="status-dname">{{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}}</bdi>
<a href="/user/{{.Account.ID}}"> <a href="/user/{{.Account.ID}}">
<span class="status-uname"> @{{.Account.Acct}} </span> <span class="status-uname">@{{.Account.Acct}}</span>
</a> </a>
<div class="more-container"> <div class="more-container">
<div class="remote-link"> <div class="remote-link">
@ -92,8 +92,8 @@
</div> </div>
{{if (or .Content .SpoilerText)}} {{if (or .Content .SpoilerText)}}
<div class="status-content"> <div class="status-content">
{{if .SpoilerText}}{{EmojiFilter (HTML .SpoilerText) .Emojis | Raw}}<br/>{{end}} {{- if .SpoilerText}}{{EmojiFilter (HTML .SpoilerText) .Emojis | Raw}}<br/>{{end -}}
{{StatusContentFilter .Content .Emojis .Mentions | Raw}} {{- StatusContentFilter .Content .Emojis .Mentions | Raw -}}
</div> </div>
{{end}} {{end}}
{{if .MediaAttachments}} {{if .MediaAttachments}}

View File

@ -3,7 +3,6 @@
<div class="page-title"> User </div> <div class="page-title"> User </div>
<div class="user-info-container"> <div class="user-info-container">
<div>
<div class="user-profile-img-container"> <div class="user-profile-img-container">
<a class="img-link" href="{{.User.Avatar}}" target="_blank"> <a class="img-link" href="{{.User.Avatar}}" target="_blank">
<img class="user-profile-img" src="{{.User.Avatar}}" alt="profile-avatar" height="96" /> <img class="user-profile-img" src="{{.User.Avatar}}" alt="profile-avatar" height="96" />
@ -11,8 +10,8 @@
</div> </div>
<div class="user-profile-details-container"> <div class="user-profile-details-container">
<div> <div>
<bdi class="status-dname"> {{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}} </bdi> <bdi class="status-dname">{{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}}</bdi>
<span class="status-uname"> @{{.User.Acct}} </span> <span class="status-uname">@{{.User.Acct}}</span>
<a class="remote-link" href="{{.User.URL}}" target="_blank" title="remote profile"> <a class="remote-link" href="{{.User.URL}}" target="_blank" title="remote profile">
source source
</a> </a>
@ -120,17 +119,16 @@
</div> </div>
</div> </div>
<div class="user-profile-decription"> <div class="user-profile-decription">
{{EmojiFilter .User.Note .User.Emojis | Raw}} {{- EmojiFilter .User.Note .User.Emojis | Raw -}}
</div> </div>
{{if .User.Fields}} {{if .User.Fields}}
<div class="user-fields"> <div class="user-fields">
{{range .User.Fields}} {{range .User.Fields}}
<div>{{EmojiFilter .Name $.Data.User.Emojis | Raw}} - {{EmojiFilter .Value $.Data.User.Emojis | Raw}}</div> <div>{{- EmojiFilter .Name $.Data.User.Emojis | Raw}} - {{EmojiFilter .Value $.Data.User.Emojis | Raw -}}</div>
{{end}} {{end}}
</div> </div>
{{end}} {{end}}
</div> </div>
</div>
{{if eq .Type ""}} {{if eq .Type ""}}
<div class="page-title"> Statuses </div> <div class="page-title"> Statuses </div>

View File

@ -6,9 +6,9 @@
</a> </a>
</div> </div>
<div class="user-list-name"> <div class="user-list-name">
<div class="status-dname"> {{EmojiFilter (HTML .DisplayName) .Emojis | Raw}} </div> <div class="status-dname">{{EmojiFilter (HTML .DisplayName) .Emojis | Raw}}</div>
<a class="img-link" href="/user/{{.ID}}"> <a class="img-link" href="/user/{{.ID}}">
<div class="status-uname"> @{{.Acct}} </div> <div class="status-uname">@{{.Acct}}</div>
</a> </a>
</div> </div>
</div> </div>