* Add modservice screen and profile-header-card * Drop the guidelines for now * Remove ununsed constants * Add label & label group descriptions * Not found state * Reorg, add icon * Subheader * Header * Complete header * Clean up * Add all groups * Fix scroll view * Dialogs side quest * Remove log * Add (WIP) debug mod page * Dialog solution * Add note * Clean up and reorganize localized moderation strings * Memoize * Add example * Add first ReportDialog screen * Report dialog step 2 * Submit * Integrate updates * Move moderation screen * Migrate buttons * Migrate everything * Rough sketch * Fix types * Update atoms values * Abstract ModerationServiceCard * Hook up data to settings page * Handle subscription * Rough enablement * Rough enablement * Some validation, fixes * More work on the mod debug screen * Hook up data * Update invalidation * Hook up data to ReportDialog * Fix native error * Refactor/rewrite the entire moderation-application system * Fix toggles * Add copyright and other option to report * Handle reports on profile vs content * Little cleanup * Get post hiding back in gear * Better loading flow on Mod screen * Clean up Mod screen * Clean up ProfileMod screen * Handle muting correctly * Update enablement on ProfileMod screen * Improve Moderation screen and dialog * Styling, handle disabled labelers * Rework list of labels on own content * Use moderateNotification() * ReportDialog updates * Fix button overflow * Simplify the ProfileModerationService ui * Mod screen design * Move moderation card from the profile header to a tab * Small tweaks to the moderation screen * Enable toggle on mod page * Add notifs to debugmod and dont filter notifs from followed users * Add moderator-service profile view * Wire up more of the modservice data to profiles * A bunch of speculative non-working UI * Cleanup: delete old code * Update ModerationDetailsDialog * Update ReportDialog * Update LabelsOnMe dialog * Handle ReportDialog load better * Rename LabelsOnMeDialog, fix close * Experiment to put labeling under a tab of a normal profile * Moderator variation of profile * Remove dead code and start moving toward latest modsdk * Remove a bunch of now-dead label strings * Update ModDebug to be a bit more intuitive and support custom labels * Minor ui tweaks * Improve consistency of display name blurring * Fix profile-card warning rendering * More debugmod UI tuning * Update to use new labeler semantics * Delete some dead code and do some refactoring * Update profile to pull from labeler definition * Implement new label config controls (wip) * Tweak ui * Implement preference controls on labelers * Rework label pref ui * Get moderation screen working * Add asyncstorage query persistence * Implement label handling * Small cleanup * Implement Likes dialog * Fix: remove text outside of text element * Cleanup * Fix likes dialog on mobile * Implement the label appeal flow * Get report flow working again with temporarily fixed report options * Update onboarding * Enforce limit of ten labeler subscriptions * Fix type errors * Fix lint errors * Improve types of RQ * Some work on Likes dialog, needs discussion * Bit of ReportDialog cleanup * Replace non-single-path SVG * Update nudity descriptions * Update to use new sdk updates * Add adult-content-enabled behavior to label config * Use the default setting of custom labels * Handle global moderation label prefs with the global settings * Fix missing postAuthor * Fix empty moderation page * Add mutewords control back to Mod screen * Tweak adult setting styles * Remove deprecated global labels * Handle underage users on mod screen * Adjust font sizes * Swap in RichText * Like button improvements * Tweaks to Labeler profile * Design tweaks for mod pref dialog * Add tertiary button color * Switch moderation UIs to tertiary color * Update mutewords and hiddenposts to use the new sdk * Add test-environment mod authority * Switch 'gore' to 'graphic-media' * Move nudity out of the adult content control * Remove focus styles from buttons - let the browser behavior handle it * Fixes to the adult content age-gating in moderaiton * Ditch tertiary button color, lighten secondary button * Fix some colors * Remove focused overrides from toggles * Liked by screen * Rework the moderationlabelpref * Fix optimistic like * Cleanup * Change how onboarding handles adult content enabled/disabled * Add special handling of the mod authorities * Tweaks * Update the default labeler avatar to a shield * Add route to go server * Avoid dups due to bad config * Fix attrs * Fix: dont try to detect link/label mismatches on post meta * Correctly show the label behavior when adult content is disabled * Readd the local hiddenPosts handling * WIP * Fix bad merge * Conten hider design tweaks * Fix text string breakage * Adjust source text in ContentHider * Fix link bug * Design tweaks to ContentHider and ModDetailsDialog * Adjust spacing of inform badges * Adjust spacing of embeds in posts * Style tweaks to post/profile alerts * Labels on me and dialog * Remove bad focus styles from post dropdown * Better spacing solution * Tune moderation UIs * Moderation UI tweaks for mobile * Move labelers query on Mod screen * Update to use new SDK appLabelers semantics * Implement report submission * Replace the report modal entirely with the report dialog * Add @ to mod details dialog handle * Bump SDK package * Remove silly type * Add to AWS build CI * Fix ToggleButton overflow * Clean up ModServiceCard, rename to LabelingServiceCard * Hackfix to translate gore labels to graphic-media * Tune content hider sizing on web desktop * Handle self labels * Fix spacing below text-only posts * Fix: send appeals to the right labeler * Give mod page links interactive states * Fix references * Remove focus handling * Remove remnant * Remove the like count from the subscribed labeler listing * Bump @atproto/api@0.11.1 * Remove extra @ * Fix: persist labels to local storage to reduce coverage gaps * update dipendencies * revert dipendencies * Add some explainers on how blocking affects labelers * Tweak copy * Fix underline color in header * Fix profile menu * Handle card overflow * Remove metrics from header * Mute 'account' not 'user' * Show metrics if self * Show the labels tab on logged out view * Fix bad merge * Use purple theming on labelers * Tighten space on LabelerCard * Set staleTime to 6hrs for labeler details * Memoize the memoizers * Drop staleTime to 60s * Move label defs into a context to reduce recomputes * Submit view tweaks * Move labeler fetch below auth * Mitigation: hardcode the bluesky moderation labeler name * Bump sdk * Add missing translated string Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Add missing translated string Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Hailey's fix for incorrect profile tabs Co-authored-by: Hailey <me@haileyok.com> * Feedback * Fix borders, add bottom space * Hailey's fix pt 2 Co-authored-by: Hailey <me@haileyok.com> * Fix post tabs * Integrate feedback pt 1 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 2 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 3 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 4 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 5 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 6 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 7 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 8 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Format * Integrate new bday modal * Use public agent for getServices * Update casing --------- Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> Co-authored-by: Hailey <me@haileyok.com>
390 lines
12 KiB
Go
390 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
appbsky "github.com/bluesky-social/indigo/api/bsky"
|
|
"github.com/bluesky-social/indigo/atproto/syntax"
|
|
"github.com/bluesky-social/indigo/util/cliutil"
|
|
"github.com/bluesky-social/indigo/xrpc"
|
|
"github.com/bluesky-social/social-app/bskyweb"
|
|
|
|
"github.com/flosch/pongo2/v6"
|
|
"github.com/klauspost/compress/gzhttp"
|
|
"github.com/klauspost/compress/gzip"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
type Server struct {
|
|
echo *echo.Echo
|
|
httpd *http.Server
|
|
xrpcc *xrpc.Client
|
|
}
|
|
|
|
func serve(cctx *cli.Context) error {
|
|
debug := cctx.Bool("debug")
|
|
httpAddress := cctx.String("http-address")
|
|
appviewHost := cctx.String("appview-host")
|
|
|
|
// Echo
|
|
e := echo.New()
|
|
|
|
// create a new session (no auth)
|
|
xrpcc := &xrpc.Client{
|
|
Client: cliutil.NewHttpClient(),
|
|
Host: appviewHost,
|
|
}
|
|
|
|
// httpd
|
|
var (
|
|
httpTimeout = 2 * time.Minute
|
|
httpMaxHeaderBytes = 2 * (1024 * 1024)
|
|
gzipMinSizeBytes = 1024 * 2
|
|
gzipCompressionLevel = gzip.BestSpeed
|
|
gzipExceptMIMETypes = []string{"image/png"}
|
|
)
|
|
|
|
// Wrap the server handler in a gzip handler to compress larger responses.
|
|
gzipHandler, err := gzhttp.NewWrapper(
|
|
gzhttp.MinSize(gzipMinSizeBytes),
|
|
gzhttp.CompressionLevel(gzipCompressionLevel),
|
|
gzhttp.ExceptContentTypes(gzipExceptMIMETypes),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
//
|
|
// server
|
|
//
|
|
server := &Server{
|
|
echo: e,
|
|
xrpcc: xrpcc,
|
|
}
|
|
|
|
// Create the HTTP server.
|
|
server.httpd = &http.Server{
|
|
Handler: gzipHandler(server),
|
|
Addr: httpAddress,
|
|
WriteTimeout: httpTimeout,
|
|
ReadTimeout: httpTimeout,
|
|
MaxHeaderBytes: httpMaxHeaderBytes,
|
|
}
|
|
|
|
e.HideBanner = true
|
|
e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug)
|
|
e.HTTPErrorHandler = server.errorHandler
|
|
|
|
e.IPExtractor = echo.ExtractIPFromXFFHeader()
|
|
|
|
// SECURITY: Do not modify without due consideration.
|
|
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
|
|
ContentTypeNosniff: "nosniff",
|
|
XFrameOptions: "SAMEORIGIN",
|
|
HSTSMaxAge: 31536000, // 365 days
|
|
// TODO:
|
|
// ContentSecurityPolicy
|
|
// XSSProtection
|
|
}))
|
|
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
// Don't log requests for static content.
|
|
Skipper: func(c echo.Context) bool {
|
|
return strings.HasPrefix(c.Request().URL.Path, "/static")
|
|
},
|
|
}))
|
|
e.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
|
|
Skipper: middleware.DefaultSkipper,
|
|
Store: middleware.NewRateLimiterMemoryStoreWithConfig(
|
|
middleware.RateLimiterMemoryStoreConfig{
|
|
Rate: 10, // requests per second
|
|
Burst: 30, // allow bursts
|
|
ExpiresIn: 3 * time.Minute, // garbage collect entries older than 3 minutes
|
|
},
|
|
),
|
|
IdentifierExtractor: func(ctx echo.Context) (string, error) {
|
|
id := ctx.RealIP()
|
|
return id, nil
|
|
},
|
|
DenyHandler: func(c echo.Context, identifier string, err error) error {
|
|
return c.String(http.StatusTooManyRequests, "Your request has been rate limited. Please try again later. Contact security@bsky.app if you believe this was a mistake.\n")
|
|
},
|
|
}))
|
|
|
|
// redirect trailing slash to non-trailing slash.
|
|
// all of our current endpoints have no trailing slash.
|
|
e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{
|
|
RedirectCode: http.StatusFound,
|
|
}))
|
|
|
|
//
|
|
// configure routes
|
|
//
|
|
// static files
|
|
staticHandler := http.FileServer(func() http.FileSystem {
|
|
if debug {
|
|
log.Debugf("serving static file from the local file system")
|
|
return http.FS(os.DirFS("static"))
|
|
}
|
|
fsys, err := fs.Sub(bskyweb.StaticFS, "static")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return http.FS(fsys)
|
|
}())
|
|
|
|
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
|
|
e.GET("/ips-v4", echo.WrapHandler(staticHandler))
|
|
e.GET("/ips-v6", echo.WrapHandler(staticHandler))
|
|
e.GET("/.well-known/*", echo.WrapHandler(staticHandler))
|
|
e.GET("/security.txt", func(c echo.Context) error {
|
|
return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt")
|
|
})
|
|
e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler))
|
|
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
path := c.Request().URL.Path
|
|
maxAge := 1 * (60 * 60) // default is 1 hour
|
|
|
|
// Cache javascript and images files for 1 week, which works because
|
|
// they're always versioned (e.g. /static/js/main.64c14927.js)
|
|
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/images/") {
|
|
maxAge = 7 * (60 * 60 * 24) // 1 week
|
|
}
|
|
|
|
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
|
return next(c)
|
|
}
|
|
})
|
|
|
|
// home
|
|
e.GET("/", server.WebHome)
|
|
|
|
// generic routes
|
|
e.GET("/hashtag/:tag", server.WebGeneric)
|
|
e.GET("/search", server.WebGeneric)
|
|
e.GET("/feeds", server.WebGeneric)
|
|
e.GET("/notifications", server.WebGeneric)
|
|
e.GET("/lists", server.WebGeneric)
|
|
e.GET("/moderation", server.WebGeneric)
|
|
e.GET("/moderation/modlists", server.WebGeneric)
|
|
e.GET("/moderation/muted-accounts", server.WebGeneric)
|
|
e.GET("/moderation/blocked-accounts", server.WebGeneric)
|
|
e.GET("/settings", server.WebGeneric)
|
|
e.GET("/settings/language", server.WebGeneric)
|
|
e.GET("/settings/app-passwords", server.WebGeneric)
|
|
e.GET("/settings/following-feed", server.WebGeneric)
|
|
e.GET("/settings/saved-feeds", server.WebGeneric)
|
|
e.GET("/settings/threads", server.WebGeneric)
|
|
e.GET("/settings/external-embeds", server.WebGeneric)
|
|
e.GET("/sys/debug", server.WebGeneric)
|
|
e.GET("/sys/debug-mod", server.WebGeneric)
|
|
e.GET("/sys/log", server.WebGeneric)
|
|
e.GET("/support", server.WebGeneric)
|
|
e.GET("/support/privacy", server.WebGeneric)
|
|
e.GET("/support/tos", server.WebGeneric)
|
|
e.GET("/support/community-guidelines", server.WebGeneric)
|
|
e.GET("/support/copyright", server.WebGeneric)
|
|
e.GET("/intent/compose", server.WebGeneric)
|
|
|
|
// profile endpoints; only first populates info
|
|
e.GET("/profile/:handleOrDID", server.WebProfile)
|
|
e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
|
|
e.GET("/profile/:handleOrDID/followers", server.WebGeneric)
|
|
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
|
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
|
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
|
e.GET("/profile/:handleOrDID/labeler/liked-by", server.WebGeneric)
|
|
|
|
// profile RSS feed (DID not handle)
|
|
e.GET("/profile/:ident/rss", server.WebProfileRSS)
|
|
|
|
// post endpoints; only first populates info
|
|
e.GET("/profile/:handleOrDID/post/:rkey", server.WebPost)
|
|
e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric)
|
|
e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric)
|
|
|
|
// Start the server.
|
|
log.Infof("starting server address=%s", httpAddress)
|
|
go func() {
|
|
if err := server.httpd.ListenAndServe(); err != nil {
|
|
if !errors.Is(err, http.ErrServerClosed) {
|
|
log.Errorf("HTTP server shutting down unexpectedly: %s", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Wait for a signal to exit.
|
|
log.Info("registering OS exit signal handler")
|
|
quit := make(chan struct{})
|
|
exitSignals := make(chan os.Signal, 1)
|
|
signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM)
|
|
go func() {
|
|
sig := <-exitSignals
|
|
log.Infof("received OS exit signal: %s", sig)
|
|
|
|
// Shut down the HTTP server.
|
|
if err := server.Shutdown(); err != nil {
|
|
log.Errorf("HTTP server shutdown error: %s", err)
|
|
}
|
|
|
|
// Trigger the return that causes an exit.
|
|
close(quit)
|
|
}()
|
|
<-quit
|
|
log.Infof("graceful shutdown complete")
|
|
return nil
|
|
}
|
|
|
|
func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|
srv.echo.ServeHTTP(rw, req)
|
|
}
|
|
|
|
func (srv *Server) Shutdown() error {
|
|
log.Info("shutting down")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
return srv.httpd.Shutdown(ctx)
|
|
}
|
|
|
|
func (srv *Server) errorHandler(err error, c echo.Context) {
|
|
code := http.StatusInternalServerError
|
|
if he, ok := err.(*echo.HTTPError); ok {
|
|
code = he.Code
|
|
}
|
|
c.Logger().Error(err)
|
|
data := pongo2.Context{
|
|
"statusCode": code,
|
|
}
|
|
c.Render(code, "error.html", data)
|
|
}
|
|
|
|
// handler for endpoint that have no specific server-side handling
|
|
func (srv *Server) WebGeneric(c echo.Context) error {
|
|
data := pongo2.Context{}
|
|
return c.Render(http.StatusOK, "base.html", data)
|
|
}
|
|
|
|
func (srv *Server) WebHome(c echo.Context) error {
|
|
data := pongo2.Context{}
|
|
return c.Render(http.StatusOK, "home.html", data)
|
|
}
|
|
|
|
func (srv *Server) WebPost(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
data := pongo2.Context{}
|
|
|
|
// sanity check arguments. don't 4xx, just let app handle if not expected format
|
|
rkeyParam := c.Param("rkey")
|
|
rkey, err := syntax.ParseRecordKey(rkeyParam)
|
|
if err != nil {
|
|
return c.Render(http.StatusOK, "post.html", data)
|
|
}
|
|
handleOrDIDParam := c.Param("handleOrDID")
|
|
handleOrDID, err := syntax.ParseAtIdentifier(handleOrDIDParam)
|
|
if err != nil {
|
|
return c.Render(http.StatusOK, "post.html", data)
|
|
}
|
|
|
|
identifier := handleOrDID.Normalize().String()
|
|
|
|
// requires two fetches: first fetch profile (!)
|
|
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, identifier)
|
|
if err != nil {
|
|
log.Warnf("failed to fetch profile for: %s\t%v", identifier, err)
|
|
return c.Render(http.StatusOK, "post.html", data)
|
|
}
|
|
unauthedViewingOkay := true
|
|
for _, label := range pv.Labels {
|
|
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
|
|
unauthedViewingOkay = false
|
|
}
|
|
}
|
|
|
|
if !unauthedViewingOkay {
|
|
return c.Render(http.StatusOK, "post.html", data)
|
|
}
|
|
did := pv.Did
|
|
data["did"] = did
|
|
|
|
// then fetch the post thread (with extra context)
|
|
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey)
|
|
tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, 0, uri)
|
|
if err != nil {
|
|
log.Warnf("failed to fetch post: %s\t%v", uri, err)
|
|
return c.Render(http.StatusOK, "post.html", data)
|
|
}
|
|
req := c.Request()
|
|
postView := tpv.Thread.FeedDefs_ThreadViewPost.Post
|
|
data["postView"] = postView
|
|
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
|
if postView.Embed != nil {
|
|
if postView.Embed.EmbedImages_View != nil {
|
|
var thumbUrls []string
|
|
for i := range postView.Embed.EmbedImages_View.Images {
|
|
thumbUrls = append(thumbUrls, postView.Embed.EmbedImages_View.Images[i].Thumb)
|
|
}
|
|
data["imgThumbUrls"] = thumbUrls
|
|
} else if postView.Embed.EmbedRecordWithMedia_View != nil && postView.Embed.EmbedRecordWithMedia_View.Media != nil && postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View != nil {
|
|
var thumbUrls []string
|
|
for i := range postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images {
|
|
thumbUrls = append(thumbUrls, postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images[i].Thumb)
|
|
}
|
|
data["imgThumbUrls"] = thumbUrls
|
|
}
|
|
}
|
|
|
|
if postView.Record != nil {
|
|
postRecord, ok := postView.Record.Val.(*appbsky.FeedPost)
|
|
if ok {
|
|
data["postText"] = ExpandPostText(postRecord)
|
|
}
|
|
}
|
|
|
|
return c.Render(http.StatusOK, "post.html", data)
|
|
}
|
|
|
|
func (srv *Server) WebProfile(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
data := pongo2.Context{}
|
|
|
|
// sanity check arguments. don't 4xx, just let app handle if not expected format
|
|
handleOrDIDParam := c.Param("handleOrDID")
|
|
handleOrDID, err := syntax.ParseAtIdentifier(handleOrDIDParam)
|
|
if err != nil {
|
|
return c.Render(http.StatusOK, "profile.html", data)
|
|
}
|
|
identifier := handleOrDID.Normalize().String()
|
|
|
|
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, identifier)
|
|
if err != nil {
|
|
log.Warnf("failed to fetch profile for: %s\t%v", identifier, err)
|
|
return c.Render(http.StatusOK, "profile.html", data)
|
|
}
|
|
unauthedViewingOkay := true
|
|
for _, label := range pv.Labels {
|
|
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
|
|
unauthedViewingOkay = false
|
|
}
|
|
}
|
|
if !unauthedViewingOkay {
|
|
return c.Render(http.StatusOK, "profile.html", data)
|
|
}
|
|
req := c.Request()
|
|
data["profileView"] = pv
|
|
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
|
data["requestHost"] = req.Host
|
|
return c.Render(http.StatusOK, "profile.html", data)
|
|
}
|