···18// RECORDTYPE: ActorProfile
19type ActorProfile struct {
20 LexiconTypeID string `json:"$type,const=sh.tangled.actor.profile" cborgen:"$type,const=sh.tangled.actor.profile"`
0021 // bluesky: Include link to this account on Bluesky.
22 Bluesky bool `json:"bluesky" cborgen:"bluesky"`
23 // description: Free-form profile description text.
···18// RECORDTYPE: ActorProfile
19type ActorProfile struct {
20 LexiconTypeID string `json:"$type,const=sh.tangled.actor.profile" cborgen:"$type,const=sh.tangled.actor.profile"`
21+ // avatar: Small image to be displayed next to posts from account. AKA, 'profile picture'
22+ Avatar *util.LexBlob `json:"avatar,omitempty" cborgen:"avatar,omitempty"`
23 // bluesky: Include link to this account on Bluesky.
24 Bluesky bool `json:"bluesky" cborgen:"bluesky"`
25 // description: Free-form profile description text.
···7 "time"
89 "tangled.org/core/appview/models"
010)
1112-func GetPipelines(e Execer, filters ...filter) ([]models.Pipeline, error) {
13 var pipelines []models.Pipeline
1415 var conditions []string
···168169// this is a mega query, but the most useful one:
170// get N pipelines, for each one get the latest status of its N workflows
171-func GetPipelineStatuses(e Execer, limit int, filters ...filter) ([]models.Pipeline, error) {
172 var conditions []string
173 var args []any
174 for _, filter := range filters {
175- filter.key = "p." + filter.key // the table is aliased in the query to `p`
176 conditions = append(conditions, filter.Condition())
177 args = append(args, filter.Arg()...)
178 }
···264 conditions = nil
265 args = nil
266 for _, p := range pipelines {
267- knotFilter := FilterEq("pipeline_knot", p.Knot)
268- rkeyFilter := FilterEq("pipeline_rkey", p.Rkey)
269 conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition()))
270 args = append(args, p.Knot)
271 args = append(args, p.Rkey)
···7 "time"
89 "tangled.org/core/appview/models"
10+ "tangled.org/core/orm"
11)
1213+func GetPipelines(e Execer, filters ...orm.Filter) ([]models.Pipeline, error) {
14 var pipelines []models.Pipeline
1516 var conditions []string
···169170// this is a mega query, but the most useful one:
171// get N pipelines, for each one get the latest status of its N workflows
172+func GetPipelineStatuses(e Execer, limit int, filters ...orm.Filter) ([]models.Pipeline, error) {
173 var conditions []string
174 var args []any
175 for _, filter := range filters {
176+ filter.Key = "p." + filter.Key // the table is aliased in the query to `p`
177 conditions = append(conditions, filter.Condition())
178 args = append(args, filter.Arg()...)
179 }
···265 conditions = nil
266 args = nil
267 for _, p := range pipelines {
268+ knotFilter := orm.FilterEq("pipeline_knot", p.Knot)
269+ rkeyFilter := orm.FilterEq("pipeline_rkey", p.Rkey)
270 conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition()))
271 args = append(args, p.Knot)
272 args = append(args, p.Rkey)
+16-8
appview/db/profile.go
···1112 "github.com/bluesky-social/indigo/atproto/syntax"
13 "tangled.org/core/appview/models"
014)
1516const TimeframeMonths = 7
···4445 issues, err := GetIssues(
46 e,
47- FilterEq("did", forDid),
48- FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
49 )
50 if err != nil {
51 return nil, fmt.Errorf("error getting issues by owner did: %w", err)
···65 *items = append(*items, &issue)
66 }
6768- repos, err := GetRepos(e, 0, FilterEq("did", forDid))
69 if err != nil {
70 return nil, fmt.Errorf("error getting all repos by did: %w", err)
71 }
···127 _, err = tx.Exec(
128 `insert or replace into profile (
129 did,
0130 description,
131 include_bluesky,
132 location,
133 pronouns
134 )
135- values (?, ?, ?, ?, ?)`,
136 profile.Did,
0137 profile.Description,
138 includeBskyValue,
139 profile.Location,
···199 return tx.Commit()
200}
201202-func GetProfiles(e Execer, filters ...filter) (map[string]*models.Profile, error) {
203 var conditions []string
204 var args []any
205 for _, filter := range filters {
···311func GetProfile(e Execer, did string) (*models.Profile, error) {
312 var profile models.Profile
313 var pronouns sql.Null[string]
0314315 profile.Did = did
316317 includeBluesky := 0
318319 err := e.QueryRow(
320- `select description, include_bluesky, location, pronouns from profile where did = ?`,
321 did,
322- ).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns)
323 if err == sql.ErrNoRows {
324 profile := models.Profile{}
325 profile.Did = did
···338 profile.Pronouns = pronouns.V
339 }
3400000341 rows, err := e.Query(`select link from profile_links where did = ?`, did)
342 if err != nil {
343 return nil, err
···441 }
442443 // ensure all pinned repos are either own repos or collaborating repos
444- repos, err := GetRepos(e, 0, FilterEq("did", profile.Did))
445 if err != nil {
446 log.Printf("getting repos for %s: %s", profile.Did, err)
447 }
···1112 "github.com/bluesky-social/indigo/atproto/syntax"
13 "tangled.org/core/appview/models"
14+ "tangled.org/core/orm"
15)
1617const TimeframeMonths = 7
···4546 issues, err := GetIssues(
47 e,
48+ orm.FilterEq("did", forDid),
49+ orm.FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
50 )
51 if err != nil {
52 return nil, fmt.Errorf("error getting issues by owner did: %w", err)
···66 *items = append(*items, &issue)
67 }
6869+ repos, err := GetRepos(e, 0, orm.FilterEq("did", forDid))
70 if err != nil {
71 return nil, fmt.Errorf("error getting all repos by did: %w", err)
72 }
···128 _, err = tx.Exec(
129 `insert or replace into profile (
130 did,
131+ avatar,
132 description,
133 include_bluesky,
134 location,
135 pronouns
136 )
137+ values (?, ?, ?, ?, ?, ?)`,
138 profile.Did,
139+ profile.Avatar,
140 profile.Description,
141 includeBskyValue,
142 profile.Location,
···202 return tx.Commit()
203}
204205+func GetProfiles(e Execer, filters ...orm.Filter) (map[string]*models.Profile, error) {
206 var conditions []string
207 var args []any
208 for _, filter := range filters {
···314func GetProfile(e Execer, did string) (*models.Profile, error) {
315 var profile models.Profile
316 var pronouns sql.Null[string]
317+ var avatar sql.Null[string]
318319 profile.Did = did
320321 includeBluesky := 0
322323 err := e.QueryRow(
324+ `select avatar, description, include_bluesky, location, pronouns from profile where did = ?`,
325 did,
326+ ).Scan(&avatar, &profile.Description, &includeBluesky, &profile.Location, &pronouns)
327 if err == sql.ErrNoRows {
328 profile := models.Profile{}
329 profile.Did = did
···342 profile.Pronouns = pronouns.V
343 }
344345+ if avatar.Valid {
346+ profile.Avatar = avatar.V
347+ }
348+349 rows, err := e.Query(`select link from profile_links where did = ?`, did)
350 if err != nil {
351 return nil, err
···449 }
450451 // ensure all pinned repos are either own repos or collaborating repos
452+ repos, err := GetRepos(e, 0, orm.FilterEq("did", profile.Did))
453 if err != nil {
454 log.Printf("getting repos for %s: %s", profile.Did, err)
455 }
+21-20
appview/db/pulls.go
···1314 "github.com/bluesky-social/indigo/atproto/syntax"
15 "tangled.org/core/appview/models"
016)
1718func NewPull(tx *sql.Tx, pull *models.Pull) error {
···118 return pullId - 1, err
119}
120121-func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*models.Pull, error) {
122 pulls := make(map[syntax.ATURI]*models.Pull)
123124 var conditions []string
···229 for _, p := range pulls {
230 pullAts = append(pullAts, p.AtUri())
231 }
232- submissionsMap, err := GetPullSubmissions(e, FilterIn("pull_at", pullAts))
233 if err != nil {
234 return nil, fmt.Errorf("failed to get submissions: %w", err)
235 }
···241 }
242243 // collect allLabels for each issue
244- allLabels, err := GetLabels(e, FilterIn("subject", pullAts))
245 if err != nil {
246 return nil, fmt.Errorf("failed to query labels: %w", err)
247 }
···258 sourceAts = append(sourceAts, *p.PullSource.RepoAt)
259 }
260 }
261- sourceRepos, err := GetRepos(e, 0, FilterIn("at_uri", sourceAts))
262 if err != nil && !errors.Is(err, sql.ErrNoRows) {
263 return nil, fmt.Errorf("failed to get source repos: %w", err)
264 }
···274 }
275 }
276277- allReferences, err := GetReferencesAll(e, FilterIn("from_at", pullAts))
278 if err != nil {
279 return nil, fmt.Errorf("failed to query reference_links: %w", err)
280 }
···295 return orderedByPullId, nil
296}
297298-func GetPulls(e Execer, filters ...filter) ([]*models.Pull, error) {
299 return GetPullsWithLimit(e, 0, filters...)
300}
301302func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
303 var ids []int64
304305- var filters []filter
306- filters = append(filters, FilterEq("state", opts.State))
307 if opts.RepoAt != "" {
308- filters = append(filters, FilterEq("repo_at", opts.RepoAt))
309 }
310311 var conditions []string
···361}
362363func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
364- pulls, err := GetPullsWithLimit(e, 1, FilterEq("repo_at", repoAt), FilterEq("pull_id", pullId))
365 if err != nil {
366 return nil, err
367 }
···373}
374375// mapping from pull -> pull submissions
376-func GetPullSubmissions(e Execer, filters ...filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
377 var conditions []string
378 var args []any
379 for _, filter := range filters {
···448449 // Get comments for all submissions using GetPullComments
450 submissionIds := slices.Collect(maps.Keys(submissionMap))
451- comments, err := GetPullComments(e, FilterIn("submission_id", submissionIds))
452 if err != nil {
453 return nil, fmt.Errorf("failed to get pull comments: %w", err)
454 }
···474 return m, nil
475}
476477-func GetPullComments(e Execer, filters ...filter) ([]models.PullComment, error) {
478 var conditions []string
479 var args []any
480 for _, filter := range filters {
···542543 // collect references for each comments
544 commentAts := slices.Collect(maps.Keys(commentMap))
545- allReferencs, err := GetReferencesAll(e, FilterIn("from_at", commentAts))
546 if err != nil {
547 return nil, fmt.Errorf("failed to query reference_links: %w", err)
548 }
···708 return err
709}
710711-func SetPullParentChangeId(e Execer, parentChangeId string, filters ...filter) error {
712 var conditions []string
713 var args []any
714···732733// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
734// otherwise submissions are immutable
735-func UpdatePull(e Execer, newPatch, sourceRev string, filters ...filter) error {
736 var conditions []string
737 var args []any
738···790func GetStack(e Execer, stackId string) (models.Stack, error) {
791 unorderedPulls, err := GetPulls(
792 e,
793- FilterEq("stack_id", stackId),
794- FilterNotEq("state", models.PullDeleted),
795 )
796 if err != nil {
797 return nil, err
···835func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) {
836 pulls, err := GetPulls(
837 e,
838- FilterEq("stack_id", stackId),
839- FilterEq("state", models.PullDeleted),
840 )
841 if err != nil {
842 return nil, err
···1314 "github.com/bluesky-social/indigo/atproto/syntax"
15 "tangled.org/core/appview/models"
16+ "tangled.org/core/orm"
17)
1819func NewPull(tx *sql.Tx, pull *models.Pull) error {
···119 return pullId - 1, err
120}
121122+func GetPullsWithLimit(e Execer, limit int, filters ...orm.Filter) ([]*models.Pull, error) {
123 pulls := make(map[syntax.ATURI]*models.Pull)
124125 var conditions []string
···230 for _, p := range pulls {
231 pullAts = append(pullAts, p.AtUri())
232 }
233+ submissionsMap, err := GetPullSubmissions(e, orm.FilterIn("pull_at", pullAts))
234 if err != nil {
235 return nil, fmt.Errorf("failed to get submissions: %w", err)
236 }
···242 }
243244 // collect allLabels for each issue
245+ allLabels, err := GetLabels(e, orm.FilterIn("subject", pullAts))
246 if err != nil {
247 return nil, fmt.Errorf("failed to query labels: %w", err)
248 }
···259 sourceAts = append(sourceAts, *p.PullSource.RepoAt)
260 }
261 }
262+ sourceRepos, err := GetRepos(e, 0, orm.FilterIn("at_uri", sourceAts))
263 if err != nil && !errors.Is(err, sql.ErrNoRows) {
264 return nil, fmt.Errorf("failed to get source repos: %w", err)
265 }
···275 }
276 }
277278+ allReferences, err := GetReferencesAll(e, orm.FilterIn("from_at", pullAts))
279 if err != nil {
280 return nil, fmt.Errorf("failed to query reference_links: %w", err)
281 }
···296 return orderedByPullId, nil
297}
298299+func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) {
300 return GetPullsWithLimit(e, 0, filters...)
301}
302303func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
304 var ids []int64
305306+ var filters []orm.Filter
307+ filters = append(filters, orm.FilterEq("state", opts.State))
308 if opts.RepoAt != "" {
309+ filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt))
310 }
311312 var conditions []string
···362}
363364func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
365+ pulls, err := GetPullsWithLimit(e, 1, orm.FilterEq("repo_at", repoAt), orm.FilterEq("pull_id", pullId))
366 if err != nil {
367 return nil, err
368 }
···374}
375376// mapping from pull -> pull submissions
377+func GetPullSubmissions(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
378 var conditions []string
379 var args []any
380 for _, filter := range filters {
···449450 // Get comments for all submissions using GetPullComments
451 submissionIds := slices.Collect(maps.Keys(submissionMap))
452+ comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds))
453 if err != nil {
454 return nil, fmt.Errorf("failed to get pull comments: %w", err)
455 }
···475 return m, nil
476}
477478+func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) {
479 var conditions []string
480 var args []any
481 for _, filter := range filters {
···543544 // collect references for each comments
545 commentAts := slices.Collect(maps.Keys(commentMap))
546+ allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
547 if err != nil {
548 return nil, fmt.Errorf("failed to query reference_links: %w", err)
549 }
···709 return err
710}
711712+func SetPullParentChangeId(e Execer, parentChangeId string, filters ...orm.Filter) error {
713 var conditions []string
714 var args []any
715···733734// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
735// otherwise submissions are immutable
736+func UpdatePull(e Execer, newPatch, sourceRev string, filters ...orm.Filter) error {
737 var conditions []string
738 var args []any
739···791func GetStack(e Execer, stackId string) (models.Stack, error) {
792 unorderedPulls, err := GetPulls(
793 e,
794+ orm.FilterEq("stack_id", stackId),
795+ orm.FilterNotEq("state", models.PullDeleted),
796 )
797 if err != nil {
798 return nil, err
···836func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) {
837 pulls, err := GetPulls(
838 e,
839+ orm.FilterEq("stack_id", stackId),
840+ orm.FilterEq("state", models.PullDeleted),
841 )
842 if err != nil {
843 return nil, err
···62 hx-swap="none"
63 class="flex flex-col gap-2"
64>
65- <p class="uppercase p-0">ADD EMAIL</p>
0066 <p class="text-sm text-gray-500 dark:text-gray-400">Commits using this email will be associated with your profile.</p>
67 <input
68 type="email"
···91 <div id="settings-emails-error" class="text-red-500 dark:text-red-400"></div>
92 <div id="settings-emails-success" class="text-green-500 dark:text-green-400"></div>
93</form>
94-{{ end }}
···62 hx-swap="none"
63 class="flex flex-col gap-2"
64>
65+ <label for="email-address" class="uppercase p-0">
66+ add email
67+ </label>
68 <p class="text-sm text-gray-500 dark:text-gray-400">Commits using this email will be associated with your profile.</p>
69 <input
70 type="email"
···93 <div id="settings-emails-error" class="text-red-500 dark:text-red-400"></div>
94 <div id="settings-emails-success" class="text-green-500 dark:text-green-400"></div>
95</form>
96+{{ end }}
+4-2
appview/pages/templates/user/settings/keys.html
···21 <div class="col-span-1 md:col-span-2">
22 <h2 class="text-sm pb-2 uppercase font-bold">SSH Keys</h2>
23 <p class="text-gray-500 dark:text-gray-400">
24- SSH public keys added here will be broadcasted to knots that you are a member of,
25 allowing you to push to repositories there.
26 </p>
27 </div>
···63 hx-swap="none"
64 class="flex flex-col gap-2"
65>
66- <p class="uppercase p-0">ADD SSH KEY</p>
0067 <p class="text-sm text-gray-500 dark:text-gray-400">SSH keys allow you to push to repositories in knots you're a member of.</p>
68 <input
69 type="text"
···21 <div class="col-span-1 md:col-span-2">
22 <h2 class="text-sm pb-2 uppercase font-bold">SSH Keys</h2>
23 <p class="text-gray-500 dark:text-gray-400">
24+ SSH public keys added here will be broadcasted to knots that you are a member of,
25 allowing you to push to repositories there.
26 </p>
27 </div>
···63 hx-swap="none"
64 class="flex flex-col gap-2"
65>
66+ <label for="key-name" class="uppercase p-0">
67+ add ssh key
68+ </label>
69 <p class="text-sm text-gray-500 dark:text-gray-400">SSH keys allow you to push to repositories in knots you're a member of.</p>
70 <input
71 type="text"
···001export default {
2 async fetch(request, env) {
3 // Helper function to generate a color from a string
···14 return color;
15 };
1600000000000000000000000000000000000000000000000000000000000000000000000000000000000017 const url = new URL(request.url);
18 const { pathname, searchParams } = url;
1920 if (!pathname || pathname === "/") {
21- return new Response(`This is Tangled's avatar service. It fetches your pretty avatar from Bluesky and caches it on Cloudflare.
22-You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`);
0023 }
2425 const size = searchParams.get("size");
···68 }
6970 try {
71- const profileResponse = await fetch(
72- `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`,
73- );
74- const profile = await profileResponse.json();
75- const avatar = profile.avatar;
007677- let avatarUrl = profile.avatar;
00000000000000007879 if (!avatarUrl) {
80 // Generate a random color based on the actor string
00000081 const bgColor = stringToColor(actor);
82 const size = resizeToTiny ? 32 : 128;
83 const svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg"><rect width="${size}" height="${size}" fill="${bgColor}"/></svg>`;
···93 return response;
94 }
9596- // Resize if requested
97 let avatarResponse;
98 if (resizeToTiny) {
99 avatarResponse = await fetch(avatarUrl, {
···1+import { IdResolver } from "@atproto/identity";
2+3export default {
4 async fetch(request, env) {
5 // Helper function to generate a color from a string
···16 return color;
17 };
1819+ // Helper function to fetch Tangled profile from PDS
20+ const getTangledAvatarFromPDS = async (actor, resolver) => {
21+ try {
22+ // Resolve the identity
23+ const identity = await resolver.resolve(actor);
24+ if (!identity) {
25+ console.log({
26+ level: "debug",
27+ message: "failed to resolve identity",
28+ actor: actor,
29+ });
30+ return null;
31+ }
32+33+ const did = identity.did;
34+ const pdsEndpoint = identity.pdsUrl;
35+36+ if (!pdsEndpoint) {
37+ console.log({
38+ level: "debug",
39+ message: "no PDS endpoint found",
40+ actor: actor,
41+ did: did,
42+ });
43+ return null;
44+ }
45+46+ console.log({
47+ level: "debug",
48+ message: "fetching Tangled profile from PDS",
49+ actor: actor,
50+ did: did,
51+ pdsEndpoint: pdsEndpoint,
52+ });
53+54+ // Fetch the Tangled profile record from PDS
55+ const profileResponse = await fetch(
56+ `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=org.tangled.actor.profile&rkey=self`,
57+ );
58+59+ if (!profileResponse.ok) {
60+ console.log({
61+ level: "debug",
62+ message: "no Tangled profile found on PDS",
63+ actor: actor,
64+ status: profileResponse.status,
65+ });
66+ return null;
67+ }
68+69+ const profileData = await profileResponse.json();
70+ const avatarCID = profileData?.value?.avatar;
71+72+ if (!avatarCID) {
73+ console.log({
74+ level: "debug",
75+ message: "Tangled profile has no avatar CID",
76+ actor: actor,
77+ });
78+ return null;
79+ }
80+81+ // Construct blob URL
82+ const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${avatarCID}`;
83+84+ console.log({
85+ level: "debug",
86+ message: "found Tangled avatar",
87+ actor: actor,
88+ avatarCID: avatarCID,
89+ });
90+91+ return blobUrl;
92+ } catch (e) {
93+ console.log({
94+ level: "warn",
95+ message: "error fetching Tangled avatar from PDS",
96+ actor: actor,
97+ error: e.message,
98+ });
99+ return null;
100+ }
101+ };
102+103 const url = new URL(request.url);
104 const { pathname, searchParams } = url;
105106 if (!pathname || pathname === "/") {
107+ return new Response(
108+ `This is Tangled's avatar service. It fetches your pretty avatar from your PDS, Bluesky, or generates a placeholder.
109+You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`,
110+ );
111 }
112113 const size = searchParams.get("size");
···156 }
157158 try {
159+ let avatarUrl = null;
160+161+ // Create identity resolver
162+ const resolver = new IdResolver();
163+164+ // Try to get Tangled avatar from user's PDS first
165+ avatarUrl = await getTangledAvatarFromPDS(actor, resolver);
166167+ // If no Tangled avatar, fall back to Bluesky
168+ if (!avatarUrl) {
169+ console.log({
170+ level: "debug",
171+ message: "no Tangled avatar, falling back to Bluesky",
172+ actor: actor,
173+ });
174+175+ const profileResponse = await fetch(
176+ `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`,
177+ );
178+179+ if (profileResponse.ok) {
180+ const profile = await profileResponse.json();
181+ avatarUrl = profile.avatar;
182+ }
183+ }
184185 if (!avatarUrl) {
186 // Generate a random color based on the actor string
187+ console.log({
188+ level: "debug",
189+ message: "no avatar found, generating placeholder",
190+ actor: actor,
191+ });
192+193 const bgColor = stringToColor(actor);
194 const size = resizeToTiny ? 32 : 128;
195 const svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg"><rect width="${size}" height="${size}" fill="${bgColor}"/></svg>`;
···205 return response;
206 }
207208+ // Fetch and optionally resize the avatar
209 let avatarResponse;
210 if (resizeToTiny) {
211 avatarResponse = await fetch(avatarUrl, {