this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

import old cmd/pubweb work as cmd/athome

+704
+14
cmd/athome/README.md
··· 1 + 2 + athome: Public Bluesky Web Home 3 + =============================== 4 + 5 + ```text 6 + me: can we have public web interface? 7 + mom: we have public web interface at home 8 + public web interface at home: 9 + ``` 10 + 11 + 1. run this web service somewhere 12 + 2. point one or more handle domains to it (CNAME or reverse proxy) 13 + 3. serves up profile and feed for that account only 14 + 4. fetches data from public bsky app view API
+72
cmd/athome/main.go
··· 1 + package main 2 + 3 + import ( 4 + "os" 5 + 6 + _ "github.com/joho/godotenv/autoload" 7 + 8 + logging "github.com/ipfs/go-log" 9 + "github.com/urfave/cli/v2" 10 + ) 11 + 12 + var log = logging.Logger("pubweb") 13 + 14 + func init() { 15 + logging.SetAllLoggers(logging.LevelDebug) 16 + //logging.SetAllLoggers(logging.LevelWarn) 17 + } 18 + 19 + func main() { 20 + run(os.Args) 21 + } 22 + 23 + func run(args []string) { 24 + 25 + app := cli.App{ 26 + Name: "pubweb", 27 + Usage: "public web interface to bluesky content", 28 + } 29 + 30 + app.Commands = []*cli.Command{ 31 + &cli.Command{ 32 + Name: "serve", 33 + Usage: "run the server", 34 + Action: serve, 35 + Flags: []cli.Flag{ 36 + &cli.StringFlag{ 37 + Name: "pds-host", 38 + Usage: "method, hostname, and port of PDS instance", 39 + Value: "http://localhost:4849", 40 + EnvVars: []string{"ATP_PDS_HOST"}, 41 + }, 42 + &cli.StringFlag{ 43 + Name: "handle", 44 + Usage: "for PDS login", 45 + Required: true, 46 + EnvVars: []string{"ATP_AUTH_HANDLE"}, 47 + }, 48 + &cli.StringFlag{ 49 + Name: "password", 50 + Usage: "for PDS login", 51 + Required: true, 52 + EnvVars: []string{"ATP_AUTH_PASSWORD"}, 53 + }, 54 + &cli.StringFlag{ 55 + Name: "http-address", 56 + Usage: "Specify the local IP/port to bind to", 57 + Required: false, 58 + Value: ":8200", 59 + EnvVars: []string{"PUBWEB_BIND"}, 60 + }, 61 + &cli.BoolFlag{ 62 + Name: "debug", 63 + Usage: "Enable debug mode", 64 + Value: false, 65 + Required: false, 66 + EnvVars: []string{"DEBUG"}, 67 + }, 68 + }, 69 + }, 70 + } 71 + app.RunAndExitOnError() 72 + }
+85
cmd/athome/renderer.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "embed" 6 + "errors" 7 + "fmt" 8 + "io" 9 + "path/filepath" 10 + 11 + "github.com/flosch/pongo2/v6" 12 + "github.com/labstack/echo/v4" 13 + ) 14 + 15 + //go:embed templates/* 16 + var TemplateFS embed.FS 17 + 18 + type RendererLoader struct { 19 + prefix string 20 + fs *embed.FS 21 + } 22 + 23 + func NewRendererLoader(prefix string, fs *embed.FS) pongo2.TemplateLoader { 24 + return &RendererLoader{ 25 + prefix: prefix, 26 + fs: fs, 27 + } 28 + } 29 + func (l *RendererLoader) Abs(_, name string) string { 30 + // TODO: remove this workaround 31 + // Figure out why this method is being called 32 + // twice on template names resulting in a failure to resolve 33 + // the template name. 34 + if filepath.HasPrefix(name, l.prefix) { 35 + return name 36 + } 37 + return filepath.Join(l.prefix, name) 38 + } 39 + 40 + func (l *RendererLoader) Get(path string) (io.Reader, error) { 41 + b, err := l.fs.ReadFile(path) 42 + if err != nil { 43 + return nil, fmt.Errorf("reading template %q failed: %w", path, err) 44 + } 45 + return bytes.NewReader(b), nil 46 + } 47 + 48 + type Renderer struct { 49 + TemplateSet *pongo2.TemplateSet 50 + Debug bool 51 + } 52 + 53 + func NewRenderer(prefix string, fs *embed.FS, debug bool) *Renderer { 54 + return &Renderer{ 55 + TemplateSet: pongo2.NewSet(prefix, NewRendererLoader(prefix, fs)), 56 + Debug: debug, 57 + } 58 + } 59 + 60 + func (r Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 61 + var ctx pongo2.Context 62 + 63 + if data != nil { 64 + var ok bool 65 + ctx, ok = data.(pongo2.Context) 66 + if !ok { 67 + return errors.New("no pongo2.Context data was passed") 68 + } 69 + } 70 + 71 + var t *pongo2.Template 72 + var err error 73 + 74 + if r.Debug { 75 + t, err = pongo2.FromFile(name) 76 + } else { 77 + t, err = r.TemplateSet.FromFile(name) 78 + } 79 + 80 + if err != nil { 81 + return err 82 + } 83 + 84 + return t.ExecuteWriter(ctx, w) 85 + }
+218
cmd/athome/server.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "io/fs" 7 + "net/http" 8 + "os" 9 + "strings" 10 + "embed" 11 + 12 + comatproto "github.com/bluesky-social/indigo/api/atproto" 13 + appbsky "github.com/bluesky-social/indigo/api/bsky" 14 + cliutil "github.com/bluesky-social/indigo/cmd/gosky/util" 15 + "github.com/bluesky-social/indigo/xrpc" 16 + 17 + "github.com/flosch/pongo2/v6" 18 + "github.com/labstack/echo/v4" 19 + "github.com/labstack/echo/v4/middleware" 20 + "github.com/urfave/cli/v2" 21 + ) 22 + 23 + //go:embed static/* 24 + var StaticFS embed.FS 25 + 26 + type Server struct { 27 + xrpcc *xrpc.Client 28 + } 29 + 30 + func serve(cctx *cli.Context) error { 31 + debug := cctx.Bool("debug") 32 + httpAddress := cctx.String("http-address") 33 + pdsHost := cctx.String("pds-host") 34 + atpHandle := cctx.String("handle") 35 + atpPassword := cctx.String("password") 36 + 37 + // create a new session 38 + // TODO: does this work with no auth at all? 39 + xrpcc := &xrpc.Client{ 40 + Client: cliutil.NewHttpClient(), 41 + Host: pdsHost, 42 + Auth: &xrpc.AuthInfo{ 43 + Handle: atpHandle, 44 + }, 45 + } 46 + 47 + auth, err := comatproto.ServerCreateSession(context.TODO(), xrpcc, &comatproto.ServerCreateSession_Input{ 48 + Identifier: xrpcc.Auth.Handle, 49 + Password: atpPassword, 50 + }) 51 + if err != nil { 52 + return err 53 + } 54 + xrpcc.Auth.AccessJwt = auth.AccessJwt 55 + xrpcc.Auth.RefreshJwt = auth.RefreshJwt 56 + xrpcc.Auth.Did = auth.Did 57 + xrpcc.Auth.Handle = auth.Handle 58 + 59 + server := Server{xrpcc} 60 + 61 + staticHandler := http.FileServer(func() http.FileSystem { 62 + if debug { 63 + return http.FS(os.DirFS("static")) 64 + } 65 + fsys, err := fs.Sub(StaticFS, "static") 66 + if err != nil { 67 + log.Fatal(err) 68 + } 69 + return http.FS(fsys) 70 + }()) 71 + 72 + e := echo.New() 73 + e.HideBanner = true 74 + // SECURITY: Do not modify without due consideration. 75 + e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ 76 + ContentTypeNosniff: "nosniff", 77 + XFrameOptions: "SAMEORIGIN", 78 + HSTSMaxAge: 31536000, // 365 days 79 + // TODO: 80 + // ContentSecurityPolicy 81 + // XSSProtection 82 + })) 83 + e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ 84 + // Don't log requests for static content. 85 + Skipper: func(c echo.Context) bool { 86 + return strings.HasPrefix(c.Request().URL.Path, "/static") 87 + }, 88 + Format: "method=${method} path=${uri} status=${status} latency=${latency_human}\n", 89 + })) 90 + e.Renderer = NewRenderer("templates/", &TemplateFS, debug) 91 + e.HTTPErrorHandler = customHTTPErrorHandler 92 + 93 + // redirect trailing slash to non-trailing slash. 94 + // all of our current endpoints have no trailing slash. 95 + e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ 96 + RedirectCode: http.StatusFound, 97 + })) 98 + 99 + // basic static routes 100 + e.GET("/robots.txt", echo.WrapHandler(staticHandler)) 101 + e.GET("/favicon.ico", echo.WrapHandler(staticHandler)) 102 + e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) 103 + e.GET("/", server.WebHome) 104 + 105 + // generic routes 106 + //e.GET("/search", server.WebGeneric) 107 + //e.GET("/support", server.WebGeneric) 108 + //e.GET("/support/privacy", server.WebGeneric) 109 + //e.GET("/support/tos", server.WebGeneric) 110 + //e.GET("/support/community-guidelines", server.WebGeneric) 111 + //e.GET("/support/copyright", server.WebGeneric) 112 + 113 + // profile endpoints; only first populates info 114 + e.GET("/profile/:handle", server.WebProfile) 115 + //e.GET("/profile/:handle/repo.car.gz", server.WebProfile) 116 + //e.GET("/profile/:handle/follows", server.WebGeneric) 117 + //e.GET("/profile/:handle/followers", server.WebGeneric) 118 + 119 + // post endpoints; only first populates info 120 + e.GET("/profile/:handle/post/:rkey", server.WebPost) 121 + //e.GET("/profile/:handle/post/:rkey/liked-by", server.WebGeneric) 122 + //e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric) 123 + 124 + // feeds 125 + e.GET("/feed/:name", server.WebFeed) 126 + 127 + // redirect 128 + //e.GET("/at://:account", server.WebAccountURI) 129 + //e.GET("/at://:account/:nsid/:rkey", server.WebRecordURI) 130 + 131 + log.Infow("starting server", "bind", httpAddress) 132 + return e.Start(httpAddress) 133 + } 134 + 135 + func customHTTPErrorHandler(err error, c echo.Context) { 136 + code := http.StatusInternalServerError 137 + if he, ok := err.(*echo.HTTPError); ok { 138 + code = he.Code 139 + } 140 + c.Logger().Error(err) 141 + data := pongo2.Context{ 142 + "statusCode": code, 143 + } 144 + c.Render(code, "error.html", data) 145 + } 146 + 147 + func (srv *Server) WebHome(c echo.Context) error { 148 + data := pongo2.Context{} 149 + return c.Render(http.StatusOK, "home.html", data) 150 + } 151 + 152 + func (srv *Server) WebPost(c echo.Context) error { 153 + data := pongo2.Context{} 154 + handle := c.Param("handle") 155 + rkey := c.Param("rkey") 156 + // sanity check argument 157 + if len(handle) > 4 && len(handle) < 128 && len(rkey) > 0 { 158 + ctx := c.Request().Context() 159 + // requires two fetches: first fetch profile (!) 160 + pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle) 161 + if err != nil { 162 + log.Warnf("failed to fetch handle: %s\t%v", handle, err) 163 + // TODO: only if "not found" 164 + return echo.NewHTTPError(404, "handle not found: %s", handle) 165 + } else { 166 + did := pv.Did 167 + data["did"] = did 168 + 169 + // then fetch the post thread (with extra context) 170 + uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey) 171 + tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, uri) 172 + if err != nil { 173 + log.Warnf("failed to fetch post: %s\t%v", uri, err) 174 + // TODO: only if "not found" 175 + return echo.NewHTTPError(404, "post not found: %s", handle) 176 + } else { 177 + req := c.Request() 178 + postView := tpv.Thread.FeedDefs_ThreadViewPost.Post 179 + data["postView"] = postView 180 + data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) 181 + if postView.Embed != nil && postView.Embed.EmbedImages_View != nil { 182 + data["imgThumbUrl"] = postView.Embed.EmbedImages_View.Images[0].Thumb 183 + } 184 + } 185 + } 186 + 187 + } 188 + return c.Render(http.StatusOK, "post.html", data) 189 + } 190 + 191 + func (srv *Server) WebProfile(c echo.Context) error { 192 + data := pongo2.Context{} 193 + handle := c.Param("handle") 194 + // sanity check argument 195 + if len(handle) > 4 && len(handle) < 128 { 196 + ctx := c.Request().Context() 197 + pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle) 198 + if err != nil { 199 + log.Warnw("failed to fetch handle", "handle", handle, "err", err) 200 + // TODO: only if "not found" 201 + return echo.NewHTTPError(404, "handle not found: %s", handle) 202 + } else { 203 + req := c.Request() 204 + data["profileView"] = pv 205 + data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) 206 + } 207 + af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, handle, "", 100) 208 + if err != nil { 209 + log.Warnw("failed to fetch author feed", "handle", handle, "err", err) 210 + // TODO: show some error? 211 + } else { 212 + data["authorFeed"] = af.Feed 213 + //log.Warnw("author feed", "feed", af.Feed) 214 + } 215 + } 216 + 217 + return c.Render(http.StatusOK, "profile.html", data) 218 + }
cmd/athome/static/apple-touch-icon.png

This is a binary file and will not be displayed.

cmd/athome/static/default-avatar.png

This is a binary file and will not be displayed.

cmd/athome/static/favicon-16x16.png

This is a binary file and will not be displayed.

cmd/athome/static/favicon-32x32.png

This is a binary file and will not be displayed.

cmd/athome/static/favicon.ico

This is a binary file and will not be displayed.

cmd/athome/static/favicon.png

This is a binary file and will not be displayed.

+9
cmd/athome/static/robots.txt
··· 1 + # Hello Friends! 2 + # If you are considering bulk or automated crawling, you may want to look in 3 + # to our protocol (API), including a firehose of updates. See: https://atproto.com/ 4 + 5 + # By default, may crawl anything on this domain. HTTP 429 ("backoff") status 6 + # codes are used for rate-limiting. Up to a handful concurrent requests should 7 + # be ok. 8 + User-Agent: * 9 + Allow: /
+55
cmd/athome/templates/base.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta httpEquiv="X-UA-Compatible" content="IE=edge" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <meta name="referrer" content="strict-origin-when-cross-origin"> 8 + <title>{%- block head_title -%}Bluesky{%- endblock -%}</title> 9 + 10 + <!-- Hello Humans! API docs at https://atproto.com --> 11 + 12 + <link rel="stylesheet" 13 + type="text/css" 14 + href="https://cdn.jsdelivr.net/npm/semantic-ui@2.5.0/dist/semantic.min.css" 15 + type="text/css" 16 + crossorigin="anonymous"> 17 + <!-- 18 + <link rel="preload" 19 + href="https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=latin&display=swap" 20 + as="style"> 21 + <link rel="preload" 22 + href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.6/dist/themes/default/assets/fonts/icons.woff2" 23 + as="font" 24 + type="font/woff2" 25 + crossorigin="anonymous"> 26 + --> 27 + <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"/> 28 + <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"/> 29 + <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png"/> 30 + {% block html_head_extra -%}{%- endblock %} 31 + <meta name="application-name" name="Bluesky"> 32 + <meta name="generator" name="pubweb"> 33 + </head> 34 + <body> 35 + {%- block body_all %} 36 + <main class="ui main container" style="min-height: calc(100vh);"> 37 + <div class="ui grid"> 38 + <div class="fixed four wide column"> 39 + <div class="ui vertical text menu" style="padding-top: 2em; font-size: 1.3rem;"> 40 + <h2 style="color: blue;">Bluesky</h2> 41 + <a href="/feed/whatshot" class="item">What's Hot</a> 42 + <a href="/profile/nori.gay/post/3juzlwllznd24" class="item">Hell Thread</a> 43 + <a href="/profile/bnewbold.bsky.team" class="item">Prod bnewbold</a> 44 + <a href="/profile/bnewbold.staging.bsky.dev" class="item">Staging bnewbold</a> 45 + <a href="/profile/paul.staging.bsky.dev" class="item">Staging paul</a> 46 + </div> 47 + </div> 48 + <div class="ten wide column"> 49 + {% block main_content %}blank page{% endblock %} 50 + </div> 51 + </div> 52 + </main> 53 + {% endblock -%} 54 + </body> 55 + </html>
+12
cmd/athome/templates/error.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block head_title %}Error {{ statusCode }} - Bluesky{% endblock %} 4 + 5 + {% block main_content %} 6 + <br> 7 + <center> 8 + <h1 style="font-size: 8em;">{{ statusCode }}</h1> 9 + <h2 style="font-size: 3em;">Error!</h2> 10 + <p>Sorry about that! Our <a href="https://bluesky.statuspage.io/">Status Page</a> might have more context. 11 + </center> 12 + {% endblock %}
+106
cmd/athome/templates/feed_macros.html
··· 1 + 2 + {% macro feed_post(feedItem) export %} 3 + <div class="event"> 4 + <div class="label"> 5 + {% if feedItem.Post.Author.Avatar %} 6 + <img src="{{ feedItem.Post.Author.Avatar }}"> 7 + {% else %} 8 + <img src="/static/default-avatar.png"> 9 + {% endif %} 10 + </div> 11 + <div class="content"> 12 + {% if feedItem.Reason %} 13 + {{ feedItem.Reason.FeedDefs_ReasonRepost }} 14 + {% endif %} 15 + <div class="summary"> 16 + <a href="/profile/{{ feedItem.Post.Author.Handle }}" class="user"> 17 + {% if feedItem.Post.Author.DisplayName %} 18 + <b>{{ feedItem.Post.Author.DisplayName }}</b> 19 + {% endif %} 20 + @{{ feedItem.Post.Author.Handle }} 21 + </a> 22 + 23 + 24 + <div class="date"> 25 + {# TODO: relative time#} 26 + {# TODO: parse and fix link (custom filter?) #} 27 + <a href="/profile/{{ feedItem.Author.Handle }}/post/{{ feedItem.Post.Uri }}">{{ feedItem.Post.IndexedAt }}</a> 28 + </div> 29 + </div> 30 + <div class="extra text"> 31 + {{ feedItem.Post.Record.Val.Text }} 32 + {% if feedItem.Post.Embed and feedItem.Post.Embed.EmbedImages_View %} 33 + <div class="ui four cards"> 34 + {% for image in feedItem.Post.Embed.EmbedImages_View.Images %} 35 + <div class="card"> 36 + <div class="image"> 37 + <a href="{{ image.Fullsize }}"> 38 + <img alt="{{ image.Alt }}" src="{{ image.Thumb }}" style="width: 100%;"> 39 + </a> 40 + </div> 41 + </div> 42 + {% endfor %} 43 + </div> 44 + {% endif %} 45 + </div> 46 + <div class="meta"> 47 + <a class="like"><i class="reply icon"></i> {{ feedItem.Post.ReplyCount }}</a> 48 + <a class="like"><i class="comment outline icon"></i> {{ feedItem.Post.RepostCount }}</a> 49 + <a class="like"><i class="like outline icon"></i> {{ feedItem.Post.LikeCount }}</a> 50 + </div> 51 + </div> 52 + </div> 53 + {% endmacro %} 54 + 55 + {% macro primary_post(feedItem) export %} 56 + <div class="event"> 57 + <div class="label"> 58 + {% if feedItem.Post.Author.Avatar %} 59 + <img src="{{ feedItem.Post.Author.Avatar }}"> 60 + {% else %} 61 + <img src="/static/default-avatar.png"> 62 + {% endif %} 63 + </div> 64 + <div class="content"> 65 + {% if feedItem.Reason %} 66 + {{ feedItem.Reason.FeedDefs_ReasonRepost }} 67 + {% endif %} 68 + <div class="summary"> 69 + <a href="/profile/{{ feedItem.Post.Author.Handle }}" class="user"> 70 + {% if feedItem.Post.Author.DisplayName %} 71 + <b>{{ feedItem.Post.Author.DisplayName }}</b> 72 + {% endif %} 73 + @{{ feedItem.Post.Author.Handle }} 74 + </a> 75 + 76 + 77 + <div class="date"> 78 + {# TODO: relative time#} 79 + {# TODO: parse and fix link (custom filter?) #} 80 + <a href="/profile/{{ feedItem.Author.Handle }}/post/{{ feedItem.Post.Uri }}">{{ feedItem.Post.IndexedAt }}</a> 81 + </div> 82 + </div> 83 + <div class="extra text"> 84 + {{ feedItem.Post.Record.Val.Text }} 85 + {% if feedItem.Post.Embed and feedItem.Post.Embed.EmbedImages_View %} 86 + <div class="ui four cards"> 87 + {% for image in feedItem.Post.Embed.EmbedImages_View.Images %} 88 + <div class="card"> 89 + <div class="image"> 90 + <a href="{{ image.Fullsize }}"> 91 + <img alt="{{ image.Alt }}" src="{{ image.Thumb }}" style="width: 100%;"> 92 + </a> 93 + </div> 94 + </div> 95 + {% endfor %} 96 + </div> 97 + {% endif %} 98 + </div> 99 + <div class="meta"> 100 + <a class="like"><i class="reply icon"></i> {{ feedItem.Post.ReplyCount }}</a> 101 + <a class="like"><i class="comment outline icon"></i> {{ feedItem.Post.RepostCount }}</a> 102 + <a class="like"><i class="like outline icon"></i> {{ feedItem.Post.LikeCount }}</a> 103 + </div> 104 + </div> 105 + </div> 106 + {% endmacro %}
+20
cmd/athome/templates/home.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block head_title %}Bluesky{% endblock %} 4 + 5 + {% block html_head_extra -%} 6 + <meta name="description" content="See what's next."/> 7 + <meta property="og:type" content="website"/> 8 + <meta property="og:title" content="Bluesky Social"/> 9 + <meta property="og:description" content="See what's next."/> 10 + <meta property="og:image" content="/static/social-card-default.png"/> 11 + <meta name="twitter:card" content="summary"/> 12 + <meta name="twitter:site" content="@bluesky"/> 13 + {%- endblock %} 14 + 15 + {% block main_content %} 16 + <center style="padding-top: 8em;"> 17 + <h1 style="font-size: 3em; color: blue;">See what’s next</h1> 18 + <h1 style="font-size: 3em; color: black;">Bluesky Social</h1> 19 + </center> 20 + {% endblock %}
+46
cmd/athome/templates/post.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block head_title %} 4 + {%- if postView -%} 5 + @{{ postView.Author.Handle }} on Bluesky 6 + {%- else -%} 7 + Bluesky 8 + {%- endif -%} 9 + {% endblock %} 10 + 11 + {% block html_head_extra -%} 12 + {%- if postView -%} 13 + <meta property="og:type" content="website"> 14 + <meta property="og:site_name" content="Bluesky Social"> 15 + {%- if requestURI %} 16 + <meta property="og:url" content="{{ requestURI }}"> 17 + {% endif -%} 18 + {%- if postView.Author.DisplayName %} 19 + <meta property="og:title" content="{{ postView.Author.DisplayName }} (@{{ postView.Author.Handle }})"> 20 + {% else %} 21 + <meta property="og:title" content="@{{ postView.Author.Handle }}"> 22 + {% endif -%} 23 + {%- if postView.Record.Val.Text %} 24 + <meta name="description" content="{{ postView.Record.Val.Text }}"> 25 + <meta property="og:description" content="{{ postView.Record.Val.Text }}"> 26 + {% endif -%} 27 + {%- if imgThumbUrl %} 28 + <meta property="og:image" content="{{ imgThumbUrl }}"> 29 + <meta name="twitter:card" content="summary_large_image"> 30 + {%- elif postView.Author.Avatar %} 31 + {# Don't use avatar image in cards; usually looks bad #} 32 + <meta name="twitter:card" content="summary"> 33 + {% endif %} 34 + <meta name="twitter:label1" content="Posted At"> 35 + <meta name="twitter:value1" content="{{ postView.CreatedAt }}"> 36 + <meta name="twitter:site" content="@bluesky"> 37 + {% endif -%} 38 + {%- endblock %} 39 + 40 + {% block main_content %} 41 + <h3>Post</h3> 42 + <p id="bsky_display_name">{{ postView.Author.DisplayName }}</p> 43 + <p id="bsky_handle">{{ postView.Author.Handle }}</p> 44 + <p id="bsky_did">{{ postView.Author.Did }}</p> 45 + <p id="bsky_post_text">{{ postView.Record.Val.Text }}</p> 46 + {%- endblock %}
+67
cmd/athome/templates/profile.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block head_title %} 4 + {%- if profileView -%} 5 + @{{ profileView.Handle }} on Bluesky 6 + {%- else -%} 7 + Bluesky 8 + {%- endif -%} 9 + {% endblock %} 10 + 11 + {% block html_head_extra -%} 12 + {%- if profileView -%} 13 + <meta property="og:type" content="website"> 14 + <meta property="og:site_name" content="Bluesky Social"> 15 + {%- if requestURI %} 16 + <meta property="og:url" content="{{ requestURI }}"> 17 + {% endif -%} 18 + {%- if profileView.DisplayName %} 19 + <meta property="og:title" content="{{ profileView.DisplayName }} (@{{ profileView.Handle }})"> 20 + {% else %} 21 + <meta property="og:title" content="{{ profileView.Handle }}"> 22 + {% endif -%} 23 + {%- if profileView.Description %} 24 + <meta name="description" content="{{ profileView.Description }}"> 25 + <meta property="og:description" content="{{ profileView.Description }}"> 26 + {% endif -%} 27 + {%- if profileView.Banner %} 28 + <meta property="og:image" content="{{ profileView.Banner }}"> 29 + <meta name="twitter:card" content="summary_large_image"> 30 + {%- elif profileView.Avatar -%} 31 + {# Don't use avatar image in cards; usually looks bad #} 32 + <meta name="twitter:card" content="summary"> 33 + {% endif %} 34 + <meta name="twitter:label1" content="Account DID"> 35 + <meta name="twitter:value1" content="{{ profileView.Did }}"> 36 + <meta name="twitter:site" content="@bluesky"> 37 + {% endif -%} 38 + {%- endblock %} 39 + 40 + {% block main_content %} 41 + {% import "feed_macros.html" feed_post %} 42 + {% if profileView.Banner %} 43 + <img src="{{ profileView.Banner }}" style="width: 100%;"> 44 + <br> 45 + {% endif %} 46 + {% if profileView.DisplayName %} 47 + <h2>{{ profileView.DisplayName }}</h2> 48 + {% else %} 49 + <h2>{{ profileView.Handle}}</h2> 50 + {% endif %} 51 + <h3>@{{ profileView.Handle }}</h3> 52 + <p><code>{{ profileView.Did }}</code></p> 53 + <p> 54 + {{ profileView.FollowersCount }} followers | 55 + {{ profileView.FollowsCount }} following | 56 + {{ profileView.PostsCount }} posts 57 + </p> 58 + <p>{{ profileView.Description }}</p> 59 + 60 + <div class="ui divider"></div> 61 + <div class="ui feed"> 62 + {% for feedItem in authorFeed %} 63 + {{ feed_post(feedItem) }} 64 + <div class="ui divider"></div> 65 + {% endfor %} 66 + </div> 67 + {%- endblock %}