diff --git a/.woodpecker/.backend.yml b/.woodpecker/.backend.yml
new file mode 100644
index 0000000..07b7898
--- /dev/null
+++ b/.woodpecker/.backend.yml
@@ -0,0 +1,13 @@
+when:
+ branch:
+ exclude: stable
+
+steps:
+ check:
+ image: golang:alpine
+ commands:
+ - apk update && apk add curl vips-dev build-base
+ - make backend
+ # Install golangci-lint
+ - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2
+ - golangci-lint run
diff --git a/.woodpecker/.frontend.yml b/.woodpecker/.frontend.yml
new file mode 100644
index 0000000..bedfa1f
--- /dev/null
+++ b/.woodpecker/.frontend.yml
@@ -0,0 +1,20 @@
+when:
+ branch:
+ exclude: stable
+
+steps:
+ check:
+ image: node
+ directory: frontend
+ environment: # SvelteKit expects these in the environment during build time.
+ - PRIVATE_SENTRY_DSN=
+ - PUBLIC_BASE_URL=http://pronouns.localhost
+ - PUBLIC_MEDIA_URL=http://pronouns.localhost/media
+ - PUBLIC_SHORT_BASE=http://prns.localhost
+ - PUBLIC_HCAPTCHA_SITEKEY=non_existent_sitekey
+ commands:
+ - corepack enable
+ - pnpm install
+ - pnpm check
+ - pnpm lint
+ - pnpm build
diff --git a/Makefile b/Makefile
index 1b15eff..8a7c089 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@ all: generate backend frontend
.PHONY: backend
backend:
- go build -v -o pronouns -ldflags="-buildid= -X codeberg.org/pronounscc/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD` -X codeberg.org/pronounscc/pronouns.cc/backend/server.Tag=`git describe --tags --long`" .
+ go build -v -o pronouns -ldflags="-buildid= -X codeberg.org/pronounscc/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD` -X codeberg.org/pronounscc/pronouns.cc/backend/server.Tag=`git describe --tags --long --always`" .
.PHONY: generate
generate:
diff --git a/backend/db/export.go b/backend/db/export.go
index 6141aac..5095445 100644
--- a/backend/db/export.go
+++ b/backend/db/export.go
@@ -79,7 +79,7 @@ func (db *DB) CreateExport(ctx context.Context, userID xid.ID, filename string,
return de, errors.Wrap(err, "building query")
}
- pgxscan.Get(ctx, db, &de, sql, args...)
+ err = pgxscan.Get(ctx, db, &de, sql, args...)
if err != nil {
return de, errors.Wrap(err, "executing sql")
}
diff --git a/backend/db/invites.go b/backend/db/invites.go
index d56662e..d13ab0e 100644
--- a/backend/db/invites.go
+++ b/backend/db/invites.go
@@ -6,6 +6,7 @@ import (
"encoding/base64"
"time"
+ "codeberg.org/pronounscc/pronouns.cc/backend/log"
"emperror.dev/errors"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
@@ -43,7 +44,12 @@ func (db *DB) CreateInvite(ctx context.Context, userID xid.ID) (i Invite, err er
if err != nil {
return i, errors.Wrap(err, "beginning transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
var maxInvites, inviteCount int
err = tx.QueryRow(ctx, "SELECT max_invites FROM users WHERE id = $1", userID).Scan(&maxInvites)
diff --git a/backend/db/member.go b/backend/db/member.go
index f301c89..a50c431 100644
--- a/backend/db/member.go
+++ b/backend/db/member.go
@@ -7,6 +7,7 @@ import (
"time"
"codeberg.org/pronounscc/pronouns.cc/backend/common"
+ "codeberg.org/pronounscc/pronouns.cc/backend/log"
"emperror.dev/errors"
"github.com/Masterminds/squirrel"
"github.com/georgysavva/scany/v2/pgxscan"
@@ -41,12 +42,14 @@ const (
)
// member names must match this regex
-var memberNameRegex = regexp.MustCompile("^[^@\\?!#/\\\\[\\]\"\\{\\}'$%&()+<=>^|~`,]{1,100}$")
+var memberNameRegex = regexp.MustCompile("^[^@\\?!#/\\\\[\\]\"\\{\\}'$%&()+<=>^|~`,\\*]{1,100}$")
// List of member names that cannot be used because they would break routing or be inaccessible due to page conflicts.
var invalidMemberNames = []string{
+ // these break routing outright
".",
"..",
+ // the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible
"edit",
}
@@ -285,7 +288,12 @@ func (db *DB) RerollMemberSID(ctx context.Context, userID, memberID xid.ID) (new
if err != nil {
return "", errors.Wrap(err, "beginning transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
sql, args, err := sq.Update("members").
Set("sid", squirrel.Expr("find_free_member_sid()")).
diff --git a/backend/icons/icons.go b/backend/icons/icons.go
index 3bc0e80..84355f3 100644
--- a/backend/icons/icons.go
+++ b/backend/icons/icons.go
@@ -1,1968 +1,1964 @@
// Generated code. DO NOT EDIT
package icons
-var icons = [...]string{
- "123",
- "alarm-fill",
- "alarm",
- "align-bottom",
- "align-center",
- "align-end",
- "align-middle",
- "align-start",
- "align-top",
- "alt",
- "app-indicator",
- "app",
- "archive-fill",
- "archive",
- "arrow-90deg-down",
- "arrow-90deg-left",
- "arrow-90deg-right",
- "arrow-90deg-up",
- "arrow-bar-down",
- "arrow-bar-left",
- "arrow-bar-right",
- "arrow-bar-up",
- "arrow-clockwise",
- "arrow-counterclockwise",
- "arrow-down-circle-fill",
- "arrow-down-circle",
- "arrow-down-left-circle-fill",
- "arrow-down-left-circle",
- "arrow-down-left-square-fill",
- "arrow-down-left-square",
- "arrow-down-left",
- "arrow-down-right-circle-fill",
- "arrow-down-right-circle",
- "arrow-down-right-square-fill",
- "arrow-down-right-square",
- "arrow-down-right",
- "arrow-down-short",
- "arrow-down-square-fill",
- "arrow-down-square",
- "arrow-down-up",
- "arrow-down",
- "arrow-left-circle-fill",
- "arrow-left-circle",
- "arrow-left-right",
- "arrow-left-short",
- "arrow-left-square-fill",
- "arrow-left-square",
- "arrow-left",
- "arrow-repeat",
- "arrow-return-left",
- "arrow-return-right",
- "arrow-right-circle-fill",
- "arrow-right-circle",
- "arrow-right-short",
- "arrow-right-square-fill",
- "arrow-right-square",
- "arrow-right",
- "arrow-up-circle-fill",
- "arrow-up-circle",
- "arrow-up-left-circle-fill",
- "arrow-up-left-circle",
- "arrow-up-left-square-fill",
- "arrow-up-left-square",
- "arrow-up-left",
- "arrow-up-right-circle-fill",
- "arrow-up-right-circle",
- "arrow-up-right-square-fill",
- "arrow-up-right-square",
- "arrow-up-right",
- "arrow-up-short",
- "arrow-up-square-fill",
- "arrow-up-square",
- "arrow-up",
- "arrows-angle-contract",
- "arrows-angle-expand",
- "arrows-collapse",
- "arrows-expand",
- "arrows-fullscreen",
- "arrows-move",
- "aspect-ratio-fill",
- "aspect-ratio",
- "asterisk",
- "at",
- "award-fill",
- "award",
- "back",
- "backspace-fill",
- "backspace-reverse-fill",
- "backspace-reverse",
- "backspace",
- "badge-3d-fill",
- "badge-3d",
- "badge-4k-fill",
- "badge-4k",
- "badge-8k-fill",
- "badge-8k",
- "badge-ad-fill",
- "badge-ad",
- "badge-ar-fill",
- "badge-ar",
- "badge-cc-fill",
- "badge-cc",
- "badge-hd-fill",
- "badge-hd",
- "badge-tm-fill",
- "badge-tm",
- "badge-vo-fill",
- "badge-vo",
- "badge-vr-fill",
- "badge-vr",
- "badge-wc-fill",
- "badge-wc",
- "bag-check-fill",
- "bag-check",
- "bag-dash-fill",
- "bag-dash",
- "bag-fill",
- "bag-plus-fill",
- "bag-plus",
- "bag-x-fill",
- "bag-x",
- "bag",
- "bar-chart-fill",
- "bar-chart-line-fill",
- "bar-chart-line",
- "bar-chart-steps",
- "bar-chart",
- "basket-fill",
- "basket",
- "basket2-fill",
- "basket2",
- "basket3-fill",
- "basket3",
- "battery-charging",
- "battery-full",
- "battery-half",
- "battery",
- "bell-fill",
- "bell",
- "bezier",
- "bezier2",
- "bicycle",
- "binoculars-fill",
- "binoculars",
- "blockquote-left",
- "blockquote-right",
- "book-fill",
- "book-half",
- "book",
- "bookmark-check-fill",
- "bookmark-check",
- "bookmark-dash-fill",
- "bookmark-dash",
- "bookmark-fill",
- "bookmark-heart-fill",
- "bookmark-heart",
- "bookmark-plus-fill",
- "bookmark-plus",
- "bookmark-star-fill",
- "bookmark-star",
- "bookmark-x-fill",
- "bookmark-x",
- "bookmark",
- "bookmarks-fill",
- "bookmarks",
- "bookshelf",
- "bootstrap-fill",
- "bootstrap-reboot",
- "bootstrap",
- "border-all",
- "border-bottom",
- "border-center",
- "border-inner",
- "border-left",
- "border-middle",
- "border-outer",
- "border-right",
- "border-style",
- "border-top",
- "border-width",
- "border",
- "bounding-box-circles",
- "bounding-box",
- "box-arrow-down-left",
- "box-arrow-down-right",
- "box-arrow-down",
- "box-arrow-in-down-left",
- "box-arrow-in-down-right",
- "box-arrow-in-down",
- "box-arrow-in-left",
- "box-arrow-in-right",
- "box-arrow-in-up-left",
- "box-arrow-in-up-right",
- "box-arrow-in-up",
- "box-arrow-left",
- "box-arrow-right",
- "box-arrow-up-left",
- "box-arrow-up-right",
- "box-arrow-up",
- "box-seam",
- "box",
- "braces",
- "bricks",
- "briefcase-fill",
- "briefcase",
- "brightness-alt-high-fill",
- "brightness-alt-high",
- "brightness-alt-low-fill",
- "brightness-alt-low",
- "brightness-high-fill",
- "brightness-high",
- "brightness-low-fill",
- "brightness-low",
- "broadcast-pin",
- "broadcast",
- "brush-fill",
- "brush",
- "bucket-fill",
- "bucket",
- "bug-fill",
- "bug",
- "building",
- "bullseye",
- "calculator-fill",
- "calculator",
- "calendar-check-fill",
- "calendar-check",
- "calendar-date-fill",
- "calendar-date",
- "calendar-day-fill",
- "calendar-day",
- "calendar-event-fill",
- "calendar-event",
- "calendar-fill",
- "calendar-minus-fill",
- "calendar-minus",
- "calendar-month-fill",
- "calendar-month",
- "calendar-plus-fill",
- "calendar-plus",
- "calendar-range-fill",
- "calendar-range",
- "calendar-week-fill",
- "calendar-week",
- "calendar-x-fill",
- "calendar-x",
- "calendar",
- "calendar2-check-fill",
- "calendar2-check",
- "calendar2-date-fill",
- "calendar2-date",
- "calendar2-day-fill",
- "calendar2-day",
- "calendar2-event-fill",
- "calendar2-event",
- "calendar2-fill",
- "calendar2-minus-fill",
- "calendar2-minus",
- "calendar2-month-fill",
- "calendar2-month",
- "calendar2-plus-fill",
- "calendar2-plus",
- "calendar2-range-fill",
- "calendar2-range",
- "calendar2-week-fill",
- "calendar2-week",
- "calendar2-x-fill",
- "calendar2-x",
- "calendar2",
- "calendar3-event-fill",
- "calendar3-event",
- "calendar3-fill",
- "calendar3-range-fill",
- "calendar3-range",
- "calendar3-week-fill",
- "calendar3-week",
- "calendar3",
- "calendar4-event",
- "calendar4-range",
- "calendar4-week",
- "calendar4",
- "camera-fill",
- "camera-reels-fill",
- "camera-reels",
- "camera-video-fill",
- "camera-video-off-fill",
- "camera-video-off",
- "camera-video",
- "camera",
- "camera2",
- "capslock-fill",
- "capslock",
- "card-checklist",
- "card-heading",
- "card-image",
- "card-list",
- "card-text",
- "caret-down-fill",
- "caret-down-square-fill",
- "caret-down-square",
- "caret-down",
- "caret-left-fill",
- "caret-left-square-fill",
- "caret-left-square",
- "caret-left",
- "caret-right-fill",
- "caret-right-square-fill",
- "caret-right-square",
- "caret-right",
- "caret-up-fill",
- "caret-up-square-fill",
- "caret-up-square",
- "caret-up",
- "cart-check-fill",
- "cart-check",
- "cart-dash-fill",
- "cart-dash",
- "cart-fill",
- "cart-plus-fill",
- "cart-plus",
- "cart-x-fill",
- "cart-x",
- "cart",
- "cart2",
- "cart3",
- "cart4",
- "cash-stack",
- "cash",
- "cast",
- "chat-dots-fill",
- "chat-dots",
- "chat-fill",
- "chat-left-dots-fill",
- "chat-left-dots",
- "chat-left-fill",
- "chat-left-quote-fill",
- "chat-left-quote",
- "chat-left-text-fill",
- "chat-left-text",
- "chat-left",
- "chat-quote-fill",
- "chat-quote",
- "chat-right-dots-fill",
- "chat-right-dots",
- "chat-right-fill",
- "chat-right-quote-fill",
- "chat-right-quote",
- "chat-right-text-fill",
- "chat-right-text",
- "chat-right",
- "chat-square-dots-fill",
- "chat-square-dots",
- "chat-square-fill",
- "chat-square-quote-fill",
- "chat-square-quote",
- "chat-square-text-fill",
- "chat-square-text",
- "chat-square",
- "chat-text-fill",
- "chat-text",
- "chat",
- "check-all",
- "check-circle-fill",
- "check-circle",
- "check-square-fill",
- "check-square",
- "check",
- "check2-all",
- "check2-circle",
- "check2-square",
- "check2",
- "chevron-bar-contract",
- "chevron-bar-down",
- "chevron-bar-expand",
- "chevron-bar-left",
- "chevron-bar-right",
- "chevron-bar-up",
- "chevron-compact-down",
- "chevron-compact-left",
- "chevron-compact-right",
- "chevron-compact-up",
- "chevron-contract",
- "chevron-double-down",
- "chevron-double-left",
- "chevron-double-right",
- "chevron-double-up",
- "chevron-down",
- "chevron-expand",
- "chevron-left",
- "chevron-right",
- "chevron-up",
- "circle-fill",
- "circle-half",
- "circle-square",
- "circle",
- "clipboard-check",
- "clipboard-data",
- "clipboard-minus",
- "clipboard-plus",
- "clipboard-x",
- "clipboard",
- "clock-fill",
- "clock-history",
- "clock",
- "cloud-arrow-down-fill",
- "cloud-arrow-down",
- "cloud-arrow-up-fill",
- "cloud-arrow-up",
- "cloud-check-fill",
- "cloud-check",
- "cloud-download-fill",
- "cloud-download",
- "cloud-drizzle-fill",
- "cloud-drizzle",
- "cloud-fill",
- "cloud-fog-fill",
- "cloud-fog",
- "cloud-fog2-fill",
- "cloud-fog2",
- "cloud-hail-fill",
- "cloud-hail",
- "cloud-haze-fill",
- "cloud-haze",
- "cloud-haze2-fill",
- "cloud-lightning-fill",
- "cloud-lightning-rain-fill",
- "cloud-lightning-rain",
- "cloud-lightning",
- "cloud-minus-fill",
- "cloud-minus",
- "cloud-moon-fill",
- "cloud-moon",
- "cloud-plus-fill",
- "cloud-plus",
- "cloud-rain-fill",
- "cloud-rain-heavy-fill",
- "cloud-rain-heavy",
- "cloud-rain",
- "cloud-slash-fill",
- "cloud-slash",
- "cloud-sleet-fill",
- "cloud-sleet",
- "cloud-snow-fill",
- "cloud-snow",
- "cloud-sun-fill",
- "cloud-sun",
- "cloud-upload-fill",
- "cloud-upload",
- "cloud",
- "clouds-fill",
- "clouds",
- "cloudy-fill",
- "cloudy",
- "code-slash",
- "code-square",
- "code",
- "collection-fill",
- "collection-play-fill",
- "collection-play",
- "collection",
- "columns-gap",
- "columns",
- "command",
- "compass-fill",
- "compass",
- "cone-striped",
- "cone",
- "controller",
- "cpu-fill",
- "cpu",
- "credit-card-2-back-fill",
- "credit-card-2-back",
- "credit-card-2-front-fill",
- "credit-card-2-front",
- "credit-card-fill",
- "credit-card",
- "crop",
- "cup-fill",
- "cup-straw",
- "cup",
- "cursor-fill",
- "cursor-text",
- "cursor",
- "dash-circle-dotted",
- "dash-circle-fill",
- "dash-circle",
- "dash-square-dotted",
- "dash-square-fill",
- "dash-square",
- "dash",
- "diagram-2-fill",
- "diagram-2",
- "diagram-3-fill",
- "diagram-3",
- "diamond-fill",
- "diamond-half",
- "diamond",
- "dice-1-fill",
- "dice-1",
- "dice-2-fill",
- "dice-2",
- "dice-3-fill",
- "dice-3",
- "dice-4-fill",
- "dice-4",
- "dice-5-fill",
- "dice-5",
- "dice-6-fill",
- "dice-6",
- "disc-fill",
- "disc",
- "discord",
- "display-fill",
- "display",
- "distribute-horizontal",
- "distribute-vertical",
- "door-closed-fill",
- "door-closed",
- "door-open-fill",
- "door-open",
- "dot",
- "download",
- "droplet-fill",
- "droplet-half",
- "droplet",
- "earbuds",
- "easel-fill",
- "easel",
- "egg-fill",
- "egg-fried",
- "egg",
- "eject-fill",
- "eject",
- "emoji-angry-fill",
- "emoji-angry",
- "emoji-dizzy-fill",
- "emoji-dizzy",
- "emoji-expressionless-fill",
- "emoji-expressionless",
- "emoji-frown-fill",
- "emoji-frown",
- "emoji-heart-eyes-fill",
- "emoji-heart-eyes",
- "emoji-laughing-fill",
- "emoji-laughing",
- "emoji-neutral-fill",
- "emoji-neutral",
- "emoji-smile-fill",
- "emoji-smile-upside-down-fill",
- "emoji-smile-upside-down",
- "emoji-smile",
- "emoji-sunglasses-fill",
- "emoji-sunglasses",
- "emoji-wink-fill",
- "emoji-wink",
- "envelope-fill",
- "envelope-open-fill",
- "envelope-open",
- "envelope",
- "eraser-fill",
- "eraser",
- "exclamation-circle-fill",
- "exclamation-circle",
- "exclamation-diamond-fill",
- "exclamation-diamond",
- "exclamation-octagon-fill",
- "exclamation-octagon",
- "exclamation-square-fill",
- "exclamation-square",
- "exclamation-triangle-fill",
- "exclamation-triangle",
- "exclamation",
- "exclude",
- "eye-fill",
- "eye-slash-fill",
- "eye-slash",
- "eye",
- "eyedropper",
- "eyeglasses",
- "facebook",
- "file-arrow-down-fill",
- "file-arrow-down",
- "file-arrow-up-fill",
- "file-arrow-up",
- "file-bar-graph-fill",
- "file-bar-graph",
- "file-binary-fill",
- "file-binary",
- "file-break-fill",
- "file-break",
- "file-check-fill",
- "file-check",
- "file-code-fill",
- "file-code",
- "file-diff-fill",
- "file-diff",
- "file-earmark-arrow-down-fill",
- "file-earmark-arrow-down",
- "file-earmark-arrow-up-fill",
- "file-earmark-arrow-up",
- "file-earmark-bar-graph-fill",
- "file-earmark-bar-graph",
- "file-earmark-binary-fill",
- "file-earmark-binary",
- "file-earmark-break-fill",
- "file-earmark-break",
- "file-earmark-check-fill",
- "file-earmark-check",
- "file-earmark-code-fill",
- "file-earmark-code",
- "file-earmark-diff-fill",
- "file-earmark-diff",
- "file-earmark-easel-fill",
- "file-earmark-easel",
- "file-earmark-excel-fill",
- "file-earmark-excel",
- "file-earmark-fill",
- "file-earmark-font-fill",
- "file-earmark-font",
- "file-earmark-image-fill",
- "file-earmark-image",
- "file-earmark-lock-fill",
- "file-earmark-lock",
- "file-earmark-lock2-fill",
- "file-earmark-lock2",
- "file-earmark-medical-fill",
- "file-earmark-medical",
- "file-earmark-minus-fill",
- "file-earmark-minus",
- "file-earmark-music-fill",
- "file-earmark-music",
- "file-earmark-person-fill",
- "file-earmark-person",
- "file-earmark-play-fill",
- "file-earmark-play",
- "file-earmark-plus-fill",
- "file-earmark-plus",
- "file-earmark-post-fill",
- "file-earmark-post",
- "file-earmark-ppt-fill",
- "file-earmark-ppt",
- "file-earmark-richtext-fill",
- "file-earmark-richtext",
- "file-earmark-ruled-fill",
- "file-earmark-ruled",
- "file-earmark-slides-fill",
- "file-earmark-slides",
- "file-earmark-spreadsheet-fill",
- "file-earmark-spreadsheet",
- "file-earmark-text-fill",
- "file-earmark-text",
- "file-earmark-word-fill",
- "file-earmark-word",
- "file-earmark-x-fill",
- "file-earmark-x",
- "file-earmark-zip-fill",
- "file-earmark-zip",
- "file-earmark",
- "file-easel-fill",
- "file-easel",
- "file-excel-fill",
- "file-excel",
- "file-fill",
- "file-font-fill",
- "file-font",
- "file-image-fill",
- "file-image",
- "file-lock-fill",
- "file-lock",
- "file-lock2-fill",
- "file-lock2",
- "file-medical-fill",
- "file-medical",
- "file-minus-fill",
- "file-minus",
- "file-music-fill",
- "file-music",
- "file-person-fill",
- "file-person",
- "file-play-fill",
- "file-play",
- "file-plus-fill",
- "file-plus",
- "file-post-fill",
- "file-post",
- "file-ppt-fill",
- "file-ppt",
- "file-richtext-fill",
- "file-richtext",
- "file-ruled-fill",
- "file-ruled",
- "file-slides-fill",
- "file-slides",
- "file-spreadsheet-fill",
- "file-spreadsheet",
- "file-text-fill",
- "file-text",
- "file-word-fill",
- "file-word",
- "file-x-fill",
- "file-x",
- "file-zip-fill",
- "file-zip",
- "file",
- "files-alt",
- "files",
- "film",
- "filter-circle-fill",
- "filter-circle",
- "filter-left",
- "filter-right",
- "filter-square-fill",
- "filter-square",
- "filter",
- "flag-fill",
- "flag",
- "flower1",
- "flower2",
- "flower3",
- "folder-check",
- "folder-fill",
- "folder-minus",
- "folder-plus",
- "folder-symlink-fill",
- "folder-symlink",
- "folder-x",
- "folder",
- "folder2-open",
- "folder2",
- "fonts",
- "forward-fill",
- "forward",
- "front",
- "fullscreen-exit",
- "fullscreen",
- "funnel-fill",
- "funnel",
- "gear-fill",
- "gear-wide-connected",
- "gear-wide",
- "gear",
- "gem",
- "geo-alt-fill",
- "geo-alt",
- "geo-fill",
- "geo",
- "gift-fill",
- "gift",
- "github",
- "globe",
- "globe2",
- "google",
- "graph-down",
- "graph-up",
- "grid-1x2-fill",
- "grid-1x2",
- "grid-3x2-gap-fill",
- "grid-3x2-gap",
- "grid-3x2",
- "grid-3x3-gap-fill",
- "grid-3x3-gap",
- "grid-3x3",
- "grid-fill",
- "grid",
- "grip-horizontal",
- "grip-vertical",
- "hammer",
- "hand-index-fill",
- "hand-index-thumb-fill",
- "hand-index-thumb",
- "hand-index",
- "hand-thumbs-down-fill",
- "hand-thumbs-down",
- "hand-thumbs-up-fill",
- "hand-thumbs-up",
- "handbag-fill",
- "handbag",
- "hash",
- "hdd-fill",
- "hdd-network-fill",
- "hdd-network",
- "hdd-rack-fill",
- "hdd-rack",
- "hdd-stack-fill",
- "hdd-stack",
- "hdd",
- "headphones",
- "headset",
- "heart-fill",
- "heart-half",
- "heart",
- "heptagon-fill",
- "heptagon-half",
- "heptagon",
- "hexagon-fill",
- "hexagon-half",
- "hexagon",
- "hourglass-bottom",
- "hourglass-split",
- "hourglass-top",
- "hourglass",
- "house-door-fill",
- "house-door",
- "house-fill",
- "house",
- "hr",
- "hurricane",
- "image-alt",
- "image-fill",
- "image",
- "images",
- "inbox-fill",
- "inbox",
- "inboxes-fill",
- "inboxes",
- "info-circle-fill",
- "info-circle",
- "info-square-fill",
- "info-square",
- "info",
- "input-cursor-text",
- "input-cursor",
- "instagram",
- "intersect",
- "journal-album",
- "journal-arrow-down",
- "journal-arrow-up",
- "journal-bookmark-fill",
- "journal-bookmark",
- "journal-check",
- "journal-code",
- "journal-medical",
- "journal-minus",
- "journal-plus",
- "journal-richtext",
- "journal-text",
- "journal-x",
- "journal",
- "journals",
- "joystick",
- "justify-left",
- "justify-right",
- "justify",
- "kanban-fill",
- "kanban",
- "key-fill",
- "key",
- "keyboard-fill",
- "keyboard",
- "ladder",
- "lamp-fill",
- "lamp",
- "laptop-fill",
- "laptop",
- "layer-backward",
- "layer-forward",
- "layers-fill",
- "layers-half",
- "layers",
- "layout-sidebar-inset-reverse",
- "layout-sidebar-inset",
- "layout-sidebar-reverse",
- "layout-sidebar",
- "layout-split",
- "layout-text-sidebar-reverse",
- "layout-text-sidebar",
- "layout-text-window-reverse",
- "layout-text-window",
- "layout-three-columns",
- "layout-wtf",
- "life-preserver",
- "lightbulb-fill",
- "lightbulb-off-fill",
- "lightbulb-off",
- "lightbulb",
- "lightning-charge-fill",
- "lightning-charge",
- "lightning-fill",
- "lightning",
- "link-45deg",
- "link",
- "linkedin",
- "list-check",
- "list-nested",
- "list-ol",
- "list-stars",
- "list-task",
- "list-ul",
- "list",
- "lock-fill",
- "lock",
- "mailbox",
- "mailbox2",
- "map-fill",
- "map",
- "markdown-fill",
- "markdown",
- "mask",
- "megaphone-fill",
- "megaphone",
- "menu-app-fill",
- "menu-app",
- "menu-button-fill",
- "menu-button-wide-fill",
- "menu-button-wide",
- "menu-button",
- "menu-down",
- "menu-up",
- "mic-fill",
- "mic-mute-fill",
- "mic-mute",
- "mic",
- "minecart-loaded",
- "minecart",
- "moisture",
- "moon-fill",
- "moon-stars-fill",
- "moon-stars",
- "moon",
- "mouse-fill",
- "mouse",
- "mouse2-fill",
- "mouse2",
- "mouse3-fill",
- "mouse3",
- "music-note-beamed",
- "music-note-list",
- "music-note",
- "music-player-fill",
- "music-player",
- "newspaper",
- "node-minus-fill",
- "node-minus",
- "node-plus-fill",
- "node-plus",
- "nut-fill",
- "nut",
- "octagon-fill",
- "octagon-half",
- "octagon",
- "option",
- "outlet",
- "paint-bucket",
- "palette-fill",
- "palette",
- "palette2",
- "paperclip",
- "paragraph",
- "patch-check-fill",
- "patch-check",
- "patch-exclamation-fill",
- "patch-exclamation",
- "patch-minus-fill",
- "patch-minus",
- "patch-plus-fill",
- "patch-plus",
- "patch-question-fill",
- "patch-question",
- "pause-btn-fill",
- "pause-btn",
- "pause-circle-fill",
- "pause-circle",
- "pause-fill",
- "pause",
- "peace-fill",
- "peace",
- "pen-fill",
- "pen",
- "pencil-fill",
- "pencil-square",
- "pencil",
- "pentagon-fill",
- "pentagon-half",
- "pentagon",
- "people-fill",
- "people",
- "percent",
- "person-badge-fill",
- "person-badge",
- "person-bounding-box",
- "person-check-fill",
- "person-check",
- "person-circle",
- "person-dash-fill",
- "person-dash",
- "person-fill",
- "person-lines-fill",
- "person-plus-fill",
- "person-plus",
- "person-square",
- "person-x-fill",
- "person-x",
- "person",
- "phone-fill",
- "phone-landscape-fill",
- "phone-landscape",
- "phone-vibrate-fill",
- "phone-vibrate",
- "phone",
- "pie-chart-fill",
- "pie-chart",
- "pin-angle-fill",
- "pin-angle",
- "pin-fill",
- "pin",
- "pip-fill",
- "pip",
- "play-btn-fill",
- "play-btn",
- "play-circle-fill",
- "play-circle",
- "play-fill",
- "play",
- "plug-fill",
- "plug",
- "plus-circle-dotted",
- "plus-circle-fill",
- "plus-circle",
- "plus-square-dotted",
- "plus-square-fill",
- "plus-square",
- "plus",
- "power",
- "printer-fill",
- "printer",
- "puzzle-fill",
- "puzzle",
- "question-circle-fill",
- "question-circle",
- "question-diamond-fill",
- "question-diamond",
- "question-octagon-fill",
- "question-octagon",
- "question-square-fill",
- "question-square",
- "question",
- "rainbow",
- "receipt-cutoff",
- "receipt",
- "reception-0",
- "reception-1",
- "reception-2",
- "reception-3",
- "reception-4",
- "record-btn-fill",
- "record-btn",
- "record-circle-fill",
- "record-circle",
- "record-fill",
- "record",
- "record2-fill",
- "record2",
- "reply-all-fill",
- "reply-all",
- "reply-fill",
- "reply",
- "rss-fill",
- "rss",
- "rulers",
- "save-fill",
- "save",
- "save2-fill",
- "save2",
- "scissors",
- "screwdriver",
- "search",
- "segmented-nav",
- "server",
- "share-fill",
- "share",
- "shield-check",
- "shield-exclamation",
- "shield-fill-check",
- "shield-fill-exclamation",
- "shield-fill-minus",
- "shield-fill-plus",
- "shield-fill-x",
- "shield-fill",
- "shield-lock-fill",
- "shield-lock",
- "shield-minus",
- "shield-plus",
- "shield-shaded",
- "shield-slash-fill",
- "shield-slash",
- "shield-x",
- "shield",
- "shift-fill",
- "shift",
- "shop-window",
- "shop",
- "shuffle",
- "signpost-2-fill",
- "signpost-2",
- "signpost-fill",
- "signpost-split-fill",
- "signpost-split",
- "signpost",
- "sim-fill",
- "sim",
- "skip-backward-btn-fill",
- "skip-backward-btn",
- "skip-backward-circle-fill",
- "skip-backward-circle",
- "skip-backward-fill",
- "skip-backward",
- "skip-end-btn-fill",
- "skip-end-btn",
- "skip-end-circle-fill",
- "skip-end-circle",
- "skip-end-fill",
- "skip-end",
- "skip-forward-btn-fill",
- "skip-forward-btn",
- "skip-forward-circle-fill",
- "skip-forward-circle",
- "skip-forward-fill",
- "skip-forward",
- "skip-start-btn-fill",
- "skip-start-btn",
- "skip-start-circle-fill",
- "skip-start-circle",
- "skip-start-fill",
- "skip-start",
- "slack",
- "slash-circle-fill",
- "slash-circle",
- "slash-square-fill",
- "slash-square",
- "slash",
- "sliders",
- "smartwatch",
- "snow",
- "snow2",
- "snow3",
- "sort-alpha-down-alt",
- "sort-alpha-down",
- "sort-alpha-up-alt",
- "sort-alpha-up",
- "sort-down-alt",
- "sort-down",
- "sort-numeric-down-alt",
- "sort-numeric-down",
- "sort-numeric-up-alt",
- "sort-numeric-up",
- "sort-up-alt",
- "sort-up",
- "soundwave",
- "speaker-fill",
- "speaker",
- "speedometer",
- "speedometer2",
- "spellcheck",
- "square-fill",
- "square-half",
- "square",
- "stack",
- "star-fill",
- "star-half",
- "star",
- "stars",
- "stickies-fill",
- "stickies",
- "sticky-fill",
- "sticky",
- "stop-btn-fill",
- "stop-btn",
- "stop-circle-fill",
- "stop-circle",
- "stop-fill",
- "stop",
- "stoplights-fill",
- "stoplights",
- "stopwatch-fill",
- "stopwatch",
- "subtract",
- "suit-club-fill",
- "suit-club",
- "suit-diamond-fill",
- "suit-diamond",
- "suit-heart-fill",
- "suit-heart",
- "suit-spade-fill",
- "suit-spade",
- "sun-fill",
- "sun",
- "sunglasses",
- "sunrise-fill",
- "sunrise",
- "sunset-fill",
- "sunset",
- "symmetry-horizontal",
- "symmetry-vertical",
- "table",
- "tablet-fill",
- "tablet-landscape-fill",
- "tablet-landscape",
- "tablet",
- "tag-fill",
- "tag",
- "tags-fill",
- "tags",
- "telegram",
- "telephone-fill",
- "telephone-forward-fill",
- "telephone-forward",
- "telephone-inbound-fill",
- "telephone-inbound",
- "telephone-minus-fill",
- "telephone-minus",
- "telephone-outbound-fill",
- "telephone-outbound",
- "telephone-plus-fill",
- "telephone-plus",
- "telephone-x-fill",
- "telephone-x",
- "telephone",
- "terminal-fill",
- "terminal",
- "text-center",
- "text-indent-left",
- "text-indent-right",
- "text-left",
- "text-paragraph",
- "text-right",
- "textarea-resize",
- "textarea-t",
- "textarea",
- "thermometer-half",
- "thermometer-high",
- "thermometer-low",
- "thermometer-snow",
- "thermometer-sun",
- "thermometer",
- "three-dots-vertical",
- "three-dots",
- "toggle-off",
- "toggle-on",
- "toggle2-off",
- "toggle2-on",
- "toggles",
- "toggles2",
- "tools",
- "tornado",
- "trash-fill",
- "trash",
- "trash2-fill",
- "trash2",
- "tree-fill",
- "tree",
- "triangle-fill",
- "triangle-half",
- "triangle",
- "trophy-fill",
- "trophy",
- "tropical-storm",
- "truck-flatbed",
- "truck",
- "tsunami",
- "tv-fill",
- "tv",
- "twitch",
- "twitter",
- "type-bold",
- "type-h1",
- "type-h2",
- "type-h3",
- "type-italic",
- "type-strikethrough",
- "type-underline",
- "type",
- "ui-checks-grid",
- "ui-checks",
- "ui-radios-grid",
- "ui-radios",
- "umbrella-fill",
- "umbrella",
- "union",
- "unlock-fill",
- "unlock",
- "upc-scan",
- "upc",
- "upload",
- "vector-pen",
- "view-list",
- "view-stacked",
- "vinyl-fill",
- "vinyl",
- "voicemail",
- "volume-down-fill",
- "volume-down",
- "volume-mute-fill",
- "volume-mute",
- "volume-off-fill",
- "volume-off",
- "volume-up-fill",
- "volume-up",
- "vr",
- "wallet-fill",
- "wallet",
- "wallet2",
- "watch",
- "water",
- "whatsapp",
- "wifi-1",
- "wifi-2",
- "wifi-off",
- "wifi",
- "wind",
- "window-dock",
- "window-sidebar",
- "window",
- "wrench",
- "x-circle-fill",
- "x-circle",
- "x-diamond-fill",
- "x-diamond",
- "x-octagon-fill",
- "x-octagon",
- "x-square-fill",
- "x-square",
- "x",
- "youtube",
- "zoom-in",
- "zoom-out",
- "bank",
- "bank2",
- "bell-slash-fill",
- "bell-slash",
- "cash-coin",
- "check-lg",
- "coin",
- "currency-bitcoin",
- "currency-dollar",
- "currency-euro",
- "currency-exchange",
- "currency-pound",
- "currency-yen",
- "dash-lg",
- "exclamation-lg",
- "file-earmark-pdf-fill",
- "file-earmark-pdf",
- "file-pdf-fill",
- "file-pdf",
- "gender-ambiguous",
- "gender-female",
- "gender-male",
- "gender-trans",
- "headset-vr",
- "info-lg",
- "mastodon",
- "messenger",
- "piggy-bank-fill",
- "piggy-bank",
- "pin-map-fill",
- "pin-map",
- "plus-lg",
- "question-lg",
- "recycle",
- "reddit",
- "safe-fill",
- "safe2-fill",
- "safe2",
- "sd-card-fill",
- "sd-card",
- "skype",
- "slash-lg",
- "translate",
- "x-lg",
- "safe",
- "apple",
- "microsoft",
- "windows",
- "behance",
- "dribbble",
- "line",
- "medium",
- "paypal",
- "pinterest",
- "signal",
- "snapchat",
- "spotify",
- "stack-overflow",
- "strava",
- "wordpress",
- "vimeo",
- "activity",
- "easel2-fill",
- "easel2",
- "easel3-fill",
- "easel3",
- "fan",
- "fingerprint",
- "graph-down-arrow",
- "graph-up-arrow",
- "hypnotize",
- "magic",
- "person-rolodex",
- "person-video",
- "person-video2",
- "person-video3",
- "person-workspace",
- "radioactive",
- "webcam-fill",
- "webcam",
- "yin-yang",
- "bandaid-fill",
- "bandaid",
- "bluetooth",
- "body-text",
- "boombox",
- "boxes",
- "dpad-fill",
- "dpad",
- "ear-fill",
- "ear",
- "envelope-check-fill",
- "envelope-check",
- "envelope-dash-fill",
- "envelope-dash",
- "envelope-exclamation-fill",
- "envelope-exclamation",
- "envelope-plus-fill",
- "envelope-plus",
- "envelope-slash-fill",
- "envelope-slash",
- "envelope-x-fill",
- "envelope-x",
- "explicit-fill",
- "explicit",
- "git",
- "infinity",
- "list-columns-reverse",
- "list-columns",
- "meta",
- "nintendo-switch",
- "pc-display-horizontal",
- "pc-display",
- "pc-horizontal",
- "pc",
- "playstation",
- "plus-slash-minus",
- "projector-fill",
- "projector",
- "qr-code-scan",
- "qr-code",
- "quora",
- "quote",
- "robot",
- "send-check-fill",
- "send-check",
- "send-dash-fill",
- "send-dash",
- "send-exclamation-fill",
- "send-exclamation",
- "send-fill",
- "send-plus-fill",
- "send-plus",
- "send-slash-fill",
- "send-slash",
- "send-x-fill",
- "send-x",
- "send",
- "steam",
- "terminal-dash",
- "terminal-plus",
- "terminal-split",
- "ticket-detailed-fill",
- "ticket-detailed",
- "ticket-fill",
- "ticket-perforated-fill",
- "ticket-perforated",
- "ticket",
- "tiktok",
- "window-dash",
- "window-desktop",
- "window-fullscreen",
- "window-plus",
- "window-split",
- "window-stack",
- "window-x",
- "xbox",
- "ethernet",
- "hdmi-fill",
- "hdmi",
- "usb-c-fill",
- "usb-c",
- "usb-fill",
- "usb-plug-fill",
- "usb-plug",
- "usb-symbol",
- "usb",
- "boombox-fill",
- "displayport",
- "gpu-card",
- "memory",
- "modem-fill",
- "modem",
- "motherboard-fill",
- "motherboard",
- "optical-audio-fill",
- "optical-audio",
- "pci-card",
- "router-fill",
- "router",
- "thunderbolt-fill",
- "thunderbolt",
- "usb-drive-fill",
- "usb-drive",
- "usb-micro-fill",
- "usb-micro",
- "usb-mini-fill",
- "usb-mini",
- "cloud-haze2",
- "device-hdd-fill",
- "device-hdd",
- "device-ssd-fill",
- "device-ssd",
- "displayport-fill",
- "mortarboard-fill",
- "mortarboard",
- "terminal-x",
- "arrow-through-heart-fill",
- "arrow-through-heart",
- "badge-sd-fill",
- "badge-sd",
- "bag-heart-fill",
- "bag-heart",
- "balloon-fill",
- "balloon-heart-fill",
- "balloon-heart",
- "balloon",
- "box2-fill",
- "box2-heart-fill",
- "box2-heart",
- "box2",
- "braces-asterisk",
- "calendar-heart-fill",
- "calendar-heart",
- "calendar2-heart-fill",
- "calendar2-heart",
- "chat-heart-fill",
- "chat-heart",
- "chat-left-heart-fill",
- "chat-left-heart",
- "chat-right-heart-fill",
- "chat-right-heart",
- "chat-square-heart-fill",
- "chat-square-heart",
- "clipboard-check-fill",
- "clipboard-data-fill",
- "clipboard-fill",
- "clipboard-heart-fill",
- "clipboard-heart",
- "clipboard-minus-fill",
- "clipboard-plus-fill",
- "clipboard-pulse",
- "clipboard-x-fill",
- "clipboard2-check-fill",
- "clipboard2-check",
- "clipboard2-data-fill",
- "clipboard2-data",
- "clipboard2-fill",
- "clipboard2-heart-fill",
- "clipboard2-heart",
- "clipboard2-minus-fill",
- "clipboard2-minus",
- "clipboard2-plus-fill",
- "clipboard2-plus",
- "clipboard2-pulse-fill",
- "clipboard2-pulse",
- "clipboard2-x-fill",
- "clipboard2-x",
- "clipboard2",
- "emoji-kiss-fill",
- "emoji-kiss",
- "envelope-heart-fill",
- "envelope-heart",
- "envelope-open-heart-fill",
- "envelope-open-heart",
- "envelope-paper-fill",
- "envelope-paper-heart-fill",
- "envelope-paper-heart",
- "envelope-paper",
- "filetype-aac",
- "filetype-ai",
- "filetype-bmp",
- "filetype-cs",
- "filetype-css",
- "filetype-csv",
- "filetype-doc",
- "filetype-docx",
- "filetype-exe",
- "filetype-gif",
- "filetype-heic",
- "filetype-html",
- "filetype-java",
- "filetype-jpg",
- "filetype-js",
- "filetype-jsx",
- "filetype-key",
- "filetype-m4p",
- "filetype-md",
- "filetype-mdx",
- "filetype-mov",
- "filetype-mp3",
- "filetype-mp4",
- "filetype-otf",
- "filetype-pdf",
- "filetype-php",
- "filetype-png",
- "filetype-ppt",
- "filetype-psd",
- "filetype-py",
- "filetype-raw",
- "filetype-rb",
- "filetype-sass",
- "filetype-scss",
- "filetype-sh",
- "filetype-svg",
- "filetype-tiff",
- "filetype-tsx",
- "filetype-ttf",
- "filetype-txt",
- "filetype-wav",
- "filetype-woff",
- "filetype-xls",
- "filetype-xml",
- "filetype-yml",
- "heart-arrow",
- "heart-pulse-fill",
- "heart-pulse",
- "heartbreak-fill",
- "heartbreak",
- "hearts",
- "hospital-fill",
- "hospital",
- "house-heart-fill",
- "house-heart",
- "incognito",
- "magnet-fill",
- "magnet",
- "person-heart",
- "person-hearts",
- "phone-flip",
- "plugin",
- "postage-fill",
- "postage-heart-fill",
- "postage-heart",
- "postage",
- "postcard-fill",
- "postcard-heart-fill",
- "postcard-heart",
- "postcard",
- "search-heart-fill",
- "search-heart",
- "sliders2-vertical",
- "sliders2",
- "trash3-fill",
- "trash3",
- "valentine",
- "valentine2",
- "wrench-adjustable-circle-fill",
- "wrench-adjustable-circle",
- "wrench-adjustable",
- "filetype-json",
- "filetype-pptx",
- "filetype-xlsx",
- "1-circle-fill",
- "1-circle",
- "1-square-fill",
- "1-square",
- "2-circle-fill",
- "2-circle",
- "2-square-fill",
- "2-square",
- "3-circle-fill",
- "3-circle",
- "3-square-fill",
- "3-square",
- "4-circle-fill",
- "4-circle",
- "4-square-fill",
- "4-square",
- "5-circle-fill",
- "5-circle",
- "5-square-fill",
- "5-square",
- "6-circle-fill",
- "6-circle",
- "6-square-fill",
- "6-square",
- "7-circle-fill",
- "7-circle",
- "7-square-fill",
- "7-square",
- "8-circle-fill",
- "8-circle",
- "8-square-fill",
- "8-square",
- "9-circle-fill",
- "9-circle",
- "9-square-fill",
- "9-square",
- "airplane-engines-fill",
- "airplane-engines",
- "airplane-fill",
- "airplane",
- "alexa",
- "alipay",
- "android",
- "android2",
- "box-fill",
- "box-seam-fill",
- "browser-chrome",
- "browser-edge",
- "browser-firefox",
- "browser-safari",
- "c-circle-fill",
- "c-circle",
- "c-square-fill",
- "c-square",
- "capsule-pill",
- "capsule",
- "car-front-fill",
- "car-front",
- "cassette-fill",
- "cassette",
- "cc-circle-fill",
- "cc-circle",
- "cc-square-fill",
- "cc-square",
- "cup-hot-fill",
- "cup-hot",
- "currency-rupee",
- "dropbox",
- "escape",
- "fast-forward-btn-fill",
- "fast-forward-btn",
- "fast-forward-circle-fill",
- "fast-forward-circle",
- "fast-forward-fill",
- "fast-forward",
- "filetype-sql",
- "fire",
- "google-play",
- "h-circle-fill",
- "h-circle",
- "h-square-fill",
- "h-square",
- "indent",
- "lungs-fill",
- "lungs",
- "microsoft-teams",
- "p-circle-fill",
- "p-circle",
- "p-square-fill",
- "p-square",
- "pass-fill",
- "pass",
- "prescription",
- "prescription2",
- "r-circle-fill",
- "r-circle",
- "r-square-fill",
- "r-square",
- "repeat-1",
- "repeat",
- "rewind-btn-fill",
- "rewind-btn",
- "rewind-circle-fill",
- "rewind-circle",
- "rewind-fill",
- "rewind",
- "train-freight-front-fill",
- "train-freight-front",
- "train-front-fill",
- "train-front",
- "train-lightrail-front-fill",
- "train-lightrail-front",
- "truck-front-fill",
- "truck-front",
- "ubuntu",
- "unindent",
- "unity",
- "universal-access-circle",
- "universal-access",
- "virus",
- "virus2",
- "wechat",
- "yelp",
- "sign-stop-fill",
- "sign-stop-lights-fill",
- "sign-stop-lights",
- "sign-stop",
- "sign-turn-left-fill",
- "sign-turn-left",
- "sign-turn-right-fill",
- "sign-turn-right",
- "sign-turn-slight-left-fill",
- "sign-turn-slight-left",
- "sign-turn-slight-right-fill",
- "sign-turn-slight-right",
- "sign-yield-fill",
- "sign-yield",
- "ev-station-fill",
- "ev-station",
- "fuel-pump-diesel-fill",
- "fuel-pump-diesel",
- "fuel-pump-fill",
- "fuel-pump",
- "0-circle-fill",
- "0-circle",
- "0-square-fill",
- "0-square",
- "rocket-fill",
- "rocket-takeoff-fill",
- "rocket-takeoff",
- "rocket",
- "stripe",
- "subscript",
- "superscript",
- "trello",
- "envelope-at-fill",
- "envelope-at",
- "regex",
- "text-wrap",
- "sign-dead-end-fill",
- "sign-dead-end",
- "sign-do-not-enter-fill",
- "sign-do-not-enter",
- "sign-intersection-fill",
- "sign-intersection-side-fill",
- "sign-intersection-side",
- "sign-intersection-t-fill",
- "sign-intersection-t",
- "sign-intersection-y-fill",
- "sign-intersection-y",
- "sign-intersection",
- "sign-merge-left-fill",
- "sign-merge-left",
- "sign-merge-right-fill",
- "sign-merge-right",
- "sign-no-left-turn-fill",
- "sign-no-left-turn",
- "sign-no-parking-fill",
- "sign-no-parking",
- "sign-no-right-turn-fill",
- "sign-no-right-turn",
- "sign-railroad-fill",
- "sign-railroad",
- "building-add",
- "building-check",
- "building-dash",
- "building-down",
- "building-exclamation",
- "building-fill-add",
- "building-fill-check",
- "building-fill-dash",
- "building-fill-down",
- "building-fill-exclamation",
- "building-fill-gear",
- "building-fill-lock",
- "building-fill-slash",
- "building-fill-up",
- "building-fill-x",
- "building-fill",
- "building-gear",
- "building-lock",
- "building-slash",
- "building-up",
- "building-x",
- "buildings-fill",
- "buildings",
- "bus-front-fill",
- "bus-front",
- "ev-front-fill",
- "ev-front",
- "globe-americas",
- "globe-asia-australia",
- "globe-central-south-asia",
- "globe-europe-africa",
- "house-add-fill",
- "house-add",
- "house-check-fill",
- "house-check",
- "house-dash-fill",
- "house-dash",
- "house-down-fill",
- "house-down",
- "house-exclamation-fill",
- "house-exclamation",
- "house-gear-fill",
- "house-gear",
- "house-lock-fill",
- "house-lock",
- "house-slash-fill",
- "house-slash",
- "house-up-fill",
- "house-up",
- "house-x-fill",
- "house-x",
- "person-add",
- "person-down",
- "person-exclamation",
- "person-fill-add",
- "person-fill-check",
- "person-fill-dash",
- "person-fill-down",
- "person-fill-exclamation",
- "person-fill-gear",
- "person-fill-lock",
- "person-fill-slash",
- "person-fill-up",
- "person-fill-x",
- "person-gear",
- "person-lock",
- "person-slash",
- "person-up",
- "scooter",
- "taxi-front-fill",
- "taxi-front",
- "amd",
- "database-add",
- "database-check",
- "database-dash",
- "database-down",
- "database-exclamation",
- "database-fill-add",
- "database-fill-check",
- "database-fill-dash",
- "database-fill-down",
- "database-fill-exclamation",
- "database-fill-gear",
- "database-fill-lock",
- "database-fill-slash",
- "database-fill-up",
- "database-fill-x",
- "database-fill",
- "database-gear",
- "database-lock",
- "database-slash",
- "database-up",
- "database-x",
- "database",
- "houses-fill",
- "houses",
- "nvidia",
- "person-vcard-fill",
- "person-vcard",
- "sina-weibo",
- "tencent-qq",
- "wikipedia",
+var icons = map[string]struct{}{
+ "123": {},
+ "alarm-fill": {},
+ "alarm": {},
+ "align-bottom": {},
+ "align-center": {},
+ "align-end": {},
+ "align-middle": {},
+ "align-start": {},
+ "align-top": {},
+ "alt": {},
+ "app-indicator": {},
+ "app": {},
+ "archive-fill": {},
+ "archive": {},
+ "arrow-90deg-down": {},
+ "arrow-90deg-left": {},
+ "arrow-90deg-right": {},
+ "arrow-90deg-up": {},
+ "arrow-bar-down": {},
+ "arrow-bar-left": {},
+ "arrow-bar-right": {},
+ "arrow-bar-up": {},
+ "arrow-clockwise": {},
+ "arrow-counterclockwise": {},
+ "arrow-down-circle-fill": {},
+ "arrow-down-circle": {},
+ "arrow-down-left-circle-fill": {},
+ "arrow-down-left-circle": {},
+ "arrow-down-left-square-fill": {},
+ "arrow-down-left-square": {},
+ "arrow-down-left": {},
+ "arrow-down-right-circle-fill": {},
+ "arrow-down-right-circle": {},
+ "arrow-down-right-square-fill": {},
+ "arrow-down-right-square": {},
+ "arrow-down-right": {},
+ "arrow-down-short": {},
+ "arrow-down-square-fill": {},
+ "arrow-down-square": {},
+ "arrow-down-up": {},
+ "arrow-down": {},
+ "arrow-left-circle-fill": {},
+ "arrow-left-circle": {},
+ "arrow-left-right": {},
+ "arrow-left-short": {},
+ "arrow-left-square-fill": {},
+ "arrow-left-square": {},
+ "arrow-left": {},
+ "arrow-repeat": {},
+ "arrow-return-left": {},
+ "arrow-return-right": {},
+ "arrow-right-circle-fill": {},
+ "arrow-right-circle": {},
+ "arrow-right-short": {},
+ "arrow-right-square-fill": {},
+ "arrow-right-square": {},
+ "arrow-right": {},
+ "arrow-up-circle-fill": {},
+ "arrow-up-circle": {},
+ "arrow-up-left-circle-fill": {},
+ "arrow-up-left-circle": {},
+ "arrow-up-left-square-fill": {},
+ "arrow-up-left-square": {},
+ "arrow-up-left": {},
+ "arrow-up-right-circle-fill": {},
+ "arrow-up-right-circle": {},
+ "arrow-up-right-square-fill": {},
+ "arrow-up-right-square": {},
+ "arrow-up-right": {},
+ "arrow-up-short": {},
+ "arrow-up-square-fill": {},
+ "arrow-up-square": {},
+ "arrow-up": {},
+ "arrows-angle-contract": {},
+ "arrows-angle-expand": {},
+ "arrows-collapse": {},
+ "arrows-expand": {},
+ "arrows-fullscreen": {},
+ "arrows-move": {},
+ "aspect-ratio-fill": {},
+ "aspect-ratio": {},
+ "asterisk": {},
+ "at": {},
+ "award-fill": {},
+ "award": {},
+ "back": {},
+ "backspace-fill": {},
+ "backspace-reverse-fill": {},
+ "backspace-reverse": {},
+ "backspace": {},
+ "badge-3d-fill": {},
+ "badge-3d": {},
+ "badge-4k-fill": {},
+ "badge-4k": {},
+ "badge-8k-fill": {},
+ "badge-8k": {},
+ "badge-ad-fill": {},
+ "badge-ad": {},
+ "badge-ar-fill": {},
+ "badge-ar": {},
+ "badge-cc-fill": {},
+ "badge-cc": {},
+ "badge-hd-fill": {},
+ "badge-hd": {},
+ "badge-tm-fill": {},
+ "badge-tm": {},
+ "badge-vo-fill": {},
+ "badge-vo": {},
+ "badge-vr-fill": {},
+ "badge-vr": {},
+ "badge-wc-fill": {},
+ "badge-wc": {},
+ "bag-check-fill": {},
+ "bag-check": {},
+ "bag-dash-fill": {},
+ "bag-dash": {},
+ "bag-fill": {},
+ "bag-plus-fill": {},
+ "bag-plus": {},
+ "bag-x-fill": {},
+ "bag-x": {},
+ "bag": {},
+ "bar-chart-fill": {},
+ "bar-chart-line-fill": {},
+ "bar-chart-line": {},
+ "bar-chart-steps": {},
+ "bar-chart": {},
+ "basket-fill": {},
+ "basket": {},
+ "basket2-fill": {},
+ "basket2": {},
+ "basket3-fill": {},
+ "basket3": {},
+ "battery-charging": {},
+ "battery-full": {},
+ "battery-half": {},
+ "battery": {},
+ "bell-fill": {},
+ "bell": {},
+ "bezier": {},
+ "bezier2": {},
+ "bicycle": {},
+ "binoculars-fill": {},
+ "binoculars": {},
+ "blockquote-left": {},
+ "blockquote-right": {},
+ "book-fill": {},
+ "book-half": {},
+ "book": {},
+ "bookmark-check-fill": {},
+ "bookmark-check": {},
+ "bookmark-dash-fill": {},
+ "bookmark-dash": {},
+ "bookmark-fill": {},
+ "bookmark-heart-fill": {},
+ "bookmark-heart": {},
+ "bookmark-plus-fill": {},
+ "bookmark-plus": {},
+ "bookmark-star-fill": {},
+ "bookmark-star": {},
+ "bookmark-x-fill": {},
+ "bookmark-x": {},
+ "bookmark": {},
+ "bookmarks-fill": {},
+ "bookmarks": {},
+ "bookshelf": {},
+ "bootstrap-fill": {},
+ "bootstrap-reboot": {},
+ "bootstrap": {},
+ "border-all": {},
+ "border-bottom": {},
+ "border-center": {},
+ "border-inner": {},
+ "border-left": {},
+ "border-middle": {},
+ "border-outer": {},
+ "border-right": {},
+ "border-style": {},
+ "border-top": {},
+ "border-width": {},
+ "border": {},
+ "bounding-box-circles": {},
+ "bounding-box": {},
+ "box-arrow-down-left": {},
+ "box-arrow-down-right": {},
+ "box-arrow-down": {},
+ "box-arrow-in-down-left": {},
+ "box-arrow-in-down-right": {},
+ "box-arrow-in-down": {},
+ "box-arrow-in-left": {},
+ "box-arrow-in-right": {},
+ "box-arrow-in-up-left": {},
+ "box-arrow-in-up-right": {},
+ "box-arrow-in-up": {},
+ "box-arrow-left": {},
+ "box-arrow-right": {},
+ "box-arrow-up-left": {},
+ "box-arrow-up-right": {},
+ "box-arrow-up": {},
+ "box-seam": {},
+ "box": {},
+ "braces": {},
+ "bricks": {},
+ "briefcase-fill": {},
+ "briefcase": {},
+ "brightness-alt-high-fill": {},
+ "brightness-alt-high": {},
+ "brightness-alt-low-fill": {},
+ "brightness-alt-low": {},
+ "brightness-high-fill": {},
+ "brightness-high": {},
+ "brightness-low-fill": {},
+ "brightness-low": {},
+ "broadcast-pin": {},
+ "broadcast": {},
+ "brush-fill": {},
+ "brush": {},
+ "bucket-fill": {},
+ "bucket": {},
+ "bug-fill": {},
+ "bug": {},
+ "building": {},
+ "bullseye": {},
+ "calculator-fill": {},
+ "calculator": {},
+ "calendar-check-fill": {},
+ "calendar-check": {},
+ "calendar-date-fill": {},
+ "calendar-date": {},
+ "calendar-day-fill": {},
+ "calendar-day": {},
+ "calendar-event-fill": {},
+ "calendar-event": {},
+ "calendar-fill": {},
+ "calendar-minus-fill": {},
+ "calendar-minus": {},
+ "calendar-month-fill": {},
+ "calendar-month": {},
+ "calendar-plus-fill": {},
+ "calendar-plus": {},
+ "calendar-range-fill": {},
+ "calendar-range": {},
+ "calendar-week-fill": {},
+ "calendar-week": {},
+ "calendar-x-fill": {},
+ "calendar-x": {},
+ "calendar": {},
+ "calendar2-check-fill": {},
+ "calendar2-check": {},
+ "calendar2-date-fill": {},
+ "calendar2-date": {},
+ "calendar2-day-fill": {},
+ "calendar2-day": {},
+ "calendar2-event-fill": {},
+ "calendar2-event": {},
+ "calendar2-fill": {},
+ "calendar2-minus-fill": {},
+ "calendar2-minus": {},
+ "calendar2-month-fill": {},
+ "calendar2-month": {},
+ "calendar2-plus-fill": {},
+ "calendar2-plus": {},
+ "calendar2-range-fill": {},
+ "calendar2-range": {},
+ "calendar2-week-fill": {},
+ "calendar2-week": {},
+ "calendar2-x-fill": {},
+ "calendar2-x": {},
+ "calendar2": {},
+ "calendar3-event-fill": {},
+ "calendar3-event": {},
+ "calendar3-fill": {},
+ "calendar3-range-fill": {},
+ "calendar3-range": {},
+ "calendar3-week-fill": {},
+ "calendar3-week": {},
+ "calendar3": {},
+ "calendar4-event": {},
+ "calendar4-range": {},
+ "calendar4-week": {},
+ "calendar4": {},
+ "camera-fill": {},
+ "camera-reels-fill": {},
+ "camera-reels": {},
+ "camera-video-fill": {},
+ "camera-video-off-fill": {},
+ "camera-video-off": {},
+ "camera-video": {},
+ "camera": {},
+ "camera2": {},
+ "capslock-fill": {},
+ "capslock": {},
+ "card-checklist": {},
+ "card-heading": {},
+ "card-image": {},
+ "card-list": {},
+ "card-text": {},
+ "caret-down-fill": {},
+ "caret-down-square-fill": {},
+ "caret-down-square": {},
+ "caret-down": {},
+ "caret-left-fill": {},
+ "caret-left-square-fill": {},
+ "caret-left-square": {},
+ "caret-left": {},
+ "caret-right-fill": {},
+ "caret-right-square-fill": {},
+ "caret-right-square": {},
+ "caret-right": {},
+ "caret-up-fill": {},
+ "caret-up-square-fill": {},
+ "caret-up-square": {},
+ "caret-up": {},
+ "cart-check-fill": {},
+ "cart-check": {},
+ "cart-dash-fill": {},
+ "cart-dash": {},
+ "cart-fill": {},
+ "cart-plus-fill": {},
+ "cart-plus": {},
+ "cart-x-fill": {},
+ "cart-x": {},
+ "cart": {},
+ "cart2": {},
+ "cart3": {},
+ "cart4": {},
+ "cash-stack": {},
+ "cash": {},
+ "cast": {},
+ "chat-dots-fill": {},
+ "chat-dots": {},
+ "chat-fill": {},
+ "chat-left-dots-fill": {},
+ "chat-left-dots": {},
+ "chat-left-fill": {},
+ "chat-left-quote-fill": {},
+ "chat-left-quote": {},
+ "chat-left-text-fill": {},
+ "chat-left-text": {},
+ "chat-left": {},
+ "chat-quote-fill": {},
+ "chat-quote": {},
+ "chat-right-dots-fill": {},
+ "chat-right-dots": {},
+ "chat-right-fill": {},
+ "chat-right-quote-fill": {},
+ "chat-right-quote": {},
+ "chat-right-text-fill": {},
+ "chat-right-text": {},
+ "chat-right": {},
+ "chat-square-dots-fill": {},
+ "chat-square-dots": {},
+ "chat-square-fill": {},
+ "chat-square-quote-fill": {},
+ "chat-square-quote": {},
+ "chat-square-text-fill": {},
+ "chat-square-text": {},
+ "chat-square": {},
+ "chat-text-fill": {},
+ "chat-text": {},
+ "chat": {},
+ "check-all": {},
+ "check-circle-fill": {},
+ "check-circle": {},
+ "check-square-fill": {},
+ "check-square": {},
+ "check": {},
+ "check2-all": {},
+ "check2-circle": {},
+ "check2-square": {},
+ "check2": {},
+ "chevron-bar-contract": {},
+ "chevron-bar-down": {},
+ "chevron-bar-expand": {},
+ "chevron-bar-left": {},
+ "chevron-bar-right": {},
+ "chevron-bar-up": {},
+ "chevron-compact-down": {},
+ "chevron-compact-left": {},
+ "chevron-compact-right": {},
+ "chevron-compact-up": {},
+ "chevron-contract": {},
+ "chevron-double-down": {},
+ "chevron-double-left": {},
+ "chevron-double-right": {},
+ "chevron-double-up": {},
+ "chevron-down": {},
+ "chevron-expand": {},
+ "chevron-left": {},
+ "chevron-right": {},
+ "chevron-up": {},
+ "circle-fill": {},
+ "circle-half": {},
+ "circle-square": {},
+ "circle": {},
+ "clipboard-check": {},
+ "clipboard-data": {},
+ "clipboard-minus": {},
+ "clipboard-plus": {},
+ "clipboard-x": {},
+ "clipboard": {},
+ "clock-fill": {},
+ "clock-history": {},
+ "clock": {},
+ "cloud-arrow-down-fill": {},
+ "cloud-arrow-down": {},
+ "cloud-arrow-up-fill": {},
+ "cloud-arrow-up": {},
+ "cloud-check-fill": {},
+ "cloud-check": {},
+ "cloud-download-fill": {},
+ "cloud-download": {},
+ "cloud-drizzle-fill": {},
+ "cloud-drizzle": {},
+ "cloud-fill": {},
+ "cloud-fog-fill": {},
+ "cloud-fog": {},
+ "cloud-fog2-fill": {},
+ "cloud-fog2": {},
+ "cloud-hail-fill": {},
+ "cloud-hail": {},
+ "cloud-haze-fill": {},
+ "cloud-haze": {},
+ "cloud-haze2-fill": {},
+ "cloud-lightning-fill": {},
+ "cloud-lightning-rain-fill": {},
+ "cloud-lightning-rain": {},
+ "cloud-lightning": {},
+ "cloud-minus-fill": {},
+ "cloud-minus": {},
+ "cloud-moon-fill": {},
+ "cloud-moon": {},
+ "cloud-plus-fill": {},
+ "cloud-plus": {},
+ "cloud-rain-fill": {},
+ "cloud-rain-heavy-fill": {},
+ "cloud-rain-heavy": {},
+ "cloud-rain": {},
+ "cloud-slash-fill": {},
+ "cloud-slash": {},
+ "cloud-sleet-fill": {},
+ "cloud-sleet": {},
+ "cloud-snow-fill": {},
+ "cloud-snow": {},
+ "cloud-sun-fill": {},
+ "cloud-sun": {},
+ "cloud-upload-fill": {},
+ "cloud-upload": {},
+ "cloud": {},
+ "clouds-fill": {},
+ "clouds": {},
+ "cloudy-fill": {},
+ "cloudy": {},
+ "code-slash": {},
+ "code-square": {},
+ "code": {},
+ "collection-fill": {},
+ "collection-play-fill": {},
+ "collection-play": {},
+ "collection": {},
+ "columns-gap": {},
+ "columns": {},
+ "command": {},
+ "compass-fill": {},
+ "compass": {},
+ "cone-striped": {},
+ "cone": {},
+ "controller": {},
+ "cpu-fill": {},
+ "cpu": {},
+ "credit-card-2-back-fill": {},
+ "credit-card-2-back": {},
+ "credit-card-2-front-fill": {},
+ "credit-card-2-front": {},
+ "credit-card-fill": {},
+ "credit-card": {},
+ "crop": {},
+ "cup-fill": {},
+ "cup-straw": {},
+ "cup": {},
+ "cursor-fill": {},
+ "cursor-text": {},
+ "cursor": {},
+ "dash-circle-dotted": {},
+ "dash-circle-fill": {},
+ "dash-circle": {},
+ "dash-square-dotted": {},
+ "dash-square-fill": {},
+ "dash-square": {},
+ "dash": {},
+ "diagram-2-fill": {},
+ "diagram-2": {},
+ "diagram-3-fill": {},
+ "diagram-3": {},
+ "diamond-fill": {},
+ "diamond-half": {},
+ "diamond": {},
+ "dice-1-fill": {},
+ "dice-1": {},
+ "dice-2-fill": {},
+ "dice-2": {},
+ "dice-3-fill": {},
+ "dice-3": {},
+ "dice-4-fill": {},
+ "dice-4": {},
+ "dice-5-fill": {},
+ "dice-5": {},
+ "dice-6-fill": {},
+ "dice-6": {},
+ "disc-fill": {},
+ "disc": {},
+ "discord": {},
+ "display-fill": {},
+ "display": {},
+ "distribute-horizontal": {},
+ "distribute-vertical": {},
+ "door-closed-fill": {},
+ "door-closed": {},
+ "door-open-fill": {},
+ "door-open": {},
+ "dot": {},
+ "download": {},
+ "droplet-fill": {},
+ "droplet-half": {},
+ "droplet": {},
+ "earbuds": {},
+ "easel-fill": {},
+ "easel": {},
+ "egg-fill": {},
+ "egg-fried": {},
+ "egg": {},
+ "eject-fill": {},
+ "eject": {},
+ "emoji-angry-fill": {},
+ "emoji-angry": {},
+ "emoji-dizzy-fill": {},
+ "emoji-dizzy": {},
+ "emoji-expressionless-fill": {},
+ "emoji-expressionless": {},
+ "emoji-frown-fill": {},
+ "emoji-frown": {},
+ "emoji-heart-eyes-fill": {},
+ "emoji-heart-eyes": {},
+ "emoji-laughing-fill": {},
+ "emoji-laughing": {},
+ "emoji-neutral-fill": {},
+ "emoji-neutral": {},
+ "emoji-smile-fill": {},
+ "emoji-smile-upside-down-fill": {},
+ "emoji-smile-upside-down": {},
+ "emoji-smile": {},
+ "emoji-sunglasses-fill": {},
+ "emoji-sunglasses": {},
+ "emoji-wink-fill": {},
+ "emoji-wink": {},
+ "envelope-fill": {},
+ "envelope-open-fill": {},
+ "envelope-open": {},
+ "envelope": {},
+ "eraser-fill": {},
+ "eraser": {},
+ "exclamation-circle-fill": {},
+ "exclamation-circle": {},
+ "exclamation-diamond-fill": {},
+ "exclamation-diamond": {},
+ "exclamation-octagon-fill": {},
+ "exclamation-octagon": {},
+ "exclamation-square-fill": {},
+ "exclamation-square": {},
+ "exclamation-triangle-fill": {},
+ "exclamation-triangle": {},
+ "exclamation": {},
+ "exclude": {},
+ "eye-fill": {},
+ "eye-slash-fill": {},
+ "eye-slash": {},
+ "eye": {},
+ "eyedropper": {},
+ "eyeglasses": {},
+ "facebook": {},
+ "file-arrow-down-fill": {},
+ "file-arrow-down": {},
+ "file-arrow-up-fill": {},
+ "file-arrow-up": {},
+ "file-bar-graph-fill": {},
+ "file-bar-graph": {},
+ "file-binary-fill": {},
+ "file-binary": {},
+ "file-break-fill": {},
+ "file-break": {},
+ "file-check-fill": {},
+ "file-check": {},
+ "file-code-fill": {},
+ "file-code": {},
+ "file-diff-fill": {},
+ "file-diff": {},
+ "file-earmark-arrow-down-fill": {},
+ "file-earmark-arrow-down": {},
+ "file-earmark-arrow-up-fill": {},
+ "file-earmark-arrow-up": {},
+ "file-earmark-bar-graph-fill": {},
+ "file-earmark-bar-graph": {},
+ "file-earmark-binary-fill": {},
+ "file-earmark-binary": {},
+ "file-earmark-break-fill": {},
+ "file-earmark-break": {},
+ "file-earmark-check-fill": {},
+ "file-earmark-check": {},
+ "file-earmark-code-fill": {},
+ "file-earmark-code": {},
+ "file-earmark-diff-fill": {},
+ "file-earmark-diff": {},
+ "file-earmark-easel-fill": {},
+ "file-earmark-easel": {},
+ "file-earmark-excel-fill": {},
+ "file-earmark-excel": {},
+ "file-earmark-fill": {},
+ "file-earmark-font-fill": {},
+ "file-earmark-font": {},
+ "file-earmark-image-fill": {},
+ "file-earmark-image": {},
+ "file-earmark-lock-fill": {},
+ "file-earmark-lock": {},
+ "file-earmark-lock2-fill": {},
+ "file-earmark-lock2": {},
+ "file-earmark-medical-fill": {},
+ "file-earmark-medical": {},
+ "file-earmark-minus-fill": {},
+ "file-earmark-minus": {},
+ "file-earmark-music-fill": {},
+ "file-earmark-music": {},
+ "file-earmark-person-fill": {},
+ "file-earmark-person": {},
+ "file-earmark-play-fill": {},
+ "file-earmark-play": {},
+ "file-earmark-plus-fill": {},
+ "file-earmark-plus": {},
+ "file-earmark-post-fill": {},
+ "file-earmark-post": {},
+ "file-earmark-ppt-fill": {},
+ "file-earmark-ppt": {},
+ "file-earmark-richtext-fill": {},
+ "file-earmark-richtext": {},
+ "file-earmark-ruled-fill": {},
+ "file-earmark-ruled": {},
+ "file-earmark-slides-fill": {},
+ "file-earmark-slides": {},
+ "file-earmark-spreadsheet-fill": {},
+ "file-earmark-spreadsheet": {},
+ "file-earmark-text-fill": {},
+ "file-earmark-text": {},
+ "file-earmark-word-fill": {},
+ "file-earmark-word": {},
+ "file-earmark-x-fill": {},
+ "file-earmark-x": {},
+ "file-earmark-zip-fill": {},
+ "file-earmark-zip": {},
+ "file-earmark": {},
+ "file-easel-fill": {},
+ "file-easel": {},
+ "file-excel-fill": {},
+ "file-excel": {},
+ "file-fill": {},
+ "file-font-fill": {},
+ "file-font": {},
+ "file-image-fill": {},
+ "file-image": {},
+ "file-lock-fill": {},
+ "file-lock": {},
+ "file-lock2-fill": {},
+ "file-lock2": {},
+ "file-medical-fill": {},
+ "file-medical": {},
+ "file-minus-fill": {},
+ "file-minus": {},
+ "file-music-fill": {},
+ "file-music": {},
+ "file-person-fill": {},
+ "file-person": {},
+ "file-play-fill": {},
+ "file-play": {},
+ "file-plus-fill": {},
+ "file-plus": {},
+ "file-post-fill": {},
+ "file-post": {},
+ "file-ppt-fill": {},
+ "file-ppt": {},
+ "file-richtext-fill": {},
+ "file-richtext": {},
+ "file-ruled-fill": {},
+ "file-ruled": {},
+ "file-slides-fill": {},
+ "file-slides": {},
+ "file-spreadsheet-fill": {},
+ "file-spreadsheet": {},
+ "file-text-fill": {},
+ "file-text": {},
+ "file-word-fill": {},
+ "file-word": {},
+ "file-x-fill": {},
+ "file-x": {},
+ "file-zip-fill": {},
+ "file-zip": {},
+ "file": {},
+ "files-alt": {},
+ "files": {},
+ "film": {},
+ "filter-circle-fill": {},
+ "filter-circle": {},
+ "filter-left": {},
+ "filter-right": {},
+ "filter-square-fill": {},
+ "filter-square": {},
+ "filter": {},
+ "flag-fill": {},
+ "flag": {},
+ "flower1": {},
+ "flower2": {},
+ "flower3": {},
+ "folder-check": {},
+ "folder-fill": {},
+ "folder-minus": {},
+ "folder-plus": {},
+ "folder-symlink-fill": {},
+ "folder-symlink": {},
+ "folder-x": {},
+ "folder": {},
+ "folder2-open": {},
+ "folder2": {},
+ "fonts": {},
+ "forward-fill": {},
+ "forward": {},
+ "front": {},
+ "fullscreen-exit": {},
+ "fullscreen": {},
+ "funnel-fill": {},
+ "funnel": {},
+ "gear-fill": {},
+ "gear-wide-connected": {},
+ "gear-wide": {},
+ "gear": {},
+ "gem": {},
+ "geo-alt-fill": {},
+ "geo-alt": {},
+ "geo-fill": {},
+ "geo": {},
+ "gift-fill": {},
+ "gift": {},
+ "github": {},
+ "globe": {},
+ "globe2": {},
+ "google": {},
+ "graph-down": {},
+ "graph-up": {},
+ "grid-1x2-fill": {},
+ "grid-1x2": {},
+ "grid-3x2-gap-fill": {},
+ "grid-3x2-gap": {},
+ "grid-3x2": {},
+ "grid-3x3-gap-fill": {},
+ "grid-3x3-gap": {},
+ "grid-3x3": {},
+ "grid-fill": {},
+ "grid": {},
+ "grip-horizontal": {},
+ "grip-vertical": {},
+ "hammer": {},
+ "hand-index-fill": {},
+ "hand-index-thumb-fill": {},
+ "hand-index-thumb": {},
+ "hand-index": {},
+ "hand-thumbs-down-fill": {},
+ "hand-thumbs-down": {},
+ "hand-thumbs-up-fill": {},
+ "hand-thumbs-up": {},
+ "handbag-fill": {},
+ "handbag": {},
+ "hash": {},
+ "hdd-fill": {},
+ "hdd-network-fill": {},
+ "hdd-network": {},
+ "hdd-rack-fill": {},
+ "hdd-rack": {},
+ "hdd-stack-fill": {},
+ "hdd-stack": {},
+ "hdd": {},
+ "headphones": {},
+ "headset": {},
+ "heart-fill": {},
+ "heart-half": {},
+ "heart": {},
+ "heptagon-fill": {},
+ "heptagon-half": {},
+ "heptagon": {},
+ "hexagon-fill": {},
+ "hexagon-half": {},
+ "hexagon": {},
+ "hourglass-bottom": {},
+ "hourglass-split": {},
+ "hourglass-top": {},
+ "hourglass": {},
+ "house-door-fill": {},
+ "house-door": {},
+ "house-fill": {},
+ "house": {},
+ "hr": {},
+ "hurricane": {},
+ "image-alt": {},
+ "image-fill": {},
+ "image": {},
+ "images": {},
+ "inbox-fill": {},
+ "inbox": {},
+ "inboxes-fill": {},
+ "inboxes": {},
+ "info-circle-fill": {},
+ "info-circle": {},
+ "info-square-fill": {},
+ "info-square": {},
+ "info": {},
+ "input-cursor-text": {},
+ "input-cursor": {},
+ "instagram": {},
+ "intersect": {},
+ "journal-album": {},
+ "journal-arrow-down": {},
+ "journal-arrow-up": {},
+ "journal-bookmark-fill": {},
+ "journal-bookmark": {},
+ "journal-check": {},
+ "journal-code": {},
+ "journal-medical": {},
+ "journal-minus": {},
+ "journal-plus": {},
+ "journal-richtext": {},
+ "journal-text": {},
+ "journal-x": {},
+ "journal": {},
+ "journals": {},
+ "joystick": {},
+ "justify-left": {},
+ "justify-right": {},
+ "justify": {},
+ "kanban-fill": {},
+ "kanban": {},
+ "key-fill": {},
+ "key": {},
+ "keyboard-fill": {},
+ "keyboard": {},
+ "ladder": {},
+ "lamp-fill": {},
+ "lamp": {},
+ "laptop-fill": {},
+ "laptop": {},
+ "layer-backward": {},
+ "layer-forward": {},
+ "layers-fill": {},
+ "layers-half": {},
+ "layers": {},
+ "layout-sidebar-inset-reverse": {},
+ "layout-sidebar-inset": {},
+ "layout-sidebar-reverse": {},
+ "layout-sidebar": {},
+ "layout-split": {},
+ "layout-text-sidebar-reverse": {},
+ "layout-text-sidebar": {},
+ "layout-text-window-reverse": {},
+ "layout-text-window": {},
+ "layout-three-columns": {},
+ "layout-wtf": {},
+ "life-preserver": {},
+ "lightbulb-fill": {},
+ "lightbulb-off-fill": {},
+ "lightbulb-off": {},
+ "lightbulb": {},
+ "lightning-charge-fill": {},
+ "lightning-charge": {},
+ "lightning-fill": {},
+ "lightning": {},
+ "link-45deg": {},
+ "link": {},
+ "linkedin": {},
+ "list-check": {},
+ "list-nested": {},
+ "list-ol": {},
+ "list-stars": {},
+ "list-task": {},
+ "list-ul": {},
+ "list": {},
+ "lock-fill": {},
+ "lock": {},
+ "mailbox": {},
+ "mailbox2": {},
+ "map-fill": {},
+ "map": {},
+ "markdown-fill": {},
+ "markdown": {},
+ "mask": {},
+ "megaphone-fill": {},
+ "megaphone": {},
+ "menu-app-fill": {},
+ "menu-app": {},
+ "menu-button-fill": {},
+ "menu-button-wide-fill": {},
+ "menu-button-wide": {},
+ "menu-button": {},
+ "menu-down": {},
+ "menu-up": {},
+ "mic-fill": {},
+ "mic-mute-fill": {},
+ "mic-mute": {},
+ "mic": {},
+ "minecart-loaded": {},
+ "minecart": {},
+ "moisture": {},
+ "moon-fill": {},
+ "moon-stars-fill": {},
+ "moon-stars": {},
+ "moon": {},
+ "mouse-fill": {},
+ "mouse": {},
+ "mouse2-fill": {},
+ "mouse2": {},
+ "mouse3-fill": {},
+ "mouse3": {},
+ "music-note-beamed": {},
+ "music-note-list": {},
+ "music-note": {},
+ "music-player-fill": {},
+ "music-player": {},
+ "newspaper": {},
+ "node-minus-fill": {},
+ "node-minus": {},
+ "node-plus-fill": {},
+ "node-plus": {},
+ "nut-fill": {},
+ "nut": {},
+ "octagon-fill": {},
+ "octagon-half": {},
+ "octagon": {},
+ "option": {},
+ "outlet": {},
+ "paint-bucket": {},
+ "palette-fill": {},
+ "palette": {},
+ "palette2": {},
+ "paperclip": {},
+ "paragraph": {},
+ "patch-check-fill": {},
+ "patch-check": {},
+ "patch-exclamation-fill": {},
+ "patch-exclamation": {},
+ "patch-minus-fill": {},
+ "patch-minus": {},
+ "patch-plus-fill": {},
+ "patch-plus": {},
+ "patch-question-fill": {},
+ "patch-question": {},
+ "pause-btn-fill": {},
+ "pause-btn": {},
+ "pause-circle-fill": {},
+ "pause-circle": {},
+ "pause-fill": {},
+ "pause": {},
+ "peace-fill": {},
+ "peace": {},
+ "pen-fill": {},
+ "pen": {},
+ "pencil-fill": {},
+ "pencil-square": {},
+ "pencil": {},
+ "pentagon-fill": {},
+ "pentagon-half": {},
+ "pentagon": {},
+ "people-fill": {},
+ "people": {},
+ "percent": {},
+ "person-badge-fill": {},
+ "person-badge": {},
+ "person-bounding-box": {},
+ "person-check-fill": {},
+ "person-check": {},
+ "person-circle": {},
+ "person-dash-fill": {},
+ "person-dash": {},
+ "person-fill": {},
+ "person-lines-fill": {},
+ "person-plus-fill": {},
+ "person-plus": {},
+ "person-square": {},
+ "person-x-fill": {},
+ "person-x": {},
+ "person": {},
+ "phone-fill": {},
+ "phone-landscape-fill": {},
+ "phone-landscape": {},
+ "phone-vibrate-fill": {},
+ "phone-vibrate": {},
+ "phone": {},
+ "pie-chart-fill": {},
+ "pie-chart": {},
+ "pin-angle-fill": {},
+ "pin-angle": {},
+ "pin-fill": {},
+ "pin": {},
+ "pip-fill": {},
+ "pip": {},
+ "play-btn-fill": {},
+ "play-btn": {},
+ "play-circle-fill": {},
+ "play-circle": {},
+ "play-fill": {},
+ "play": {},
+ "plug-fill": {},
+ "plug": {},
+ "plus-circle-dotted": {},
+ "plus-circle-fill": {},
+ "plus-circle": {},
+ "plus-square-dotted": {},
+ "plus-square-fill": {},
+ "plus-square": {},
+ "plus": {},
+ "power": {},
+ "printer-fill": {},
+ "printer": {},
+ "puzzle-fill": {},
+ "puzzle": {},
+ "question-circle-fill": {},
+ "question-circle": {},
+ "question-diamond-fill": {},
+ "question-diamond": {},
+ "question-octagon-fill": {},
+ "question-octagon": {},
+ "question-square-fill": {},
+ "question-square": {},
+ "question": {},
+ "rainbow": {},
+ "receipt-cutoff": {},
+ "receipt": {},
+ "reception-0": {},
+ "reception-1": {},
+ "reception-2": {},
+ "reception-3": {},
+ "reception-4": {},
+ "record-btn-fill": {},
+ "record-btn": {},
+ "record-circle-fill": {},
+ "record-circle": {},
+ "record-fill": {},
+ "record": {},
+ "record2-fill": {},
+ "record2": {},
+ "reply-all-fill": {},
+ "reply-all": {},
+ "reply-fill": {},
+ "reply": {},
+ "rss-fill": {},
+ "rss": {},
+ "rulers": {},
+ "save-fill": {},
+ "save": {},
+ "save2-fill": {},
+ "save2": {},
+ "scissors": {},
+ "screwdriver": {},
+ "search": {},
+ "segmented-nav": {},
+ "server": {},
+ "share-fill": {},
+ "share": {},
+ "shield-check": {},
+ "shield-exclamation": {},
+ "shield-fill-check": {},
+ "shield-fill-exclamation": {},
+ "shield-fill-minus": {},
+ "shield-fill-plus": {},
+ "shield-fill-x": {},
+ "shield-fill": {},
+ "shield-lock-fill": {},
+ "shield-lock": {},
+ "shield-minus": {},
+ "shield-plus": {},
+ "shield-shaded": {},
+ "shield-slash-fill": {},
+ "shield-slash": {},
+ "shield-x": {},
+ "shield": {},
+ "shift-fill": {},
+ "shift": {},
+ "shop-window": {},
+ "shop": {},
+ "shuffle": {},
+ "signpost-2-fill": {},
+ "signpost-2": {},
+ "signpost-fill": {},
+ "signpost-split-fill": {},
+ "signpost-split": {},
+ "signpost": {},
+ "sim-fill": {},
+ "sim": {},
+ "skip-backward-btn-fill": {},
+ "skip-backward-btn": {},
+ "skip-backward-circle-fill": {},
+ "skip-backward-circle": {},
+ "skip-backward-fill": {},
+ "skip-backward": {},
+ "skip-end-btn-fill": {},
+ "skip-end-btn": {},
+ "skip-end-circle-fill": {},
+ "skip-end-circle": {},
+ "skip-end-fill": {},
+ "skip-end": {},
+ "skip-forward-btn-fill": {},
+ "skip-forward-btn": {},
+ "skip-forward-circle-fill": {},
+ "skip-forward-circle": {},
+ "skip-forward-fill": {},
+ "skip-forward": {},
+ "skip-start-btn-fill": {},
+ "skip-start-btn": {},
+ "skip-start-circle-fill": {},
+ "skip-start-circle": {},
+ "skip-start-fill": {},
+ "skip-start": {},
+ "slack": {},
+ "slash-circle-fill": {},
+ "slash-circle": {},
+ "slash-square-fill": {},
+ "slash-square": {},
+ "slash": {},
+ "sliders": {},
+ "smartwatch": {},
+ "snow": {},
+ "snow2": {},
+ "snow3": {},
+ "sort-alpha-down-alt": {},
+ "sort-alpha-down": {},
+ "sort-alpha-up-alt": {},
+ "sort-alpha-up": {},
+ "sort-down-alt": {},
+ "sort-down": {},
+ "sort-numeric-down-alt": {},
+ "sort-numeric-down": {},
+ "sort-numeric-up-alt": {},
+ "sort-numeric-up": {},
+ "sort-up-alt": {},
+ "sort-up": {},
+ "soundwave": {},
+ "speaker-fill": {},
+ "speaker": {},
+ "speedometer": {},
+ "speedometer2": {},
+ "spellcheck": {},
+ "square-fill": {},
+ "square-half": {},
+ "square": {},
+ "stack": {},
+ "star-fill": {},
+ "star-half": {},
+ "star": {},
+ "stars": {},
+ "stickies-fill": {},
+ "stickies": {},
+ "sticky-fill": {},
+ "sticky": {},
+ "stop-btn-fill": {},
+ "stop-btn": {},
+ "stop-circle-fill": {},
+ "stop-circle": {},
+ "stop-fill": {},
+ "stop": {},
+ "stoplights-fill": {},
+ "stoplights": {},
+ "stopwatch-fill": {},
+ "stopwatch": {},
+ "subtract": {},
+ "suit-club-fill": {},
+ "suit-club": {},
+ "suit-diamond-fill": {},
+ "suit-diamond": {},
+ "suit-heart-fill": {},
+ "suit-heart": {},
+ "suit-spade-fill": {},
+ "suit-spade": {},
+ "sun-fill": {},
+ "sun": {},
+ "sunglasses": {},
+ "sunrise-fill": {},
+ "sunrise": {},
+ "sunset-fill": {},
+ "sunset": {},
+ "symmetry-horizontal": {},
+ "symmetry-vertical": {},
+ "table": {},
+ "tablet-fill": {},
+ "tablet-landscape-fill": {},
+ "tablet-landscape": {},
+ "tablet": {},
+ "tag-fill": {},
+ "tag": {},
+ "tags-fill": {},
+ "tags": {},
+ "telegram": {},
+ "telephone-fill": {},
+ "telephone-forward-fill": {},
+ "telephone-forward": {},
+ "telephone-inbound-fill": {},
+ "telephone-inbound": {},
+ "telephone-minus-fill": {},
+ "telephone-minus": {},
+ "telephone-outbound-fill": {},
+ "telephone-outbound": {},
+ "telephone-plus-fill": {},
+ "telephone-plus": {},
+ "telephone-x-fill": {},
+ "telephone-x": {},
+ "telephone": {},
+ "terminal-fill": {},
+ "terminal": {},
+ "text-center": {},
+ "text-indent-left": {},
+ "text-indent-right": {},
+ "text-left": {},
+ "text-paragraph": {},
+ "text-right": {},
+ "textarea-resize": {},
+ "textarea-t": {},
+ "textarea": {},
+ "thermometer-half": {},
+ "thermometer-high": {},
+ "thermometer-low": {},
+ "thermometer-snow": {},
+ "thermometer-sun": {},
+ "thermometer": {},
+ "three-dots-vertical": {},
+ "three-dots": {},
+ "toggle-off": {},
+ "toggle-on": {},
+ "toggle2-off": {},
+ "toggle2-on": {},
+ "toggles": {},
+ "toggles2": {},
+ "tools": {},
+ "tornado": {},
+ "trash-fill": {},
+ "trash": {},
+ "trash2-fill": {},
+ "trash2": {},
+ "tree-fill": {},
+ "tree": {},
+ "triangle-fill": {},
+ "triangle-half": {},
+ "triangle": {},
+ "trophy-fill": {},
+ "trophy": {},
+ "tropical-storm": {},
+ "truck-flatbed": {},
+ "truck": {},
+ "tsunami": {},
+ "tv-fill": {},
+ "tv": {},
+ "twitch": {},
+ "twitter": {},
+ "type-bold": {},
+ "type-h1": {},
+ "type-h2": {},
+ "type-h3": {},
+ "type-italic": {},
+ "type-strikethrough": {},
+ "type-underline": {},
+ "type": {},
+ "ui-checks-grid": {},
+ "ui-checks": {},
+ "ui-radios-grid": {},
+ "ui-radios": {},
+ "umbrella-fill": {},
+ "umbrella": {},
+ "union": {},
+ "unlock-fill": {},
+ "unlock": {},
+ "upc-scan": {},
+ "upc": {},
+ "upload": {},
+ "vector-pen": {},
+ "view-list": {},
+ "view-stacked": {},
+ "vinyl-fill": {},
+ "vinyl": {},
+ "voicemail": {},
+ "volume-down-fill": {},
+ "volume-down": {},
+ "volume-mute-fill": {},
+ "volume-mute": {},
+ "volume-off-fill": {},
+ "volume-off": {},
+ "volume-up-fill": {},
+ "volume-up": {},
+ "vr": {},
+ "wallet-fill": {},
+ "wallet": {},
+ "wallet2": {},
+ "watch": {},
+ "water": {},
+ "whatsapp": {},
+ "wifi-1": {},
+ "wifi-2": {},
+ "wifi-off": {},
+ "wifi": {},
+ "wind": {},
+ "window-dock": {},
+ "window-sidebar": {},
+ "window": {},
+ "wrench": {},
+ "x-circle-fill": {},
+ "x-circle": {},
+ "x-diamond-fill": {},
+ "x-diamond": {},
+ "x-octagon-fill": {},
+ "x-octagon": {},
+ "x-square-fill": {},
+ "x-square": {},
+ "x": {},
+ "youtube": {},
+ "zoom-in": {},
+ "zoom-out": {},
+ "bank": {},
+ "bank2": {},
+ "bell-slash-fill": {},
+ "bell-slash": {},
+ "cash-coin": {},
+ "check-lg": {},
+ "coin": {},
+ "currency-bitcoin": {},
+ "currency-dollar": {},
+ "currency-euro": {},
+ "currency-exchange": {},
+ "currency-pound": {},
+ "currency-yen": {},
+ "dash-lg": {},
+ "exclamation-lg": {},
+ "file-earmark-pdf-fill": {},
+ "file-earmark-pdf": {},
+ "file-pdf-fill": {},
+ "file-pdf": {},
+ "gender-ambiguous": {},
+ "gender-female": {},
+ "gender-male": {},
+ "gender-trans": {},
+ "headset-vr": {},
+ "info-lg": {},
+ "mastodon": {},
+ "messenger": {},
+ "piggy-bank-fill": {},
+ "piggy-bank": {},
+ "pin-map-fill": {},
+ "pin-map": {},
+ "plus-lg": {},
+ "question-lg": {},
+ "recycle": {},
+ "reddit": {},
+ "safe-fill": {},
+ "safe2-fill": {},
+ "safe2": {},
+ "sd-card-fill": {},
+ "sd-card": {},
+ "skype": {},
+ "slash-lg": {},
+ "translate": {},
+ "x-lg": {},
+ "safe": {},
+ "apple": {},
+ "microsoft": {},
+ "windows": {},
+ "behance": {},
+ "dribbble": {},
+ "line": {},
+ "medium": {},
+ "paypal": {},
+ "pinterest": {},
+ "signal": {},
+ "snapchat": {},
+ "spotify": {},
+ "stack-overflow": {},
+ "strava": {},
+ "wordpress": {},
+ "vimeo": {},
+ "activity": {},
+ "easel2-fill": {},
+ "easel2": {},
+ "easel3-fill": {},
+ "easel3": {},
+ "fan": {},
+ "fingerprint": {},
+ "graph-down-arrow": {},
+ "graph-up-arrow": {},
+ "hypnotize": {},
+ "magic": {},
+ "person-rolodex": {},
+ "person-video": {},
+ "person-video2": {},
+ "person-video3": {},
+ "person-workspace": {},
+ "radioactive": {},
+ "webcam-fill": {},
+ "webcam": {},
+ "yin-yang": {},
+ "bandaid-fill": {},
+ "bandaid": {},
+ "bluetooth": {},
+ "body-text": {},
+ "boombox": {},
+ "boxes": {},
+ "dpad-fill": {},
+ "dpad": {},
+ "ear-fill": {},
+ "ear": {},
+ "envelope-check-fill": {},
+ "envelope-check": {},
+ "envelope-dash-fill": {},
+ "envelope-dash": {},
+ "envelope-exclamation-fill": {},
+ "envelope-exclamation": {},
+ "envelope-plus-fill": {},
+ "envelope-plus": {},
+ "envelope-slash-fill": {},
+ "envelope-slash": {},
+ "envelope-x-fill": {},
+ "envelope-x": {},
+ "explicit-fill": {},
+ "explicit": {},
+ "git": {},
+ "infinity": {},
+ "list-columns-reverse": {},
+ "list-columns": {},
+ "meta": {},
+ "nintendo-switch": {},
+ "pc-display-horizontal": {},
+ "pc-display": {},
+ "pc-horizontal": {},
+ "pc": {},
+ "playstation": {},
+ "plus-slash-minus": {},
+ "projector-fill": {},
+ "projector": {},
+ "qr-code-scan": {},
+ "qr-code": {},
+ "quora": {},
+ "quote": {},
+ "robot": {},
+ "send-check-fill": {},
+ "send-check": {},
+ "send-dash-fill": {},
+ "send-dash": {},
+ "send-exclamation-fill": {},
+ "send-exclamation": {},
+ "send-fill": {},
+ "send-plus-fill": {},
+ "send-plus": {},
+ "send-slash-fill": {},
+ "send-slash": {},
+ "send-x-fill": {},
+ "send-x": {},
+ "send": {},
+ "steam": {},
+ "terminal-dash": {},
+ "terminal-plus": {},
+ "terminal-split": {},
+ "ticket-detailed-fill": {},
+ "ticket-detailed": {},
+ "ticket-fill": {},
+ "ticket-perforated-fill": {},
+ "ticket-perforated": {},
+ "ticket": {},
+ "tiktok": {},
+ "window-dash": {},
+ "window-desktop": {},
+ "window-fullscreen": {},
+ "window-plus": {},
+ "window-split": {},
+ "window-stack": {},
+ "window-x": {},
+ "xbox": {},
+ "ethernet": {},
+ "hdmi-fill": {},
+ "hdmi": {},
+ "usb-c-fill": {},
+ "usb-c": {},
+ "usb-fill": {},
+ "usb-plug-fill": {},
+ "usb-plug": {},
+ "usb-symbol": {},
+ "usb": {},
+ "boombox-fill": {},
+ "displayport": {},
+ "gpu-card": {},
+ "memory": {},
+ "modem-fill": {},
+ "modem": {},
+ "motherboard-fill": {},
+ "motherboard": {},
+ "optical-audio-fill": {},
+ "optical-audio": {},
+ "pci-card": {},
+ "router-fill": {},
+ "router": {},
+ "thunderbolt-fill": {},
+ "thunderbolt": {},
+ "usb-drive-fill": {},
+ "usb-drive": {},
+ "usb-micro-fill": {},
+ "usb-micro": {},
+ "usb-mini-fill": {},
+ "usb-mini": {},
+ "cloud-haze2": {},
+ "device-hdd-fill": {},
+ "device-hdd": {},
+ "device-ssd-fill": {},
+ "device-ssd": {},
+ "displayport-fill": {},
+ "mortarboard-fill": {},
+ "mortarboard": {},
+ "terminal-x": {},
+ "arrow-through-heart-fill": {},
+ "arrow-through-heart": {},
+ "badge-sd-fill": {},
+ "badge-sd": {},
+ "bag-heart-fill": {},
+ "bag-heart": {},
+ "balloon-fill": {},
+ "balloon-heart-fill": {},
+ "balloon-heart": {},
+ "balloon": {},
+ "box2-fill": {},
+ "box2-heart-fill": {},
+ "box2-heart": {},
+ "box2": {},
+ "braces-asterisk": {},
+ "calendar-heart-fill": {},
+ "calendar-heart": {},
+ "calendar2-heart-fill": {},
+ "calendar2-heart": {},
+ "chat-heart-fill": {},
+ "chat-heart": {},
+ "chat-left-heart-fill": {},
+ "chat-left-heart": {},
+ "chat-right-heart-fill": {},
+ "chat-right-heart": {},
+ "chat-square-heart-fill": {},
+ "chat-square-heart": {},
+ "clipboard-check-fill": {},
+ "clipboard-data-fill": {},
+ "clipboard-fill": {},
+ "clipboard-heart-fill": {},
+ "clipboard-heart": {},
+ "clipboard-minus-fill": {},
+ "clipboard-plus-fill": {},
+ "clipboard-pulse": {},
+ "clipboard-x-fill": {},
+ "clipboard2-check-fill": {},
+ "clipboard2-check": {},
+ "clipboard2-data-fill": {},
+ "clipboard2-data": {},
+ "clipboard2-fill": {},
+ "clipboard2-heart-fill": {},
+ "clipboard2-heart": {},
+ "clipboard2-minus-fill": {},
+ "clipboard2-minus": {},
+ "clipboard2-plus-fill": {},
+ "clipboard2-plus": {},
+ "clipboard2-pulse-fill": {},
+ "clipboard2-pulse": {},
+ "clipboard2-x-fill": {},
+ "clipboard2-x": {},
+ "clipboard2": {},
+ "emoji-kiss-fill": {},
+ "emoji-kiss": {},
+ "envelope-heart-fill": {},
+ "envelope-heart": {},
+ "envelope-open-heart-fill": {},
+ "envelope-open-heart": {},
+ "envelope-paper-fill": {},
+ "envelope-paper-heart-fill": {},
+ "envelope-paper-heart": {},
+ "envelope-paper": {},
+ "filetype-aac": {},
+ "filetype-ai": {},
+ "filetype-bmp": {},
+ "filetype-cs": {},
+ "filetype-css": {},
+ "filetype-csv": {},
+ "filetype-doc": {},
+ "filetype-docx": {},
+ "filetype-exe": {},
+ "filetype-gif": {},
+ "filetype-heic": {},
+ "filetype-html": {},
+ "filetype-java": {},
+ "filetype-jpg": {},
+ "filetype-js": {},
+ "filetype-jsx": {},
+ "filetype-key": {},
+ "filetype-m4p": {},
+ "filetype-md": {},
+ "filetype-mdx": {},
+ "filetype-mov": {},
+ "filetype-mp3": {},
+ "filetype-mp4": {},
+ "filetype-otf": {},
+ "filetype-pdf": {},
+ "filetype-php": {},
+ "filetype-png": {},
+ "filetype-ppt": {},
+ "filetype-psd": {},
+ "filetype-py": {},
+ "filetype-raw": {},
+ "filetype-rb": {},
+ "filetype-sass": {},
+ "filetype-scss": {},
+ "filetype-sh": {},
+ "filetype-svg": {},
+ "filetype-tiff": {},
+ "filetype-tsx": {},
+ "filetype-ttf": {},
+ "filetype-txt": {},
+ "filetype-wav": {},
+ "filetype-woff": {},
+ "filetype-xls": {},
+ "filetype-xml": {},
+ "filetype-yml": {},
+ "heart-arrow": {},
+ "heart-pulse-fill": {},
+ "heart-pulse": {},
+ "heartbreak-fill": {},
+ "heartbreak": {},
+ "hearts": {},
+ "hospital-fill": {},
+ "hospital": {},
+ "house-heart-fill": {},
+ "house-heart": {},
+ "incognito": {},
+ "magnet-fill": {},
+ "magnet": {},
+ "person-heart": {},
+ "person-hearts": {},
+ "phone-flip": {},
+ "plugin": {},
+ "postage-fill": {},
+ "postage-heart-fill": {},
+ "postage-heart": {},
+ "postage": {},
+ "postcard-fill": {},
+ "postcard-heart-fill": {},
+ "postcard-heart": {},
+ "postcard": {},
+ "search-heart-fill": {},
+ "search-heart": {},
+ "sliders2-vertical": {},
+ "sliders2": {},
+ "trash3-fill": {},
+ "trash3": {},
+ "valentine": {},
+ "valentine2": {},
+ "wrench-adjustable-circle-fill": {},
+ "wrench-adjustable-circle": {},
+ "wrench-adjustable": {},
+ "filetype-json": {},
+ "filetype-pptx": {},
+ "filetype-xlsx": {},
+ "1-circle-fill": {},
+ "1-circle": {},
+ "1-square-fill": {},
+ "1-square": {},
+ "2-circle-fill": {},
+ "2-circle": {},
+ "2-square-fill": {},
+ "2-square": {},
+ "3-circle-fill": {},
+ "3-circle": {},
+ "3-square-fill": {},
+ "3-square": {},
+ "4-circle-fill": {},
+ "4-circle": {},
+ "4-square-fill": {},
+ "4-square": {},
+ "5-circle-fill": {},
+ "5-circle": {},
+ "5-square-fill": {},
+ "5-square": {},
+ "6-circle-fill": {},
+ "6-circle": {},
+ "6-square-fill": {},
+ "6-square": {},
+ "7-circle-fill": {},
+ "7-circle": {},
+ "7-square-fill": {},
+ "7-square": {},
+ "8-circle-fill": {},
+ "8-circle": {},
+ "8-square-fill": {},
+ "8-square": {},
+ "9-circle-fill": {},
+ "9-circle": {},
+ "9-square-fill": {},
+ "9-square": {},
+ "airplane-engines-fill": {},
+ "airplane-engines": {},
+ "airplane-fill": {},
+ "airplane": {},
+ "alexa": {},
+ "alipay": {},
+ "android": {},
+ "android2": {},
+ "box-fill": {},
+ "box-seam-fill": {},
+ "browser-chrome": {},
+ "browser-edge": {},
+ "browser-firefox": {},
+ "browser-safari": {},
+ "c-circle-fill": {},
+ "c-circle": {},
+ "c-square-fill": {},
+ "c-square": {},
+ "capsule-pill": {},
+ "capsule": {},
+ "car-front-fill": {},
+ "car-front": {},
+ "cassette-fill": {},
+ "cassette": {},
+ "cc-circle-fill": {},
+ "cc-circle": {},
+ "cc-square-fill": {},
+ "cc-square": {},
+ "cup-hot-fill": {},
+ "cup-hot": {},
+ "currency-rupee": {},
+ "dropbox": {},
+ "escape": {},
+ "fast-forward-btn-fill": {},
+ "fast-forward-btn": {},
+ "fast-forward-circle-fill": {},
+ "fast-forward-circle": {},
+ "fast-forward-fill": {},
+ "fast-forward": {},
+ "filetype-sql": {},
+ "fire": {},
+ "google-play": {},
+ "h-circle-fill": {},
+ "h-circle": {},
+ "h-square-fill": {},
+ "h-square": {},
+ "indent": {},
+ "lungs-fill": {},
+ "lungs": {},
+ "microsoft-teams": {},
+ "p-circle-fill": {},
+ "p-circle": {},
+ "p-square-fill": {},
+ "p-square": {},
+ "pass-fill": {},
+ "pass": {},
+ "prescription": {},
+ "prescription2": {},
+ "r-circle-fill": {},
+ "r-circle": {},
+ "r-square-fill": {},
+ "r-square": {},
+ "repeat-1": {},
+ "repeat": {},
+ "rewind-btn-fill": {},
+ "rewind-btn": {},
+ "rewind-circle-fill": {},
+ "rewind-circle": {},
+ "rewind-fill": {},
+ "rewind": {},
+ "train-freight-front-fill": {},
+ "train-freight-front": {},
+ "train-front-fill": {},
+ "train-front": {},
+ "train-lightrail-front-fill": {},
+ "train-lightrail-front": {},
+ "truck-front-fill": {},
+ "truck-front": {},
+ "ubuntu": {},
+ "unindent": {},
+ "unity": {},
+ "universal-access-circle": {},
+ "universal-access": {},
+ "virus": {},
+ "virus2": {},
+ "wechat": {},
+ "yelp": {},
+ "sign-stop-fill": {},
+ "sign-stop-lights-fill": {},
+ "sign-stop-lights": {},
+ "sign-stop": {},
+ "sign-turn-left-fill": {},
+ "sign-turn-left": {},
+ "sign-turn-right-fill": {},
+ "sign-turn-right": {},
+ "sign-turn-slight-left-fill": {},
+ "sign-turn-slight-left": {},
+ "sign-turn-slight-right-fill": {},
+ "sign-turn-slight-right": {},
+ "sign-yield-fill": {},
+ "sign-yield": {},
+ "ev-station-fill": {},
+ "ev-station": {},
+ "fuel-pump-diesel-fill": {},
+ "fuel-pump-diesel": {},
+ "fuel-pump-fill": {},
+ "fuel-pump": {},
+ "0-circle-fill": {},
+ "0-circle": {},
+ "0-square-fill": {},
+ "0-square": {},
+ "rocket-fill": {},
+ "rocket-takeoff-fill": {},
+ "rocket-takeoff": {},
+ "rocket": {},
+ "stripe": {},
+ "subscript": {},
+ "superscript": {},
+ "trello": {},
+ "envelope-at-fill": {},
+ "envelope-at": {},
+ "regex": {},
+ "text-wrap": {},
+ "sign-dead-end-fill": {},
+ "sign-dead-end": {},
+ "sign-do-not-enter-fill": {},
+ "sign-do-not-enter": {},
+ "sign-intersection-fill": {},
+ "sign-intersection-side-fill": {},
+ "sign-intersection-side": {},
+ "sign-intersection-t-fill": {},
+ "sign-intersection-t": {},
+ "sign-intersection-y-fill": {},
+ "sign-intersection-y": {},
+ "sign-intersection": {},
+ "sign-merge-left-fill": {},
+ "sign-merge-left": {},
+ "sign-merge-right-fill": {},
+ "sign-merge-right": {},
+ "sign-no-left-turn-fill": {},
+ "sign-no-left-turn": {},
+ "sign-no-parking-fill": {},
+ "sign-no-parking": {},
+ "sign-no-right-turn-fill": {},
+ "sign-no-right-turn": {},
+ "sign-railroad-fill": {},
+ "sign-railroad": {},
+ "building-add": {},
+ "building-check": {},
+ "building-dash": {},
+ "building-down": {},
+ "building-exclamation": {},
+ "building-fill-add": {},
+ "building-fill-check": {},
+ "building-fill-dash": {},
+ "building-fill-down": {},
+ "building-fill-exclamation": {},
+ "building-fill-gear": {},
+ "building-fill-lock": {},
+ "building-fill-slash": {},
+ "building-fill-up": {},
+ "building-fill-x": {},
+ "building-fill": {},
+ "building-gear": {},
+ "building-lock": {},
+ "building-slash": {},
+ "building-up": {},
+ "building-x": {},
+ "buildings-fill": {},
+ "buildings": {},
+ "bus-front-fill": {},
+ "bus-front": {},
+ "ev-front-fill": {},
+ "ev-front": {},
+ "globe-americas": {},
+ "globe-asia-australia": {},
+ "globe-central-south-asia": {},
+ "globe-europe-africa": {},
+ "house-add-fill": {},
+ "house-add": {},
+ "house-check-fill": {},
+ "house-check": {},
+ "house-dash-fill": {},
+ "house-dash": {},
+ "house-down-fill": {},
+ "house-down": {},
+ "house-exclamation-fill": {},
+ "house-exclamation": {},
+ "house-gear-fill": {},
+ "house-gear": {},
+ "house-lock-fill": {},
+ "house-lock": {},
+ "house-slash-fill": {},
+ "house-slash": {},
+ "house-up-fill": {},
+ "house-up": {},
+ "house-x-fill": {},
+ "house-x": {},
+ "person-add": {},
+ "person-down": {},
+ "person-exclamation": {},
+ "person-fill-add": {},
+ "person-fill-check": {},
+ "person-fill-dash": {},
+ "person-fill-down": {},
+ "person-fill-exclamation": {},
+ "person-fill-gear": {},
+ "person-fill-lock": {},
+ "person-fill-slash": {},
+ "person-fill-up": {},
+ "person-fill-x": {},
+ "person-gear": {},
+ "person-lock": {},
+ "person-slash": {},
+ "person-up": {},
+ "scooter": {},
+ "taxi-front-fill": {},
+ "taxi-front": {},
+ "amd": {},
+ "database-add": {},
+ "database-check": {},
+ "database-dash": {},
+ "database-down": {},
+ "database-exclamation": {},
+ "database-fill-add": {},
+ "database-fill-check": {},
+ "database-fill-dash": {},
+ "database-fill-down": {},
+ "database-fill-exclamation": {},
+ "database-fill-gear": {},
+ "database-fill-lock": {},
+ "database-fill-slash": {},
+ "database-fill-up": {},
+ "database-fill-x": {},
+ "database-fill": {},
+ "database-gear": {},
+ "database-lock": {},
+ "database-slash": {},
+ "database-up": {},
+ "database-x": {},
+ "database": {},
+ "houses-fill": {},
+ "houses": {},
+ "nvidia": {},
+ "person-vcard-fill": {},
+ "person-vcard": {},
+ "sina-weibo": {},
+ "tencent-qq": {},
+ "wikipedia": {},
}
// IsValid returns true if the input is the name of a Bootstrap icon.
func IsValid(name string) bool {
- for i := range icons {
- if icons[i] == name {
- return true
- }
- }
- return false
+ _, ok := icons[name]
+ return ok
}
diff --git a/backend/main.go b/backend/main.go
index 008e80f..c17679e 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -11,6 +11,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"github.com/davidbyttow/govips/v2/vips"
+ "github.com/getsentry/sentry-go"
"github.com/go-chi/render"
_ "github.com/joho/godotenv/autoload"
"github.com/urfave/cli/v2"
@@ -23,6 +24,19 @@ var Command = &cli.Command{
}
func run(c *cli.Context) error {
+ // initialize sentry
+ if dsn := os.Getenv("SENTRY_DSN"); dsn != "" {
+ // We don't need to check the error here--it's fine if no DSN is set.
+ _ = sentry.Init(sentry.ClientOptions{
+ Dsn: dsn,
+ Debug: os.Getenv("DEBUG") == "true",
+ Release: server.Tag,
+ EnableTracing: os.Getenv("SENTRY_TRACING") == "true",
+ TracesSampleRate: 0.05,
+ ProfilesSampleRate: 0.05,
+ })
+ }
+
// set vips log level to WARN, else it will spam logs on info level
vips.LoggingSettings(nil, vips.LogLevelWarning)
diff --git a/backend/routes.go b/backend/routes.go
index f755929..406e05f 100644
--- a/backend/routes.go
+++ b/backend/routes.go
@@ -2,7 +2,6 @@ package backend
import (
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/auth"
- "codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/bot"
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/member"
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/meta"
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/mod"
@@ -21,7 +20,6 @@ func mountRoutes(s *server.Server) {
auth.Mount(s, r)
user.Mount(s, r)
member.Mount(s, r)
- bot.Mount(s, r)
meta.Mount(s, r)
mod.Mount(s, r)
})
diff --git a/backend/routes/v1/auth/discord.go b/backend/routes/v1/auth/discord.go
index f22518c..b8a72f3 100644
--- a/backend/routes/v1/auth/discord.go
+++ b/backend/routes/v1/auth/discord.go
@@ -11,6 +11,7 @@ import (
"emperror.dev/errors"
"github.com/bwmarrin/discordgo"
"github.com/go-chi/render"
+ "github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4"
"github.com/rs/xid"
"golang.org/x/oauth2"
@@ -61,7 +62,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
// if the state can't be validated, return
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
if err != nil {
- return err
+ return errors.Wrap(err, "validating state")
}
return server.APIError{Code: server.ErrInvalidState}
@@ -79,7 +80,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
dg, _ := discordgo.New(token.Type() + " " + token.AccessToken)
du, err := dg.User("@me")
if err != nil {
- return err
+ return errors.Wrap(err, "getting discord user")
}
u, err := s.DB.DiscordUser(ctx, du.ID)
@@ -90,7 +91,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
err = s.saveUndeleteToken(ctx, u.ID, token)
if err != nil {
log.Errorf("saving undelete token: %v", err)
- return err
+ return errors.Wrap(err, "saving undelete token")
}
render.JSON(w, r, discordCallbackResponse{
@@ -114,7 +115,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
tokenID := xid.New()
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
if err != nil {
- return err
+ return errors.Wrap(err, "creating token")
}
// save token to database
@@ -137,7 +138,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
return nil
} else if err != db.ErrUserNotFound { // internal error
- return err
+ return errors.Wrap(err, "getting user")
}
// no user found, so save a ticket + save their Discord info in Redis
@@ -145,7 +146,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
err = s.DB.SetJSON(ctx, "discord:"+ticket, du, "EX", "600")
if err != nil {
log.Errorf("setting Discord user for ticket %q: %v", ticket, err)
- return err
+ return errors.Wrap(err, "caching discord user for ticket")
}
render.JSON(w, r, discordCallbackResponse{
@@ -278,7 +279,7 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
if err != nil {
- return err
+ return errors.Wrap(err, "checking if username is taken")
}
if !valid {
return server.APIError{Code: server.ErrInvalidUsername}
@@ -291,7 +292,12 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
du := new(discordgo.User)
err = s.DB.GetJSON(ctx, "discord:"+req.Ticket, &du)
diff --git a/backend/routes/v1/auth/fedi_mastodon.go b/backend/routes/v1/auth/fedi_mastodon.go
index 1cae10b..f69a2b3 100644
--- a/backend/routes/v1/auth/fedi_mastodon.go
+++ b/backend/routes/v1/auth/fedi_mastodon.go
@@ -11,6 +11,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
+ "github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4"
"github.com/rs/xid"
)
@@ -54,7 +55,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
// if the state can't be validated, return
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
if err != nil {
- return err
+ return errors.Wrap(err, "validating state")
}
return server.APIError{Code: server.ErrInvalidState}
@@ -111,7 +112,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
err = s.saveUndeleteToken(ctx, u.ID, token)
if err != nil {
log.Errorf("saving undelete token: %v", err)
- return err
+ return errors.Wrap(err, "saving undelete token")
}
render.JSON(w, r, fediCallbackResponse{
@@ -135,7 +136,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
tokenID := xid.New()
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
if err != nil {
- return err
+ return errors.Wrap(err, "creating token")
}
// save token to database
@@ -158,7 +159,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
return nil
} else if err != db.ErrUserNotFound { // internal error
- return err
+ return errors.Wrap(err, "getting user")
}
// no user found, so save a ticket + save their Mastodon info in Redis
@@ -166,7 +167,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
err = s.DB.SetJSON(ctx, "mastodon:"+ticket, mu, "EX", "600")
if err != nil {
log.Errorf("setting mastoAPI user for ticket %q: %v", ticket, err)
- return err
+ return errors.Wrap(err, "setting user for ticket")
}
render.JSON(w, r, fediCallbackResponse{
@@ -306,7 +307,7 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
if err != nil {
- return err
+ return errors.Wrap(err, "checking if username is taken")
}
if !valid {
return server.APIError{Code: server.ErrInvalidUsername}
@@ -319,7 +320,12 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
mu := new(partialMastodonAccount)
err = s.DB.GetJSON(ctx, "mastodon:"+req.Ticket, &mu)
diff --git a/backend/routes/v1/auth/fedi_misskey.go b/backend/routes/v1/auth/fedi_misskey.go
index 864b852..d162f77 100644
--- a/backend/routes/v1/auth/fedi_misskey.go
+++ b/backend/routes/v1/auth/fedi_misskey.go
@@ -12,6 +12,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
+ "github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4"
"github.com/rs/xid"
)
@@ -90,7 +91,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
err = s.saveUndeleteToken(ctx, u.ID, token)
if err != nil {
log.Errorf("saving undelete token: %v", err)
- return err
+ return errors.Wrap(err, "saving undelete token")
}
render.JSON(w, r, fediCallbackResponse{
@@ -114,7 +115,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
tokenID := xid.New()
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
if err != nil {
- return err
+ return errors.Wrap(err, "creating token")
}
// save token to database
@@ -137,7 +138,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
return nil
} else if err != db.ErrUserNotFound { // internal error
- return err
+ return errors.Wrap(err, "getting user")
}
// no user found, so save a ticket + save their Misskey info in Redis
@@ -145,7 +146,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
err = s.DB.SetJSON(ctx, "misskey:"+ticket, mu.User, "EX", "600")
if err != nil {
log.Errorf("setting misskey user for ticket %q: %v", ticket, err)
- return err
+ return errors.Wrap(err, "setting user for ticket")
}
render.JSON(w, r, fediCallbackResponse{
@@ -234,7 +235,7 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error {
valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
if err != nil {
- return err
+ return errors.Wrap(err, "checking if username is taken")
}
if !valid {
return server.APIError{Code: server.ErrInvalidUsername}
@@ -247,7 +248,12 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
mu := new(partialMisskeyAccount)
err = s.DB.GetJSON(ctx, "misskey:"+req.Ticket, &mu)
diff --git a/backend/routes/v1/auth/google.go b/backend/routes/v1/auth/google.go
index 182c8a6..7904889 100644
--- a/backend/routes/v1/auth/google.go
+++ b/backend/routes/v1/auth/google.go
@@ -10,6 +10,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
+ "github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4"
"github.com/rs/xid"
"golang.org/x/oauth2"
@@ -60,7 +61,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
// if the state can't be validated, return
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
if err != nil {
- return err
+ return errors.Wrap(err, "validating state")
}
return server.APIError{Code: server.ErrInvalidState}
@@ -109,7 +110,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
err = s.saveUndeleteToken(ctx, u.ID, token)
if err != nil {
log.Errorf("saving undelete token: %v", err)
- return err
+ return errors.Wrap(err, "saving undelete token")
}
render.JSON(w, r, googleCallbackResponse{
@@ -133,7 +134,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
tokenID := xid.New()
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
if err != nil {
- return err
+ return errors.Wrap(err, "creating token")
}
// save token to database
@@ -156,7 +157,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
return nil
} else if err != db.ErrUserNotFound { // internal error
- return err
+ return errors.Wrap(err, "getting user")
}
// no user found, so save a ticket + save their Google info in Redis
@@ -164,7 +165,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
err = s.DB.SetJSON(ctx, "google:"+ticket, partialGoogleUser{ID: googleID, Email: googleUsername}, "EX", "600")
if err != nil {
log.Errorf("setting Google user for ticket %q: %v", ticket, err)
- return err
+ return errors.Wrap(err, "setting user for ticket")
}
render.JSON(w, r, googleCallbackResponse{
@@ -281,7 +282,7 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error {
valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
if err != nil {
- return err
+ return errors.Wrap(err, "checking if username is taken")
}
if !valid {
return server.APIError{Code: server.ErrInvalidUsername}
@@ -294,7 +295,12 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
gu := new(partialGoogleUser)
err = s.DB.GetJSON(ctx, "google:"+req.Ticket, &gu)
diff --git a/backend/routes/v1/auth/routes.go b/backend/routes/v1/auth/routes.go
index 103d43e..2e1a724 100644
--- a/backend/routes/v1/auth/routes.go
+++ b/backend/routes/v1/auth/routes.go
@@ -185,7 +185,7 @@ func (s *Server) oauthURLs(w http.ResponseWriter, r *http.Request) error {
if googleOAuthConfig.ClientID != "" {
googleCfg := googleOAuthConfig
googleCfg.RedirectURL = req.CallbackDomain + "/auth/login/google"
- resp.Google = googleCfg.AuthCodeURL(state)
+ resp.Google = googleCfg.AuthCodeURL(state) + "&prompt=select_account"
}
render.JSON(w, r, resp)
diff --git a/backend/routes/v1/auth/tokens.go b/backend/routes/v1/auth/tokens.go
index e48662d..0f8b8b8 100644
--- a/backend/routes/v1/auth/tokens.go
+++ b/backend/routes/v1/auth/tokens.go
@@ -5,9 +5,11 @@ import (
"time"
"codeberg.org/pronounscc/pronouns.cc/backend/db"
+ "codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
+ "github.com/jackc/pgx/v5"
"github.com/rs/xid"
)
@@ -63,7 +65,12 @@ func (s *Server) deleteToken(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
err = s.DB.InvalidateAllTokens(ctx, tx, claims.UserID)
if err != nil {
diff --git a/backend/routes/v1/auth/tumblr.go b/backend/routes/v1/auth/tumblr.go
index d35781a..3463c9d 100644
--- a/backend/routes/v1/auth/tumblr.go
+++ b/backend/routes/v1/auth/tumblr.go
@@ -12,6 +12,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
+ "github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4"
"github.com/rs/xid"
"golang.org/x/oauth2"
@@ -77,7 +78,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
// if the state can't be validated, return
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
if err != nil {
- return err
+ return errors.Wrap(err, "validating state")
}
return server.APIError{Code: server.ErrInvalidState}
@@ -142,7 +143,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
err = s.saveUndeleteToken(ctx, u.ID, token)
if err != nil {
log.Errorf("saving undelete token: %v", err)
- return err
+ return errors.Wrap(err, "saving undelete token")
}
render.JSON(w, r, tumblrCallbackResponse{
@@ -166,7 +167,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
tokenID := xid.New()
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
if err != nil {
- return err
+ return errors.Wrap(err, "creating token")
}
// save token to database
@@ -189,7 +190,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
return nil
} else if err != db.ErrUserNotFound { // internal error
- return err
+ return errors.Wrap(err, "getting user")
}
// no user found, so save a ticket + save their Tumblr info in Redis
@@ -197,7 +198,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
err = s.DB.SetJSON(ctx, "tumblr:"+ticket, tumblrUserInfo{ID: tumblrID, Name: tumblrName}, "EX", "600")
if err != nil {
log.Errorf("setting Tumblr user for ticket %q: %v", ticket, err)
- return err
+ return errors.Wrap(err, "setting user for ticket")
}
render.JSON(w, r, tumblrCallbackResponse{
@@ -314,7 +315,7 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error {
valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
if err != nil {
- return err
+ return errors.Wrap(err, "checking if username is taken")
}
if !valid {
return server.APIError{Code: server.ErrInvalidUsername}
@@ -327,7 +328,12 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
tui := new(tumblrUserInfo)
err = s.DB.GetJSON(ctx, "tumblr:"+req.Ticket, &tui)
diff --git a/backend/routes/v1/bot/bot.go b/backend/routes/v1/bot/bot.go
deleted file mode 100644
index 1fa8f4c..0000000
--- a/backend/routes/v1/bot/bot.go
+++ /dev/null
@@ -1,183 +0,0 @@
-package bot
-
-import (
- "crypto/ed25519"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "net/http"
- "os"
-
- "codeberg.org/pronounscc/pronouns.cc/backend/db"
- "codeberg.org/pronounscc/pronouns.cc/backend/log"
- "codeberg.org/pronounscc/pronouns.cc/backend/server"
- "github.com/bwmarrin/discordgo"
- "github.com/go-chi/chi/v5"
- "github.com/go-chi/render"
-)
-
-type Bot struct {
- *server.Server
-
- publicKey ed25519.PublicKey
- baseURL string
-}
-
-func (bot *Bot) UserAvatarURL(u db.User) string {
- if u.Avatar == nil {
- return ""
- }
-
- return bot.baseURL + "/media/users/" + u.ID.String() + "/" + *u.Avatar + ".webp"
-}
-
-func Mount(srv *server.Server, r chi.Router) {
- publicKey, err := hex.DecodeString(os.Getenv("DISCORD_PUBLIC_KEY"))
- if err != nil {
- return
- }
-
- b := &Bot{
- Server: srv,
- publicKey: publicKey,
- baseURL: os.Getenv("BASE_URL"),
- }
-
- r.HandleFunc("/interactions", b.handle)
-}
-
-func (bot *Bot) handle(w http.ResponseWriter, r *http.Request) {
- if !discordgo.VerifyInteraction(r, bot.publicKey) {
- http.Error(w, "Forbidden", http.StatusForbidden)
- return
- }
-
- var ev *discordgo.InteractionCreate
-
- if err := json.NewDecoder(r.Body).Decode(&ev); err != nil {
- http.Error(w, "Bad Request", http.StatusBadRequest)
- }
-
- // we can always respond to ping with pong
- if ev.Type == discordgo.InteractionPing {
- log.Debug("received ping interaction")
- render.JSON(w, r, discordgo.InteractionResponse{
- Type: discordgo.InteractionResponsePong,
- })
- return
- }
-
- if ev.Type != discordgo.InteractionApplicationCommand {
- return
- }
- data := ev.ApplicationCommandData()
-
- switch data.Name {
- case "Show user's pronouns":
- bot.userPronouns(w, r, ev)
- case "Show author's pronouns":
- }
-}
-
-func (bot *Bot) userPronouns(w http.ResponseWriter, r *http.Request, ev *discordgo.InteractionCreate) {
- ctx := r.Context()
-
- var du *discordgo.User
- for _, user := range ev.ApplicationCommandData().Resolved.Users {
- du = user
- break
- }
- if du == nil {
- return
- }
-
- u, err := bot.DB.DiscordUser(ctx, du.ID)
- if err != nil {
- if err == db.ErrUserNotFound {
- respond(w, r, &discordgo.MessageEmbed{
- Description: du.String() + " does not have any pronouns set.",
- })
- return
- }
-
- log.Errorf("getting discord user: %v", err)
- return
- }
-
- avatarURL := du.AvatarURL("")
- if url := bot.UserAvatarURL(u); url != "" {
- avatarURL = url
- }
- name := u.Username
- if u.DisplayName != nil {
- name = fmt.Sprintf("%s (%s)", *u.DisplayName, u.Username)
- }
- url := bot.baseURL
- if url != "" {
- url += "/@" + u.Username
- }
-
- e := &discordgo.MessageEmbed{
- Author: &discordgo.MessageEmbedAuthor{
- Name: name,
- IconURL: avatarURL,
- URL: url,
- },
- }
-
- if u.Bio != nil {
- e.Fields = append(e.Fields, &discordgo.MessageEmbedField{
- Name: "Bio",
- Value: *u.Bio,
- })
- }
-
- fields, err := bot.DB.UserFields(ctx, u.ID)
- if err != nil {
- respond(w, r, e)
-
- log.Errorf("getting user fields: %v", err)
- return
- }
-
- for _, field := range fields {
- var favs []db.FieldEntry
-
- for _, e := range field.Entries {
- if e.Status == "favourite" {
- favs = append(favs, e)
- }
- }
-
- if len(favs) == 0 {
- continue
- }
-
- var value string
- for _, fav := range favs {
- if len(fav.Value) > 500 {
- break
- }
-
- value += fav.Value + "\n"
- }
-
- e.Fields = append(e.Fields, &discordgo.MessageEmbedField{
- Name: field.Name,
- Value: value,
- Inline: true,
- })
- }
-
- respond(w, r, e)
-}
-
-func respond(w http.ResponseWriter, r *http.Request, embeds ...*discordgo.MessageEmbed) {
- render.JSON(w, r, discordgo.InteractionResponse{
- Type: discordgo.InteractionResponseChannelMessageWithSource,
- Data: &discordgo.InteractionResponseData{
- Embeds: embeds,
- Flags: discordgo.MessageFlagsEphemeral,
- },
- })
-}
diff --git a/backend/routes/v1/member/create_member.go b/backend/routes/v1/member/create_member.go
index e26ab74..288aafc 100644
--- a/backend/routes/v1/member/create_member.go
+++ b/backend/routes/v1/member/create_member.go
@@ -11,6 +11,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
+ "github.com/jackc/pgx/v5"
)
type CreateMemberRequest struct {
@@ -119,7 +120,12 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
if err != nil {
return errors.Wrap(err, "starting transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
m, err := s.DB.CreateMember(ctx, tx, claims.UserID, cmr.Name, cmr.DisplayName, cmr.Bio, cmr.Links)
if err != nil {
@@ -127,14 +133,14 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
return server.APIError{Code: server.ErrMemberNameInUse}
}
- return err
+ return errors.Wrap(err, "creating member")
}
// set names, pronouns, fields
err = s.DB.SetMemberNamesPronouns(ctx, tx, m.ID, db.NotNull(cmr.Names), db.NotNull(cmr.Pronouns))
if err != nil {
log.Errorf("setting names and pronouns for member %v: %v", m.ID, err)
- return err
+ return errors.Wrap(err, "setting names/pronouns")
}
m.Names = cmr.Names
m.Pronouns = cmr.Pronouns
@@ -142,7 +148,7 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
err = s.DB.SetMemberFields(ctx, tx, m.ID, cmr.Fields)
if err != nil {
log.Errorf("setting fields for member %v: %v", m.ID, err)
- return err
+ return errors.Wrap(err, "setting fields")
}
if cmr.Avatar != "" {
@@ -161,13 +167,13 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
}
log.Errorf("converting member avatar: %v", err)
- return err
+ return errors.Wrap(err, "converting avatar")
}
hash, err := s.DB.WriteMemberAvatar(ctx, m.ID, webp, jpg)
if err != nil {
log.Errorf("uploading member avatar: %v", err)
- return err
+ return errors.Wrap(err, "uploading avatar")
}
err = tx.QueryRow(ctx, "UPDATE members SET avatar = $1 WHERE id = $2", hash, m.ID).Scan(&m.Avatar)
@@ -180,7 +186,7 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
err = s.DB.UpdateActiveTime(ctx, tx, claims.UserID)
if err != nil {
log.Errorf("updating last active time for user %v: %v", claims.UserID, err)
- return err
+ return errors.Wrap(err, "updating last active time")
}
err = tx.Commit(ctx)
diff --git a/backend/routes/v1/member/delete_member.go b/backend/routes/v1/member/delete_member.go
index 4c2abde..5c8e68a 100644
--- a/backend/routes/v1/member/delete_member.go
+++ b/backend/routes/v1/member/delete_member.go
@@ -66,7 +66,7 @@ func (s *Server) deleteMember(w http.ResponseWriter, r *http.Request) (err error
err = s.DB.UpdateActiveTime(ctx, s.DB, claims.UserID)
if err != nil {
log.Errorf("updating last active time for user %v: %v", claims.UserID, err)
- return err
+ return errors.Wrap(err, "updating last active time")
}
render.NoContent(w, r)
diff --git a/backend/routes/v1/member/get_member.go b/backend/routes/v1/member/get_member.go
index 2a7d5e6..ffea5d5 100644
--- a/backend/routes/v1/member/get_member.go
+++ b/backend/routes/v1/member/get_member.go
@@ -105,7 +105,7 @@ func (s *Server) getMember(w http.ResponseWriter, r *http.Request) (err error) {
u, err := s.DB.User(ctx, m.UserID)
if err != nil {
- return err
+ return errors.Wrap(err, "getting user")
}
if u.DeletedAt != nil {
@@ -119,12 +119,12 @@ func (s *Server) getMember(w http.ResponseWriter, r *http.Request) (err error) {
fields, err := s.DB.MemberFields(ctx, m.ID)
if err != nil {
- return err
+ return errors.Wrap(err, "getting member fields")
}
flags, err := s.DB.MemberFlags(ctx, m.ID)
if err != nil {
- return err
+ return errors.Wrap(err, "getting member flags")
}
render.JSON(w, r, dbMemberToMember(u, m, fields, flags, isOwnMember))
@@ -159,12 +159,12 @@ func (s *Server) getUserMember(w http.ResponseWriter, r *http.Request) error {
fields, err := s.DB.MemberFields(ctx, m.ID)
if err != nil {
- return err
+ return errors.Wrap(err, "getting member fields")
}
flags, err := s.DB.MemberFlags(ctx, m.ID)
if err != nil {
- return err
+ return errors.Wrap(err, "getting member flags")
}
render.JSON(w, r, dbMemberToMember(u, m, fields, flags, isOwnMember))
@@ -189,12 +189,12 @@ func (s *Server) getMeMember(w http.ResponseWriter, r *http.Request) error {
fields, err := s.DB.MemberFields(ctx, m.ID)
if err != nil {
- return err
+ return errors.Wrap(err, "getting member fields")
}
flags, err := s.DB.MemberFlags(ctx, m.ID)
if err != nil {
- return err
+ return errors.Wrap(err, "getting member flags")
}
render.JSON(w, r, dbMemberToMember(u, m, fields, flags, true))
diff --git a/backend/routes/v1/member/get_members.go b/backend/routes/v1/member/get_members.go
index 6dba566..2442d39 100644
--- a/backend/routes/v1/member/get_members.go
+++ b/backend/routes/v1/member/get_members.go
@@ -6,6 +6,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/common"
"codeberg.org/pronounscc/pronouns.cc/backend/db"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
+ "emperror.dev/errors"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/rs/xid"
@@ -74,7 +75,7 @@ func (s *Server) getUserMembers(w http.ResponseWriter, r *http.Request) error {
ms, err := s.DB.UserMembers(ctx, u.ID, isSelf)
if err != nil {
- return err
+ return errors.Wrap(err, "getting members")
}
render.JSON(w, r, membersToMemberList(ms, isSelf))
@@ -87,7 +88,7 @@ func (s *Server) getMeMembers(w http.ResponseWriter, r *http.Request) error {
ms, err := s.DB.UserMembers(ctx, claims.UserID, true)
if err != nil {
- return err
+ return errors.Wrap(err, "getting members")
}
render.JSON(w, r, membersToMemberList(ms, true))
diff --git a/backend/routes/v1/member/patch_member.go b/backend/routes/v1/member/patch_member.go
index 8d56a87..c26cf37 100644
--- a/backend/routes/v1/member/patch_member.go
+++ b/backend/routes/v1/member/patch_member.go
@@ -13,6 +13,7 @@ import (
"emperror.dev/errors"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
+ "github.com/jackc/pgx/v5"
"github.com/rs/xid"
)
@@ -220,13 +221,13 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
}
log.Errorf("converting member avatar: %v", err)
- return err
+ return errors.Wrap(err, "converting member avatar")
}
hash, err := s.DB.WriteMemberAvatar(ctx, m.ID, webp, jpg)
if err != nil {
log.Errorf("uploading member avatar: %v", err)
- return err
+ return errors.Wrap(err, "writing member avatar")
}
avatarHash = &hash
@@ -244,9 +245,14 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
tx, err := s.DB.Begin(ctx)
if err != nil {
log.Errorf("creating transaction: %v", err)
- return err
+ return errors.Wrap(err, "creating transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
m, err = s.DB.UpdateMember(ctx, tx, m.ID, req.Name, req.DisplayName, req.Bio, req.Unlisted, req.Links, avatarHash)
if err != nil {
@@ -275,7 +281,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
err = s.DB.SetMemberNamesPronouns(ctx, tx, m.ID, names, pronouns)
if err != nil {
log.Errorf("setting names for member %v: %v", m.ID, err)
- return err
+ return errors.Wrap(err, "setting names/pronouns")
}
m.Names = names
m.Pronouns = pronouns
@@ -286,14 +292,14 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
err = s.DB.SetMemberFields(ctx, tx, m.ID, *req.Fields)
if err != nil {
log.Errorf("setting fields for member %v: %v", m.ID, err)
- return err
+ return errors.Wrap(err, "setting fields")
}
fields = *req.Fields
} else {
fields, err = s.DB.MemberFields(ctx, m.ID)
if err != nil {
log.Errorf("getting fields for member %v: %v", m.ID, err)
- return err
+ return errors.Wrap(err, "getting fields")
}
}
@@ -306,7 +312,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
}
log.Errorf("updating flags for member %v: %v", m.ID, err)
- return err
+ return errors.Wrap(err, "updating flags")
}
}
@@ -314,20 +320,20 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
err = s.DB.UpdateActiveTime(ctx, tx, claims.UserID)
if err != nil {
log.Errorf("updating last active time for user %v: %v", claims.UserID, err)
- return err
+ return errors.Wrap(err, "updating last active time")
}
err = tx.Commit(ctx)
if err != nil {
log.Errorf("committing transaction: %v", err)
- return err
+ return errors.Wrap(err, "committing transaction")
}
// get flags to return (we need to return full flag objects, not the array of IDs in the request body)
flags, err := s.DB.MemberFlags(ctx, m.ID)
if err != nil {
log.Errorf("getting user flags: %v", err)
- return err
+ return errors.Wrap(err, "getting flags")
}
// echo the updated member back on success
diff --git a/backend/routes/v1/mod/resolve_report.go b/backend/routes/v1/mod/resolve_report.go
index 77e4b8d..c36cb86 100644
--- a/backend/routes/v1/mod/resolve_report.go
+++ b/backend/routes/v1/mod/resolve_report.go
@@ -10,6 +10,7 @@ import (
"emperror.dev/errors"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
+ "github.com/jackc/pgx/v5"
)
type resolveReportRequest struct {
@@ -43,7 +44,12 @@ func (s *Server) resolveReport(w http.ResponseWriter, r *http.Request) error {
log.Errorf("creating transaction: %v", err)
return errors.Wrap(err, "creating transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
report, err := s.DB.Report(ctx, tx, id)
if err != nil {
diff --git a/backend/routes/v1/user/delete_user.go b/backend/routes/v1/user/delete_user.go
index 1523978..1132f05 100644
--- a/backend/routes/v1/user/delete_user.go
+++ b/backend/routes/v1/user/delete_user.go
@@ -3,9 +3,11 @@ package user
import (
"net/http"
+ "codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
+ "github.com/jackc/pgx/v5"
)
func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) error {
@@ -20,7 +22,12 @@ func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "creating transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
err = s.DB.DeleteUser(ctx, tx, claims.UserID, true, "")
if err != nil {
diff --git a/backend/routes/v1/user/export.go b/backend/routes/v1/user/export.go
index 2dde011..189b3bb 100644
--- a/backend/routes/v1/user/export.go
+++ b/backend/routes/v1/user/export.go
@@ -7,6 +7,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/db"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
+ "emperror.dev/errors"
"github.com/go-chi/render"
)
@@ -71,7 +72,7 @@ func (s *Server) getExport(w http.ResponseWriter, r *http.Request) error {
}
log.Errorf("getting export for user %v: %v", claims.UserID, err)
- return err
+ return errors.Wrap(err, "getting export")
}
render.JSON(w, r, dataExportResponse{
diff --git a/backend/routes/v1/user/flags.go b/backend/routes/v1/user/flags.go
index b9c3ced..c7b6ad7 100644
--- a/backend/routes/v1/user/flags.go
+++ b/backend/routes/v1/user/flags.go
@@ -13,6 +13,7 @@ import (
"emperror.dev/errors"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
+ "github.com/jackc/pgx/v5"
"github.com/rs/xid"
)
@@ -80,7 +81,12 @@ func (s *Server) postUserFlag(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "starting transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
flag, err := s.DB.CreateFlag(ctx, tx, claims.UserID, req.Name, req.Description)
if err != nil {
@@ -192,7 +198,12 @@ func (s *Server) patchUserFlag(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
flag, err = s.DB.EditFlag(ctx, tx, flag.ID, req.Name, req.Description, nil)
if err != nil {
diff --git a/backend/routes/v1/user/get_user.go b/backend/routes/v1/user/get_user.go
index af43a4b..297c469 100644
--- a/backend/routes/v1/user/get_user.go
+++ b/backend/routes/v1/user/get_user.go
@@ -8,6 +8,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/db"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
+ "emperror.dev/errors"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/rs/xid"
@@ -146,7 +147,7 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) (err error) {
}
} else if err != nil {
log.Errorf("Error getting user by username: %v", err)
- return err
+ return errors.Wrap(err, "getting user")
}
}
@@ -162,13 +163,13 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) (err error) {
fields, err := s.DB.UserFields(ctx, u.ID)
if err != nil {
log.Errorf("Error getting user fields: %v", err)
- return err
+ return errors.Wrap(err, "getting fields")
}
flags, err := s.DB.UserFlags(ctx, u.ID)
if err != nil {
log.Errorf("getting user flags: %v", err)
- return err
+ return errors.Wrap(err, "getting flags")
}
var members []db.Member
@@ -176,7 +177,7 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) (err error) {
members, err = s.DB.UserMembers(ctx, u.ID, isSelf)
if err != nil {
log.Errorf("Error getting user members: %v", err)
- return err
+ return errors.Wrap(err, "getting user members")
}
}
@@ -191,25 +192,25 @@ func (s *Server) getMeUser(w http.ResponseWriter, r *http.Request) error {
u, err := s.DB.User(ctx, claims.UserID)
if err != nil {
log.Errorf("Error getting user: %v", err)
- return err
+ return errors.Wrap(err, "getting users")
}
fields, err := s.DB.UserFields(ctx, u.ID)
if err != nil {
log.Errorf("Error getting user fields: %v", err)
- return err
+ return errors.Wrap(err, "getting fields")
}
members, err := s.DB.UserMembers(ctx, u.ID, true)
if err != nil {
log.Errorf("Error getting user members: %v", err)
- return err
+ return errors.Wrap(err, "getting members")
}
flags, err := s.DB.UserFlags(ctx, u.ID)
if err != nil {
log.Errorf("getting user flags: %v", err)
- return err
+ return errors.Wrap(err, "getting flags")
}
render.JSON(w, r, GetMeResponse{
diff --git a/backend/routes/v1/user/patch_user.go b/backend/routes/v1/user/patch_user.go
index fc465b1..6e4f6ab 100644
--- a/backend/routes/v1/user/patch_user.go
+++ b/backend/routes/v1/user/patch_user.go
@@ -12,6 +12,7 @@ import (
"emperror.dev/errors"
"github.com/go-chi/render"
"github.com/google/uuid"
+ "github.com/jackc/pgx/v5"
"github.com/rs/xid"
)
@@ -195,13 +196,13 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
}
log.Errorf("converting user avatar: %v", err)
- return err
+ return errors.Wrap(err, "converting avatar")
}
hash, err := s.DB.WriteUserAvatar(ctx, claims.UserID, webp, jpg)
if err != nil {
log.Errorf("uploading user avatar: %v", err)
- return err
+ return errors.Wrap(err, "uploading avatar")
}
avatarHash = &hash
@@ -219,9 +220,14 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
tx, err := s.DB.Begin(ctx)
if err != nil {
log.Errorf("creating transaction: %v", err)
- return err
+ return errors.Wrap(err, "creating transaction")
}
- defer tx.Rollback(ctx)
+ defer func() {
+ err := tx.Rollback(ctx)
+ if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
+ log.Error("rolling back transaction:", err)
+ }
+ }()
// update username
if req.Username != nil && *req.Username != u.Username {
@@ -243,7 +249,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
u, err = s.DB.UpdateUser(ctx, tx, claims.UserID, req.DisplayName, req.Bio, req.MemberTitle, req.ListPrivate, req.Links, avatarHash, req.Timezone, req.CustomPreferences)
if err != nil && errors.Cause(err) != db.ErrNothingToUpdate {
log.Errorf("updating user: %v", err)
- return err
+ return errors.Wrap(err, "updating user")
}
if req.Names != nil || req.Pronouns != nil {
@@ -260,7 +266,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
err = s.DB.SetUserNamesPronouns(ctx, tx, claims.UserID, names, pronouns)
if err != nil {
log.Errorf("setting names for member %v: %v", claims.UserID, err)
- return err
+ return errors.Wrap(err, "setting names/pronouns")
}
u.Names = names
u.Pronouns = pronouns
@@ -271,14 +277,14 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
err = s.DB.SetUserFields(ctx, tx, claims.UserID, *req.Fields)
if err != nil {
log.Errorf("setting fields for user %v: %v", claims.UserID, err)
- return err
+ return errors.Wrap(err, "setting fields")
}
fields = *req.Fields
} else {
fields, err = s.DB.UserFields(ctx, claims.UserID)
if err != nil {
log.Errorf("getting fields for user %v: %v", claims.UserID, err)
- return err
+ return errors.Wrap(err, "getting fields")
}
}
@@ -291,7 +297,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
}
log.Errorf("updating flags for user %v: %v", claims.UserID, err)
- return err
+ return errors.Wrap(err, "updating flags")
}
}
@@ -299,13 +305,13 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
err = s.DB.UpdateActiveTime(ctx, tx, claims.UserID)
if err != nil {
log.Errorf("updating last active time for user %v: %v", claims.UserID, err)
- return err
+ return errors.Wrap(err, "updating last active time")
}
err = tx.Commit(ctx)
if err != nil {
log.Errorf("committing transaction: %v", err)
- return err
+ return errors.Wrap(err, "committing transaction")
}
// get fedi instance name if the user has a linked fedi account
@@ -321,7 +327,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
flags, err := s.DB.UserFlags(ctx, u.ID)
if err != nil {
log.Errorf("getting user flags: %v", err)
- return err
+ return errors.Wrap(err, "getting flags")
}
// echo the updated user back on success
diff --git a/backend/routes/v2/user/get_settings.go b/backend/routes/v2/user/get_settings.go
index d618831..096c0e0 100644
--- a/backend/routes/v2/user/get_settings.go
+++ b/backend/routes/v2/user/get_settings.go
@@ -5,6 +5,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
+ "emperror.dev/errors"
"github.com/go-chi/render"
)
@@ -13,7 +14,7 @@ func (s *Server) GetSettings(w http.ResponseWriter, r *http.Request) (err error)
u, err := s.DB.User(r.Context(), claims.UserID)
if err != nil {
log.Errorf("getting user: %v", err)
- return err
+ return errors.Wrap(err, "getting user")
}
render.JSON(w, r, u.Settings)
diff --git a/backend/server/errors.go b/backend/server/errors.go
index b4b8b07..063e773 100644
--- a/backend/server/errors.go
+++ b/backend/server/errors.go
@@ -1,10 +1,14 @@
package server
import (
+ "context"
"fmt"
"net/http"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
+ "emperror.dev/errors"
+ "github.com/getsentry/sentry-go"
+ "github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
@@ -12,6 +16,11 @@ import (
// The inner HandlerFunc additionally returns an error.
func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
+ hub := sentry.GetHubFromContext(r.Context())
+ if hub == nil {
+ hub = sentry.CurrentHub().Clone()
+ }
+
err := hn(w, r)
if err != nil {
// if the function returned an API error, just render that verbatim
@@ -24,10 +33,20 @@ func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.Han
return
}
- // otherwise, we log the error and return an internal server error message
- log.Errorf("error in http handler: %v", err)
+ rctx := chi.RouteContext(r.Context())
+ hub.ConfigureScope(func(scope *sentry.Scope) {
+ scope.SetTag("method", rctx.RouteMethod)
+ scope.SetTag("path", rctx.RoutePattern())
+ })
- apiErr := APIError{Code: ErrInternalServerError}
+ var eventID *sentry.EventID = nil
+ if isExpectedError(err) {
+ log.Infof("expected error in handler for %v %v, ignoring", rctx.RouteMethod, rctx.RoutePattern())
+ } else {
+ log.Errorf("error in handler for %v %v: %v", rctx.RouteMethod, rctx.RoutePattern(), err)
+ eventID = hub.CaptureException(err)
+ }
+ apiErr := APIError{ID: eventID, Code: ErrInternalServerError}
apiErr.prepare()
render.Status(r, apiErr.Status)
@@ -36,12 +55,17 @@ func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.Han
}
}
+func isExpectedError(err error) bool {
+ return errors.Is(err, context.Canceled)
+}
+
// APIError is an object returned by the API when an error occurs.
// It implements the error interface and can be returned by handlers.
type APIError struct {
- Code int `json:"code"`
- Message string `json:"message,omitempty"`
- Details string `json:"details,omitempty"`
+ Code int `json:"code"`
+ ID *sentry.EventID `json:"id,omitempty"`
+ Message string `json:"message,omitempty"`
+ Details string `json:"details,omitempty"`
RatelimitReset *int `json:"ratelimit_reset,omitempty"`
diff --git a/backend/server/sentry.go b/backend/server/sentry.go
new file mode 100644
index 0000000..9641dec
--- /dev/null
+++ b/backend/server/sentry.go
@@ -0,0 +1,89 @@
+package server
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/getsentry/sentry-go"
+ "github.com/go-chi/chi/v5"
+ "github.com/go-chi/chi/v5/middleware"
+)
+
+func (s *Server) sentry(handler http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
+
+ ctx := r.Context()
+ hub := sentry.GetHubFromContext(ctx)
+ if hub == nil {
+ hub = sentry.CurrentHub().Clone()
+ ctx = sentry.SetHubOnContext(ctx, hub)
+ }
+
+ options := []sentry.SpanOption{
+ sentry.WithOpName("http.server"),
+ sentry.ContinueFromRequest(r),
+ sentry.WithTransactionSource(sentry.SourceURL),
+ }
+ // We don't mind getting an existing transaction back so we don't need to
+ // check if it is.
+ transaction := sentry.StartTransaction(ctx,
+ fmt.Sprintf("%s %s", r.Method, r.URL.Path),
+ options...,
+ )
+ defer transaction.Finish()
+ r = r.WithContext(transaction.Context())
+ hub.Scope().SetRequest(r)
+ defer recoverWithSentry(hub, r)
+ handler.ServeHTTP(ww, r)
+
+ transaction.Status = httpStatusToSentryStatus(ww.Status())
+ rctx := chi.RouteContext(r.Context())
+ transaction.Name = rctx.RouteMethod + " " + rctx.RoutePattern()
+ })
+}
+
+func recoverWithSentry(hub *sentry.Hub, r *http.Request) {
+ if err := recover(); err != nil {
+ hub.RecoverWithContext(
+ context.WithValue(r.Context(), sentry.RequestContextKey, r),
+ err,
+ )
+ }
+}
+
+func httpStatusToSentryStatus(status int) sentry.SpanStatus {
+ // c.f. https://develop.sentry.dev/sdk/event-payloads/span/
+
+ if status >= 200 && status < 400 {
+ return sentry.SpanStatusOK
+ }
+
+ switch status {
+ case 499:
+ return sentry.SpanStatusCanceled
+ case 500:
+ return sentry.SpanStatusInternalError
+ case 400:
+ return sentry.SpanStatusInvalidArgument
+ case 504:
+ return sentry.SpanStatusDeadlineExceeded
+ case 404:
+ return sentry.SpanStatusNotFound
+ case 409:
+ return sentry.SpanStatusAlreadyExists
+ case 403:
+ return sentry.SpanStatusPermissionDenied
+ case 429:
+ return sentry.SpanStatusResourceExhausted
+ case 501:
+ return sentry.SpanStatusUnimplemented
+ case 503:
+ return sentry.SpanStatusUnavailable
+ case 401:
+ return sentry.SpanStatusUnauthenticated
+ default:
+ return sentry.SpanStatusUnknown
+ }
+}
diff --git a/backend/server/server.go b/backend/server/server.go
index 399ead0..30cd552 100644
--- a/backend/server/server.go
+++ b/backend/server/server.go
@@ -50,6 +50,9 @@ func New() (*Server, error) {
s.Router.Use(middleware.Logger)
}
s.Router.Use(middleware.Recoverer)
+ // add Sentry tracing handler
+ s.Router.Use(s.sentry)
+
// add CORS
s.Router.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"https://*", "http://*"},
@@ -97,23 +100,23 @@ func New() (*Server, error) {
// set scopes
// users
- rateLimiter.Scope("GET", "/users/*", 60)
- rateLimiter.Scope("PATCH", "/users/@me", 10)
+ _ = rateLimiter.Scope("GET", "/users/*", 60)
+ _ = rateLimiter.Scope("PATCH", "/users/@me", 10)
// members
- rateLimiter.Scope("GET", "/users/*/members", 60)
- rateLimiter.Scope("GET", "/users/*/members/*", 60)
+ _ = rateLimiter.Scope("GET", "/users/*/members", 60)
+ _ = rateLimiter.Scope("GET", "/users/*/members/*", 60)
- rateLimiter.Scope("POST", "/members", 10)
- rateLimiter.Scope("GET", "/members/*", 60)
- rateLimiter.Scope("PATCH", "/members/*", 20)
- rateLimiter.Scope("DELETE", "/members/*", 5)
+ _ = rateLimiter.Scope("POST", "/members", 10)
+ _ = rateLimiter.Scope("GET", "/members/*", 60)
+ _ = rateLimiter.Scope("PATCH", "/members/*", 20)
+ _ = rateLimiter.Scope("DELETE", "/members/*", 5)
// auth
- rateLimiter.Scope("*", "/auth/*", 20)
- rateLimiter.Scope("*", "/auth/tokens", 10)
- rateLimiter.Scope("*", "/auth/invites", 10)
- rateLimiter.Scope("POST", "/auth/discord/*", 10)
+ _ = rateLimiter.Scope("*", "/auth/*", 20)
+ _ = rateLimiter.Scope("*", "/auth/tokens", 10)
+ _ = rateLimiter.Scope("*", "/auth/invites", 10)
+ _ = rateLimiter.Scope("POST", "/auth/discord/*", 10)
s.Router.Use(rateLimiter.Handler())
diff --git a/docs/api/endpoints/index.md b/docs/api/endpoints/index.md
index 0b14002..556f1c9 100644
--- a/docs/api/endpoints/index.md
+++ b/docs/api/endpoints/index.md
@@ -45,9 +45,10 @@ A user can set custom word preferences, which can have custom icons and tooltips
## Pride flag
-| Field | Type | Description |
-| ----------- | ------- | ------------------------------------- |
-| id | string | the flag's unique ID |
-| hash | string | the flag's [image hash](/api/#images) |
-| name | string | the flag's name |
-| description | string? | the flag's description or alt text |
+| Field | Type | Description |
+| ----------- | --------- | ------------------------------------- |
+| id | string | the flag's unique ID |
+| id_new | snowflake | the flag's unique snowflake ID |
+| hash | string | the flag's [image hash](/api/#images) |
+| name | string | the flag's name |
+| description | string? | the flag's description or alt text |
diff --git a/docs/api/endpoints/members.md b/docs/api/endpoints/members.md
index 2a41a4c..fb1438d 100644
--- a/docs/api/endpoints/members.md
+++ b/docs/api/endpoints/members.md
@@ -5,6 +5,7 @@
| Field | Type | Description |
| ------------ | ---------------------------------------------------- | --------------------------------------------------------------------------------- |
| id | string | the member's unique ID |
+| id_new | snowflake | the member's unique snowflake ID |
| sid | string | the member's 6-letter short ID |
| name | string | the member's name |
| display_name | string? | the member's display name or nickname |
@@ -23,6 +24,7 @@
| Field | Type | Description |
| ------------------ | ---------------------------------------------------- | -------------------------------------- |
| id | string | the user's unique ID |
+| id_new | snowflake | the user's unique snowflake ID |
| name | string | the user's username |
| display_name | string? | the user's display name or nickname |
| avatar | string? | the user's [avatar hash](/api/#images) |
@@ -95,18 +97,18 @@ Returns the updated [member](./members#member-object) on success.
#### Request body parameters
-| Field | Type | Description |
-| ------------------ | -------------------- | --------------------------------------------------------------------------------------------------- |
-| name | string | the member's new name. Must be unique per user, and be between 1 and 100 characters. |
-| display_name | string | the member's new display name. Must be between 1 and 100 characters |
-| bio | string | the member's new bio. Must be between 1 and 1000 characters |
-| links | string[] | the member's new profile links. Maximum 25 links, and links must be between 1 and 256 characters |
-| names | field_entry[] | the member's new preferred names |
-| pronouns | pronoun_entry[] | the member's new preferred pronouns |
-| fields | field[] | the member's new profile fields |
-| flags | string[] | the member's new flags. This must be an array of [pride flag](./#pride-flag) IDs. |
-| avatar | string | the member's new avatar. This must be a PNG, JPEG, or WebP image, encoded in base64 data URI format |
-| unlisted | bool | whether or not the member should be hidden from the member list |
+| Field | Type | Description |
+| ------------ | --------------- | ------------------------------------------------------------------------------------------------------ |
+| name | string | the member's new name. Must be unique per user, and be between 1 and 100 characters. |
+| display_name | string | the member's new display name. Must be between 1 and 100 characters |
+| bio | string | the member's new bio. Must be between 1 and 1000 characters |
+| links | string[] | the member's new profile links. Maximum 25 links, and links must be between 1 and 256 characters |
+| names | field_entry[] | the member's new preferred names |
+| pronouns | pronoun_entry[] | the member's new preferred pronouns |
+| fields | field[] | the member's new profile fields |
+| flags | string[] | the member's new flags. This must be an array of [pride flag](./#pride-flag) IDs, _not_ snowflake IDs. |
+| avatar | string | the member's new avatar. This must be a PNG, JPEG, or WebP image, encoded in base64 data URI format |
+| unlisted | bool | whether or not the member should be hidden from the member list |
### Delete member
diff --git a/docs/api/endpoints/users.md b/docs/api/endpoints/users.md
index 961a0b2..b671f3a 100644
--- a/docs/api/endpoints/users.md
+++ b/docs/api/endpoints/users.md
@@ -5,6 +5,7 @@
| Field | Type | Description |
| ------------------ | ---------------------------------------------------- | --------------------------------------------------------------------------- |
| id | string | the user's unique ID |
+| id_new | snowflake | the user's unique snowflake ID |
| sid | string | the user's 5 letter short ID |
| name | string | the user's username |
| display_name | string? | the user's display name or nickname |
@@ -45,6 +46,7 @@
| Field | Type | Description |
| ------------ | ----------------------------------- | ---------------------------------------- |
| id | string | the member's unique ID |
+| id_new | snowflake | the member's unique snowflake ID |
| sid | string | the member's 6-letter short ID |
| name | string | the member's name |
| display_name | string? | the member's display name or nickname |
@@ -89,7 +91,7 @@ Returns the updated [user](./users#user-object) object on success.
| names | field_entry[] | the user's new preferred names |
| pronouns | pronoun_entry[] | the user's new preferred pronouns |
| fields | field[] | the user's new profile fields |
-| flags | string[] | the user's new flags. This must be an array of [pride flag](./#pride-flag) IDs. |
+| flags | string[] | the user's new flags. This must be an array of [pride flag](./#pride-flag) IDs, _not_ snowflake IDs. |
| avatar | string | the user's new avatar. This must be a PNG, JPEG, or WebP image, encoded in base64 data URI format |
| timezone | string | the user's new timezone. Must be in IANA timezone database format |
| list_private | bool | whether or not the user's member list should be hidden |
diff --git a/docs/api/errors.md b/docs/api/errors.md
index afb5245..dbb6cc0 100644
--- a/docs/api/errors.md
+++ b/docs/api/errors.md
@@ -2,12 +2,13 @@
If there is an error in your request, or the server encounters an error while processing it, an error object will be returned.
-| Field | Type | Description |
-| --------------- | ------- | --------------------------------------------------------------- |
-| code | int | an [error code](./errors#error-codes) |
-| message | ?string | a human-readable description of the error |
-| details | ?string | more details about the error, most often for bad request errors |
-| ratelimit_reset | ?int | the unix time when an expired rate limit will reset |
+| Field | Type | Description |
+| --------------- | ------- | ------------------------------------------------------------------- |
+| code | int | an [error code](./errors#error-codes) |
+| id | ?string | an opaque Sentry event ID, only returned for internal server errors |
+| message | ?string | a human-readable description of the error |
+| details | ?string | more details about the error, most often for bad request errors |
+| ratelimit_reset | ?int | the unix time when an expired rate limit will reset |
### Error codes
diff --git a/docs/api/index.md b/docs/api/index.md
index 0db8486..51b9eec 100644
--- a/docs/api/index.md
+++ b/docs/api/index.md
@@ -62,19 +62,23 @@ The "type" column in tables is formatted as follows:
## IDs
-::: info
-pronouns.cc is [planning a transition](https://codeberg.org/pronounscc/pronouns.cc/issues/89)
-to [Snowflake IDs](https://en.wikipedia.org/wiki/Snowflake_ID).
-The information below pertains to the current ID format.
-:::
+### Snowflake IDs
-The API uses [xid](https://github.com/rs/xid) for unique IDs. These are always serialized as strings.
+For [multiple reasons](https://codeberg.org/pronounscc/pronouns.cc/issues/89),
+pronouns.cc is transitioning to using snowflakes for unique IDs. These will become the default in the next API version,
+but are already returned as `id_new` in the relevant objects (users, members, and flags).
+
+### xids
+
+[xid](https://github.com/rs/xid) is the previous unique ID format. These are always serialized as strings.
Although xids have timestamp information embedded in them, this is non-trivial to extract.
xids are unique across _all_ resources, they are never shared (for example, a user and a member cannot share the same ID).
+### prns.cc IDs
+
Users and members also have an additional ID type, `sid`.
These are randomly generated 5 or 6 letter strings, and are used for the prns.cc URL shortener.
-They can be rerolled once per hour.
+**These can change at any time**, as short IDs can be rerolled once per hour.
## Images
diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs
index 09dcc6a..4b0d793 100644
--- a/frontend/.eslintrc.cjs
+++ b/frontend/.eslintrc.cjs
@@ -17,4 +17,15 @@ module.exports = {
es2017: true,
node: true,
},
+ rules: {
+ "no-unused-vars": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ {
+ argsIgnorePattern: "^_",
+ destructuredArrayIgnorePattern: "^_",
+ varsIgnorePattern: "^_",
+ },
+ ],
+ },
};
diff --git a/frontend/icons.js b/frontend/icons.js
index 6b0e67f..844d640 100644
--- a/frontend/icons.js
+++ b/frontend/icons.js
@@ -16,26 +16,22 @@ writeFileSync("src/icons.ts", `const icons = ${output};\nexport default icons;`)
const goCode1 = `// Generated code. DO NOT EDIT
package icons
-var icons = [...]string{
+var icons = map[string]struct{}{
`;
const goCode2 = `}
// IsValid returns true if the input is the name of a Bootstrap icon.
func IsValid(name string) bool {
- for i := range icons {
- if icons[i] == name {
- return true
- }
- }
- return false
+ _, ok := icons[name]
+ return ok
}
`;
let goOutput = goCode1;
keys.forEach((element) => {
- goOutput += ` "${element}",\n`;
+ goOutput += ` "${element}": {},\n`;
});
goOutput += goCode2;
diff --git a/frontend/package.json b/frontend/package.json
index a460457..1628226 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,11 +12,13 @@
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
- "@sveltejs/adapter-auto": "^2.0.0",
- "@sveltejs/adapter-node": "^1.2.3",
- "@sveltejs/kit": "^1.15.0",
- "@types/luxon": "^3.2.2",
- "@types/markdown-it": "^12.2.3",
+ "@sveltejs/adapter-auto": "^3.0.0",
+ "@sveltejs/adapter-node": "^2.0.0",
+ "@sveltejs/kit": "^2.0.0",
+ "@sveltejs/vite-plugin-svelte": "^3.0.0",
+ "@sveltestrap/sveltestrap": "^6.0.5",
+ "@types/luxon": "^3.3.7",
+ "@types/markdown-it": "^13.0.7",
"@types/node": "^18.15.11",
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.57.1",
@@ -25,14 +27,13 @@
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.8.7",
- "prettier-plugin-svelte": "^2.10.0",
- "svelte": "^3.58.0",
- "svelte-check": "^3.1.4",
+ "prettier-plugin-svelte": "^2.10.1",
+ "svelte": "^4.0.0",
+ "svelte-check": "^3.4.3",
"svelte-hcaptcha": "^0.1.1",
- "sveltestrap": "^5.10.0",
"tslib": "^2.5.0",
- "typescript": "^4.9.5",
- "vite": "^4.2.1",
+ "typescript": "^5.0.0",
+ "vite": "^5.0.0",
"vite-plugin-markdown": "^2.1.0"
},
"type": "module",
@@ -41,8 +42,8 @@
"@popperjs/core": "^2.11.7",
"@sentry/node": "^7.46.0",
"base64-arraybuffer": "^1.0.2",
- "bootstrap": "5.3.0-alpha1",
- "bootstrap-icons": "^1.10.4",
+ "bootstrap": "^5.3.2",
+ "bootstrap-icons": "^1.11.2",
"jose": "^4.13.1",
"luxon": "^3.3.0",
"markdown-it": "^13.0.1",
diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts
index d4759fd..9489da5 100644
--- a/frontend/src/app.d.ts
+++ b/frontend/src/app.d.ts
@@ -16,23 +16,4 @@ declare global {
}
}
-declare module "svelte-hcaptcha" {
- import type { SvelteComponent } from "svelte";
-
- export interface HCaptchaProps {
- sitekey?: string;
- apihost?: string;
- hl?: string;
- reCaptchaCompat?: boolean;
- theme?: CaptchaTheme;
- size?: string;
- }
-
- declare class HCaptcha extends SvelteComponent {
- $$prop_def: HCaptchaProps;
- }
-
- export default HCaptcha;
-}
-
export {};
diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts
index e073554..670f914 100644
--- a/frontend/src/hooks.server.ts
+++ b/frontend/src/hooks.server.ts
@@ -2,7 +2,9 @@ import { PRIVATE_SENTRY_DSN } from "$env/static/private";
import * as Sentry from "@sentry/node";
import type { HandleServerError } from "@sveltejs/kit";
-Sentry.init({ dsn: PRIVATE_SENTRY_DSN });
+if (PRIVATE_SENTRY_DSN) {
+ Sentry.init({ dsn: PRIVATE_SENTRY_DSN });
+}
export const handleError = (({ error, event }) => {
console.log(error);
diff --git a/frontend/src/lib/api/entities.ts b/frontend/src/lib/api/entities.ts
index e245522..5aa5b2a 100644
--- a/frontend/src/lib/api/entities.ts
+++ b/frontend/src/lib/api/entities.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-unused-vars */
import { PUBLIC_BASE_URL, PUBLIC_MEDIA_URL } from "$env/static/public";
export const MAX_MEMBERS = 500;
diff --git a/frontend/src/lib/components/ActiveLink.svelte b/frontend/src/lib/components/ActiveLink.svelte
index d9bc887..969cfbd 100644
--- a/frontend/src/lib/components/ActiveLink.svelte
+++ b/frontend/src/lib/components/ActiveLink.svelte
@@ -1,5 +1,5 @@
diff --git a/frontend/src/lib/components/FallbackImage.svelte b/frontend/src/lib/components/FallbackImage.svelte
index 9572e46..18d086f 100644
--- a/frontend/src/lib/components/FallbackImage.svelte
+++ b/frontend/src/lib/components/FallbackImage.svelte
@@ -4,6 +4,7 @@
export let urls: string[];
export let alt: string;
export let width = 300;
+ export let lazyLoad = false;
const contentTypeFor = (url: string) => {
if (url.endsWith(".webp")) {
@@ -31,6 +32,7 @@
src={urls[0] || defaultAvatars[0]}
{alt}
class="rounded-circle img-fluid"
+ loading={lazyLoad ? "lazy" : "eager"}
/>
{:else}
diff --git a/frontend/src/lib/components/IconButton.svelte b/frontend/src/lib/components/IconButton.svelte
index c131a4d..240339a 100644
--- a/frontend/src/lib/components/IconButton.svelte
+++ b/frontend/src/lib/components/IconButton.svelte
@@ -1,5 +1,5 @@
diff --git a/frontend/src/routes/@[username]/+page.server.ts b/frontend/src/routes/@[username]/+page.server.ts
index 447c5e3..0ae4617 100644
--- a/frontend/src/routes/@[username]/+page.server.ts
+++ b/frontend/src/routes/@[username]/+page.server.ts
@@ -11,7 +11,7 @@ export const load = async ({ params }) => {
return resp;
} catch (e) {
if ((e as APIError).code === ErrorCode.UserNotFound) {
- throw error(404, e as APIError);
+ error(404, e as App.Error);
}
throw e;
diff --git a/frontend/src/routes/@[username]/+page.svelte b/frontend/src/routes/@[username]/+page.svelte
index 56542fc..a9fd5b4 100644
--- a/frontend/src/routes/@[username]/+page.svelte
+++ b/frontend/src/routes/@[username]/+page.svelte
@@ -4,7 +4,6 @@
import {
Alert,
- Badge,
Button,
ButtonGroup,
Icon,
@@ -14,8 +13,8 @@
ModalBody,
ModalFooter,
Tooltip,
- } from "sveltestrap";
- import { DateTime, Duration, FixedOffsetZone, Zone } from "luxon";
+ } from "@sveltestrap/sveltestrap";
+ import { DateTime, FixedOffsetZone } from "luxon";
import FieldCard from "$lib/components/FieldCard.svelte";
import PronounLink from "$lib/components/PronounLink.svelte";
import PartialMemberCard from "$lib/components/PartialMemberCard.svelte";
@@ -46,6 +45,7 @@
import ProfileFlag from "./ProfileFlag.svelte";
import IconButton from "$lib/components/IconButton.svelte";
import Badges from "./badges/Badges.svelte";
+ import PreferencesCheatsheet from "./PreferencesCheatsheet.svelte";
export let data: PageData;
@@ -190,14 +190,15 @@
{/if}
{#if data.utc_offset}
- Your profile is empty! You can customize it by going to the edit profileedit profile page. (only you can see this)
@@ -258,6 +259,12 @@ {/each} +