Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
9cc6a96691
|
@ -14,3 +14,4 @@ vite.config.ts.timestamp-*
|
||||||
target
|
target
|
||||||
/pronouns
|
/pronouns
|
||||||
tmp
|
tmp
|
||||||
|
seed.yaml
|
||||||
|
|
13
README.md
13
README.md
|
@ -26,19 +26,24 @@ Requirements:
|
||||||
- Redis 6.0 or later
|
- Redis 6.0 or later
|
||||||
- Node.js (latest version)
|
- Node.js (latest version)
|
||||||
- MinIO **if using avatars, flags, or data exports** (_not_ required otherwise)
|
- MinIO **if using avatars, flags, or data exports** (_not_ required otherwise)
|
||||||
|
- [Air](https://github.com/cosmtrek/air) for live reloading the backend
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
1. Create a PostgreSQL user and database (the user should own the database).
|
1. Create a PostgreSQL user and database (the user should own the database).
|
||||||
For example: `create user pronouns with password 'password'; create database pronouns with owner pronouns;`
|
For example: `create user pronouns with password 'password'; create database pronouns with owner pronouns;`
|
||||||
2. Copy `.env.example` in the repository root to a new file named `.env` and fill out the required options.
|
2. Copy `.env.example` in the repository root to a new file named `.env` and fill out the required options.
|
||||||
3. Run `go run -v . database migrate` to initialize the database, then optionally `go run -v . database seed` to insert a test user.
|
3. Run `go run -v . database migrate` to initialize the database, then optionally `go run -v . database seed` to insert a test user.
|
||||||
4. Run `go run -v . web` to run the backend.
|
4. Run `pnpm dev`. Alternatively, if you don't want the backend to live reload, run `go run -v . web`,
|
||||||
5. Copy `frontend/.env.example` into `frontend/.env` and tweak as necessary.
|
then change to the `frontend/` directory and run `pnpm dev`.
|
||||||
6. cd into the `frontend` directory and run `pnpm dev` to run the frontend.
|
|
||||||
|
|
||||||
See [`docs/production.md`](/docs/production.md#configuration) for more information about keys in the backend and frontend `.env` files.
|
See [`docs/production.md`](/docs/production.md#configuration) for more information about keys in the backend and frontend `.env` files.
|
||||||
|
|
||||||
|
### Seeding
|
||||||
|
|
||||||
|
To seed the database with some data, create a `seed.yaml` file, then use `go run -v . database seed`.
|
||||||
|
For the file format, refer to the `Seed` struct in `scripts/seeddb`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (C) 2022 Sam <u1f320>
|
Copyright (C) 2022 Sam <u1f320>
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"github.com/georgysavva/scany/v2/pgxscan"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Notice struct {
|
||||||
|
ID int
|
||||||
|
Notice string
|
||||||
|
StartTime time.Time
|
||||||
|
EndTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Notices(ctx context.Context) (ns []Notice, err error) {
|
||||||
|
sql, args, err := sq.Select("*").From("notices").OrderBy("id DESC").ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "building sql")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pgxscan.Select(ctx, db, &ns, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "executing query")
|
||||||
|
}
|
||||||
|
return NotNull(ns), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) CreateNotice(ctx context.Context, notice string, start, end time.Time) (n Notice, err error) {
|
||||||
|
sql, args, err := sq.Insert("notices").SetMap(map[string]any{
|
||||||
|
"notice": notice,
|
||||||
|
"start_time": start,
|
||||||
|
"end_time": end,
|
||||||
|
}).Suffix("RETURNING *").ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return n, errors.Wrap(err, "building sql")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pgxscan.Get(ctx, db, &n, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return n, errors.Wrap(err, "executing query")
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrNoNotice = errors.Sentinel("no current notice")
|
||||||
|
|
||||||
|
func (db *DB) CurrentNotice(ctx context.Context) (n Notice, err error) {
|
||||||
|
sql, args, err := sq.Select("*").From("notices").Where("end_time > ?", time.Now()).OrderBy("id DESC").Limit(1).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return n, errors.Wrap(err, "building sql")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pgxscan.Get(ctx, db, &n, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Cause(err) == pgx.ErrNoRows {
|
||||||
|
return n, ErrNoNotice
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, errors.Wrap(err, "executing query")
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
|
@ -825,3 +825,24 @@ func (db *DB) CleanUser(ctx context.Context, id xid.ID) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inactiveUsersSQL = `select id, snowflake_id from users
|
||||||
|
where last_active < now() - '30 days'::interval
|
||||||
|
and display_name is null and bio is null and timezone is null
|
||||||
|
and links is null and avatar is null and member_title is null
|
||||||
|
and names = '[]' and pronouns = '[]'
|
||||||
|
and (select count(m.id) from members m where user_id = users.id) = 0
|
||||||
|
and (select count(f.id) from user_fields f where user_id = users.id) = 0;`
|
||||||
|
|
||||||
|
// InactiveUsers gets the list of inactive users from the database.
|
||||||
|
// "Inactive" is defined as:
|
||||||
|
// - not logged in for 30 days or more
|
||||||
|
// - no display name, bio, avatar, names, pronouns, profile links, or profile fields
|
||||||
|
// - no members
|
||||||
|
func (db *DB) InactiveUsers(ctx context.Context, tx pgx.Tx) (us []User, err error) {
|
||||||
|
err = pgxscan.Select(ctx, tx, &us, inactiveUsersSQL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "executing query")
|
||||||
|
}
|
||||||
|
return us, nil
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
type UserSettings struct {
|
type UserSettings struct {
|
||||||
ReadChangelog string `json:"read_changelog"`
|
ReadChangelog string `json:"read_changelog"`
|
||||||
ReadSettingsNotice string `json:"read_settings_notice"`
|
ReadSettingsNotice string `json:"read_settings_notice"`
|
||||||
|
ReadGlobalNotice int `json:"read_global_notice"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) UpdateUserSettings(ctx context.Context, id xid.ID, us UserSettings) error {
|
func (db *DB) UpdateUserSettings(ctx context.Context, id xid.ID, us UserSettings) error {
|
||||||
|
|
|
@ -58,13 +58,9 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
||||||
} else {
|
} else {
|
||||||
id, err := common.ParseSnowflake(chi.URLParam(r, "memberRef"))
|
id, err := common.ParseSnowflake(chi.URLParam(r, "memberRef"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("%v/%v is not valid snowflake", chi.URLParam(r, "memberRef"), id)
|
|
||||||
|
|
||||||
return server.APIError{Code: server.ErrMemberNotFound}
|
return server.APIError{Code: server.ErrMemberNotFound}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("%v/%v is valid snowflake", chi.URLParam(r, "memberRef"), id)
|
|
||||||
|
|
||||||
m, err = s.DB.MemberBySnowflake(ctx, common.MemberID(id))
|
m, err = s.DB.MemberBySnowflake(ctx, common.MemberID(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == db.ErrMemberNotFound {
|
if err == db.ErrMemberNotFound {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/render"
|
"github.com/go-chi/render"
|
||||||
|
@ -20,11 +22,17 @@ func Mount(srv *server.Server, r chi.Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type MetaResponse struct {
|
type MetaResponse struct {
|
||||||
GitRepository string `json:"git_repository"`
|
GitRepository string `json:"git_repository"`
|
||||||
GitCommit string `json:"git_commit"`
|
GitCommit string `json:"git_commit"`
|
||||||
Users MetaUsers `json:"users"`
|
Users MetaUsers `json:"users"`
|
||||||
Members int64 `json:"members"`
|
Members int64 `json:"members"`
|
||||||
RequireInvite bool `json:"require_invite"`
|
RequireInvite bool `json:"require_invite"`
|
||||||
|
Notice *MetaNotice `json:"notice"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetaNotice struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Notice string `json:"notice"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MetaUsers struct {
|
type MetaUsers struct {
|
||||||
|
@ -39,6 +47,18 @@ func (s *Server) meta(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
numUsers, numMembers, activeDay, activeWeek, activeMonth := s.DB.Counts(ctx)
|
numUsers, numMembers, activeDay, activeWeek, activeMonth := s.DB.Counts(ctx)
|
||||||
|
|
||||||
|
var notice *MetaNotice
|
||||||
|
if n, err := s.DB.CurrentNotice(ctx); err != nil {
|
||||||
|
if err == db.ErrNoNotice {
|
||||||
|
log.Errorf("getting notice: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notice = &MetaNotice{
|
||||||
|
ID: n.ID,
|
||||||
|
Notice: n.Notice,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render.JSON(w, r, MetaResponse{
|
render.JSON(w, r, MetaResponse{
|
||||||
GitRepository: server.Repository,
|
GitRepository: server.Repository,
|
||||||
GitCommit: server.Revision,
|
GitCommit: server.Revision,
|
||||||
|
@ -50,6 +70,7 @@ func (s *Server) meta(w http.ResponseWriter, r *http.Request) error {
|
||||||
},
|
},
|
||||||
Members: numMembers,
|
Members: numMembers,
|
||||||
RequireInvite: os.Getenv("REQUIRE_INVITE") == "true",
|
RequireInvite: os.Getenv("REQUIRE_INVITE") == "true",
|
||||||
|
Notice: notice,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package mod
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"github.com/aarondl/opt/omit"
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createNoticeRequest struct {
|
||||||
|
Notice string `json:"notice"`
|
||||||
|
Start omit.Val[time.Time] `json:"start"`
|
||||||
|
End time.Time `json:"end"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type noticeResponse struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Notice string `json:"notice"`
|
||||||
|
StartTime time.Time `json:"start"`
|
||||||
|
EndTime time.Time `json:"end"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) createNotice(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
var req createNoticeRequest
|
||||||
|
err := render.Decode(r, &req)
|
||||||
|
if err != nil {
|
||||||
|
return server.APIError{Code: server.ErrBadRequest}
|
||||||
|
}
|
||||||
|
|
||||||
|
if common.StringLength(&req.Notice) > 2000 {
|
||||||
|
return server.APIError{Code: server.ErrBadRequest, Details: "Notice is too long, max 2000 characters"}
|
||||||
|
}
|
||||||
|
|
||||||
|
start := req.Start.GetOr(time.Now())
|
||||||
|
if req.End.IsZero() {
|
||||||
|
return server.APIError{Code: server.ErrBadRequest, Details: "`end` is missing or invalid"}
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := s.DB.CreateNotice(r.Context(), req.Notice, start, req.End)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "creating notice")
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSON(w, r, noticeResponse{
|
||||||
|
ID: n.ID,
|
||||||
|
Notice: n.Notice,
|
||||||
|
StartTime: n.StartTime,
|
||||||
|
EndTime: n.EndTime,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -22,6 +22,8 @@ func Mount(srv *server.Server, r chi.Router) {
|
||||||
r.Get("/reports/by-reporter/{id}", server.WrapHandler(s.getReportsByReporter))
|
r.Get("/reports/by-reporter/{id}", server.WrapHandler(s.getReportsByReporter))
|
||||||
|
|
||||||
r.Patch("/reports/{id}", server.WrapHandler(s.resolveReport))
|
r.Patch("/reports/{id}", server.WrapHandler(s.resolveReport))
|
||||||
|
|
||||||
|
r.Post("/notices", server.WrapHandler(s.createNotice))
|
||||||
})
|
})
|
||||||
|
|
||||||
r.With(MustAdmin).Handle("/metrics", promhttp.Handler())
|
r.With(MustAdmin).Handle("/metrics", promhttp.Handler())
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
type PatchSettingsRequest struct {
|
type PatchSettingsRequest struct {
|
||||||
ReadChangelog omitnull.Val[string] `json:"read_changelog"`
|
ReadChangelog omitnull.Val[string] `json:"read_changelog"`
|
||||||
ReadSettingsNotice omitnull.Val[string] `json:"read_settings_notice"`
|
ReadSettingsNotice omitnull.Val[string] `json:"read_settings_notice"`
|
||||||
|
ReadGlobalNotice omitnull.Val[int] `json:"read_global_notice"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) PatchSettings(w http.ResponseWriter, r *http.Request) (err error) {
|
func (s *Server) PatchSettings(w http.ResponseWriter, r *http.Request) (err error) {
|
||||||
|
@ -34,6 +35,9 @@ func (s *Server) PatchSettings(w http.ResponseWriter, r *http.Request) (err erro
|
||||||
if !req.ReadSettingsNotice.IsUnset() {
|
if !req.ReadSettingsNotice.IsUnset() {
|
||||||
u.Settings.ReadSettingsNotice = req.ReadSettingsNotice.GetOrZero()
|
u.Settings.ReadSettingsNotice = req.ReadSettingsNotice.GetOrZero()
|
||||||
}
|
}
|
||||||
|
if !req.ReadGlobalNotice.IsUnset() {
|
||||||
|
u.Settings.ReadGlobalNotice = req.ReadGlobalNotice.GetOrZero()
|
||||||
|
}
|
||||||
|
|
||||||
err = s.DB.UpdateUserSettings(ctx, u.ID, u.Settings)
|
err = s.DB.UpdateUserSettings(ctx, u.ID, u.Settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -62,6 +62,12 @@ export interface MeUser extends User {
|
||||||
timezone: string | null;
|
timezone: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
read_changelog: string;
|
||||||
|
read_settings_notice: string;
|
||||||
|
read_global_notice: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Field {
|
export interface Field {
|
||||||
name: string;
|
name: string;
|
||||||
entries: FieldEntry[];
|
entries: FieldEntry[];
|
||||||
|
|
|
@ -11,9 +11,16 @@ export async function apiFetch<T>(
|
||||||
body,
|
body,
|
||||||
token,
|
token,
|
||||||
headers,
|
headers,
|
||||||
}: { method?: string; body?: any; token?: string; headers?: Record<string, string> },
|
version,
|
||||||
|
}: {
|
||||||
|
method?: string;
|
||||||
|
body?: any;
|
||||||
|
token?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
version?: number;
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const resp = await fetch(`${PUBLIC_BASE_URL}/api/v1${path}`, {
|
const resp = await fetch(`${PUBLIC_BASE_URL}/api/v${version || 1}${path}`, {
|
||||||
method: method || "GET",
|
method: method || "GET",
|
||||||
headers: {
|
headers: {
|
||||||
...(token ? { Authorization: token } : {}),
|
...(token ? { Authorization: token } : {}),
|
||||||
|
@ -28,12 +35,18 @@ export async function apiFetch<T>(
|
||||||
return data as T;
|
return data as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiFetchClient = async <T>(path: string, method = "GET", body: any = null) => {
|
export const apiFetchClient = async <T>(
|
||||||
|
path: string,
|
||||||
|
method = "GET",
|
||||||
|
body: any = null,
|
||||||
|
version = 1,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const data = await apiFetch<T>(path, {
|
const data = await apiFetch<T>(path, {
|
||||||
method,
|
method,
|
||||||
body,
|
body,
|
||||||
token: localStorage.getItem("pronouns-token") || undefined,
|
token: localStorage.getItem("pronouns-token") || undefined,
|
||||||
|
version,
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -55,9 +68,16 @@ export async function fastFetch(
|
||||||
body,
|
body,
|
||||||
token,
|
token,
|
||||||
headers,
|
headers,
|
||||||
}: { method?: string; body?: any; token?: string; headers?: Record<string, string> },
|
version,
|
||||||
|
}: {
|
||||||
|
method?: string;
|
||||||
|
body?: any;
|
||||||
|
token?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
version?: number;
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const resp = await fetch(`${PUBLIC_BASE_URL}/api/v1${path}`, {
|
const resp = await fetch(`${PUBLIC_BASE_URL}/api/v${version || 1}${path}`, {
|
||||||
method: method || "GET",
|
method: method || "GET",
|
||||||
headers: {
|
headers: {
|
||||||
...(token ? { Authorization: token } : {}),
|
...(token ? { Authorization: token } : {}),
|
||||||
|
@ -71,12 +91,18 @@ export async function fastFetch(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetches the specified path without parsing the response body. */
|
/** Fetches the specified path without parsing the response body. */
|
||||||
export const fastFetchClient = async (path: string, method = "GET", body: any = null) => {
|
export const fastFetchClient = async (
|
||||||
|
path: string,
|
||||||
|
method = "GET",
|
||||||
|
body: any = null,
|
||||||
|
version = 1,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
await fastFetch(path, {
|
await fastFetch(path, {
|
||||||
method,
|
method,
|
||||||
body,
|
body,
|
||||||
token: localStorage.getItem("pronouns-token") || undefined,
|
token: localStorage.getItem("pronouns-token") || undefined,
|
||||||
|
version,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if ((e as APIError).code === ErrorCode.InvalidToken) {
|
if ((e as APIError).code === ErrorCode.InvalidToken) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ export interface MetaResponse {
|
||||||
users: MetaUsers;
|
users: MetaUsers;
|
||||||
members: number;
|
members: number;
|
||||||
require_invite: boolean;
|
require_invite: boolean;
|
||||||
|
notice: { id: number; notice: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetaUsers {
|
export interface MetaUsers {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
import type { MeUser } from "./api/entities";
|
import type { MeUser, Settings } from "./api/entities";
|
||||||
|
|
||||||
const initialUserValue = null;
|
const initialUserValue = null;
|
||||||
export const userStore = writable<MeUser | null>(initialUserValue);
|
export const userStore = writable<MeUser | null>(initialUserValue);
|
||||||
|
@ -13,4 +13,10 @@ const initialThemeValue = browser
|
||||||
|
|
||||||
export const themeStore = writable<string>(initialThemeValue);
|
export const themeStore = writable<string>(initialThemeValue);
|
||||||
|
|
||||||
|
const defaultSettingsValue = {
|
||||||
|
settings: { read_changelog: "0.0.0", read_settings_notice: "0" } as Settings,
|
||||||
|
current: false,
|
||||||
|
};
|
||||||
|
export const settingsStore = writable(defaultSettingsValue);
|
||||||
|
|
||||||
export const CURRENT_CHANGELOG = "0.6.0";
|
export const CURRENT_CHANGELOG = "0.6.0";
|
||||||
|
|
|
@ -7,8 +7,18 @@ const md = new MarkdownIt({
|
||||||
linkify: true,
|
linkify: true,
|
||||||
}).disable(["heading", "lheading", "link", "table", "blockquote"]);
|
}).disable(["heading", "lheading", "link", "table", "blockquote"]);
|
||||||
|
|
||||||
|
const unsafeMd = new MarkdownIt({
|
||||||
|
html: false,
|
||||||
|
breaks: true,
|
||||||
|
linkify: true,
|
||||||
|
});
|
||||||
|
|
||||||
export function renderMarkdown(src: string | null) {
|
export function renderMarkdown(src: string | null) {
|
||||||
return src ? sanitize(md.render(src)) : null;
|
return src ? sanitize(md.render(src)) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function renderUnsafeMarkdown(src: string) {
|
||||||
|
return sanitize(unsafeMd.render(src));
|
||||||
|
}
|
||||||
|
|
||||||
export const charCount = (str: string) => [...str].length;
|
export const charCount = (str: string) => [...str].length;
|
||||||
|
|
|
@ -22,7 +22,8 @@ export const load = (async () => {
|
||||||
},
|
},
|
||||||
members: 0,
|
members: 0,
|
||||||
require_invite: false,
|
require_invite: false,
|
||||||
};
|
notice: null,
|
||||||
|
} as MetaResponse;
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,13 @@
|
||||||
import Navigation from "./nav/Navigation.svelte";
|
import Navigation from "./nav/Navigation.svelte";
|
||||||
import type { LayoutData } from "./$types";
|
import type { LayoutData } from "./$types";
|
||||||
import { version } from "$app/environment";
|
import { version } from "$app/environment";
|
||||||
|
import { settingsStore } from "$lib/store";
|
||||||
import { toastStore } from "$lib/toast";
|
import { toastStore } from "$lib/toast";
|
||||||
import Toast from "$lib/components/Toast.svelte";
|
import Toast from "$lib/components/Toast.svelte";
|
||||||
import { Icon } from "sveltestrap";
|
import { Alert, Icon } from "sveltestrap";
|
||||||
|
import { apiFetchClient } from "$lib/api/fetch";
|
||||||
|
import type { Settings } from "$lib/api/entities";
|
||||||
|
import { renderUnsafeMarkdown } from "$lib/utils";
|
||||||
|
|
||||||
export let data: LayoutData;
|
export let data: LayoutData;
|
||||||
|
|
||||||
|
@ -21,6 +25,20 @@
|
||||||
if (versionParts.length >= 3) commit = versionParts[2].slice(1);
|
if (versionParts.length >= 3) commit = versionParts[2].slice(1);
|
||||||
|
|
||||||
const versionMismatch = data.git_commit !== commit && data.git_commit !== "[unknown]";
|
const versionMismatch = data.git_commit !== commit && data.git_commit !== "[unknown]";
|
||||||
|
|
||||||
|
const readNotice = async () => {
|
||||||
|
try {
|
||||||
|
const resp = await apiFetchClient<Settings>(
|
||||||
|
"/users/@me/settings",
|
||||||
|
"PATCH",
|
||||||
|
{ read_global_notice: data.notice!.id },
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
settingsStore.set({ current: true, settings: resp });
|
||||||
|
} catch (e) {
|
||||||
|
console.log("updating settings:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
@ -33,6 +51,12 @@
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<Navigation commit={data.git_commit} />
|
<Navigation commit={data.git_commit} />
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
{#if data.notice && $settingsStore.current && data.notice.id > $settingsStore.settings.read_global_notice}
|
||||||
|
<Alert color="secondary" isOpen={true} toggle={() => readNotice()}>
|
||||||
|
{@html renderUnsafeMarkdown(data.notice.notice)}
|
||||||
|
</Alert>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
<div class="position-absolute top-0 start-50 translate-middle-x">
|
<div class="position-absolute top-0 start-50 translate-middle-x">
|
||||||
{#each $toastStore as toast}
|
{#each $toastStore as toast}
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isLink}
|
{#if isLink}
|
||||||
<a href={link} class="text-decoration-none">
|
<a href={link} class="text-decoration-none" rel="me nofollow noreferrer" target="_blank">
|
||||||
<li class="py-2 py-lg-0">
|
<li class="py-2 py-lg-0">
|
||||||
<Icon name="globe" aria-hidden class="text-body" />
|
<Icon name="globe" aria-hidden class="text-body" />
|
||||||
<span class="text-decoration-underline">{displayLink}</span>
|
<span class="text-decoration-underline">{displayLink}</span>
|
||||||
|
|
|
@ -17,13 +17,14 @@
|
||||||
} from "sveltestrap";
|
} from "sveltestrap";
|
||||||
|
|
||||||
import Logo from "./Logo.svelte";
|
import Logo from "./Logo.svelte";
|
||||||
import { userStore, themeStore, CURRENT_CHANGELOG } from "$lib/store";
|
import { userStore, themeStore, CURRENT_CHANGELOG, settingsStore } from "$lib/store";
|
||||||
import {
|
import {
|
||||||
ErrorCode,
|
ErrorCode,
|
||||||
type APIError,
|
type APIError,
|
||||||
type MeUser,
|
type MeUser,
|
||||||
type Report,
|
type Report,
|
||||||
type Warning,
|
type Warning,
|
||||||
|
type Settings,
|
||||||
} from "$lib/api/entities";
|
} from "$lib/api/entities";
|
||||||
import { apiFetch, apiFetchClient } from "$lib/api/fetch";
|
import { apiFetch, apiFetchClient } from "$lib/api/fetch";
|
||||||
import { addToast } from "$lib/toast";
|
import { addToast } from "$lib/toast";
|
||||||
|
@ -37,14 +38,11 @@
|
||||||
let isAdmin = false;
|
let isAdmin = false;
|
||||||
let numReports = 0;
|
let numReports = 0;
|
||||||
let numWarnings = 0;
|
let numWarnings = 0;
|
||||||
let changelogRead = "99.99.99";
|
|
||||||
|
|
||||||
$: currentUser = $userStore;
|
$: currentUser = $userStore;
|
||||||
$: theme = $themeStore;
|
$: theme = $themeStore;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
changelogRead = localStorage.getItem("changelog-read") || "0.0.0";
|
|
||||||
|
|
||||||
const localUser = localStorage.getItem("pronouns-user");
|
const localUser = localStorage.getItem("pronouns-user");
|
||||||
userStore.set(localUser ? JSON.parse(localUser) : null);
|
userStore.set(localUser ? JSON.parse(localUser) : null);
|
||||||
|
|
||||||
|
@ -78,6 +76,38 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apiFetchClient<Settings>("/users/@me/settings", "GET", null, 2)
|
||||||
|
.then((data) => {
|
||||||
|
settingsStore.set({ current: true, settings: data });
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log("getting user settings:", e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: is there a cleaner way to do this? also, remove this eventually
|
||||||
|
const oldChangelogRead = localStorage.getItem("changelog-read");
|
||||||
|
const oldSettingsNotice = localStorage.getItem("alert-1681976313");
|
||||||
|
if (oldChangelogRead || oldSettingsNotice) {
|
||||||
|
localStorage.removeItem("changelog-read");
|
||||||
|
localStorage.removeItem("alert-1681976313");
|
||||||
|
|
||||||
|
apiFetchClient<Settings>(
|
||||||
|
"/users/@me/settings",
|
||||||
|
"PATCH",
|
||||||
|
{
|
||||||
|
read_changelog: oldChangelogRead ? oldChangelogRead : undefined,
|
||||||
|
read_settings_notice: oldSettingsNotice ? "1681976313" : undefined,
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
.then((data) => {
|
||||||
|
settingsStore.set({ current: true, settings: data });
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log("updating user settings:", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
apiFetchClient<Warning[]>("/auth/warnings")
|
apiFetchClient<Warning[]>("/auth/warnings")
|
||||||
.then((warnings) => {
|
.then((warnings) => {
|
||||||
if (warnings.length !== 0) {
|
if (warnings.length !== 0) {
|
||||||
|
@ -157,7 +187,7 @@
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#if changelogRead < CURRENT_CHANGELOG}
|
{#if $settingsStore.current && $settingsStore.settings.read_changelog < CURRENT_CHANGELOG}
|
||||||
<NavItem>
|
<NavItem>
|
||||||
<NavLink href="/page/changelog" active={$page.url.pathname === "/page/changelog"}>
|
<NavLink href="/page/changelog" active={$page.url.pathname === "/page/changelog"}>
|
||||||
Changelog <Badge color="secondary">v{CURRENT_CHANGELOG}</Badge>
|
Changelog <Badge color="secondary">v{CURRENT_CHANGELOG}</Badge>
|
||||||
|
|
|
@ -17,6 +17,9 @@ We reserve the right to modify these Terms and the Privacy Policy at any time. W
|
||||||
|
|
||||||
You must be at least 13 years old (chronologically) and meet the minimum age required by the laws in your country to use the service.
|
You must be at least 13 years old (chronologically) and meet the minimum age required by the laws in your country to use the service.
|
||||||
|
|
||||||
|
Accounts with empty profiles, no members, and no activity within the last 30 days
|
||||||
|
will have their usernames changed to the account's ID, to free up unused usernames.
|
||||||
|
|
||||||
## Content
|
## Content
|
||||||
|
|
||||||
You are responsible for the content you post on the service.
|
You are responsible for the content you post on the service.
|
||||||
|
|
|
@ -6,12 +6,13 @@
|
||||||
type APIError,
|
type APIError,
|
||||||
MAX_MEMBERS,
|
MAX_MEMBERS,
|
||||||
MAX_FIELDS,
|
MAX_FIELDS,
|
||||||
|
type Settings,
|
||||||
} from "$lib/api/entities";
|
} from "$lib/api/entities";
|
||||||
import { apiFetchClient, fastFetchClient } from "$lib/api/fetch";
|
import { apiFetchClient, fastFetchClient } from "$lib/api/fetch";
|
||||||
import { usernameRegex } from "$lib/api/regex";
|
import { usernameRegex } from "$lib/api/regex";
|
||||||
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
||||||
import FallbackImage from "$lib/components/FallbackImage.svelte";
|
import FallbackImage from "$lib/components/FallbackImage.svelte";
|
||||||
import { userStore } from "$lib/store";
|
import { settingsStore, userStore } from "$lib/store";
|
||||||
import { addToast } from "$lib/toast";
|
import { addToast } from "$lib/toast";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
@ -91,22 +92,37 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const DONATE_ALERT_STORE = "alert-1681976313";
|
const CURRENT_ALERT = "1681976313";
|
||||||
let donateAlertOpen = false;
|
const closeDonateAlert = async () => {
|
||||||
const closeDonateAlert = () => {
|
try {
|
||||||
donateAlertOpen = false;
|
const settings = await apiFetchClient<Settings>(
|
||||||
localStorage.setItem(DONATE_ALERT_STORE, "dismissed");
|
"/users/@me/settings",
|
||||||
};
|
"PATCH",
|
||||||
|
{
|
||||||
|
read_settings_notice: CURRENT_ALERT,
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
onMount(() => {
|
settingsStore.set({ current: true, settings });
|
||||||
if (!localStorage.getItem(DONATE_ALERT_STORE)) {
|
error = null;
|
||||||
donateAlertOpen = true;
|
} catch (e) {
|
||||||
|
error = e as APIError;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Alert color="secondary" fade={false} isOpen={donateAlertOpen} toggle={closeDonateAlert}>
|
{#if error}
|
||||||
If you find pronouns fu useful and have the means, I would really appreciate a <a
|
<ErrorAlert {error} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
color="secondary"
|
||||||
|
fade={false}
|
||||||
|
isOpen={$settingsStore.current && $settingsStore.settings.read_settings_notice < CURRENT_ALERT}
|
||||||
|
toggle={closeDonateAlert}
|
||||||
|
>
|
||||||
|
If you find pronouns.cc useful and have the means, I would really appreciate a <a
|
||||||
href="https://freak.university/about#donation"
|
href="https://freak.university/about#donation"
|
||||||
target="_blank">donation</a
|
target="_blank">donation</a
|
||||||
>
|
>
|
||||||
|
@ -150,9 +166,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if error}
|
|
||||||
<ErrorAlert {error} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -28,6 +28,7 @@ require (
|
||||||
go.uber.org/zap v1.24.0
|
go.uber.org/zap v1.24.0
|
||||||
golang.org/x/oauth2 v0.7.0
|
golang.org/x/oauth2 v0.7.0
|
||||||
google.golang.org/api v0.118.0
|
google.golang.org/api v0.118.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
|
@ -3,6 +3,7 @@ package cleandb
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
dbpkg "codeberg.org/pronounscc/pronouns.cc/backend/db"
|
dbpkg "codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
|
@ -25,6 +26,8 @@ func run(c *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeUnusedUsernames := os.Getenv("DB_CLEAN_CHANGE_UNUSED_USERNAMES") == "true"
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
db, err := dbpkg.New()
|
db, err := dbpkg.New()
|
||||||
|
@ -66,6 +69,41 @@ func run(c *cli.Context) error {
|
||||||
|
|
||||||
fmt.Printf("deleted %v expired exports\n", len(exports))
|
fmt.Printf("deleted %v expired exports\n", len(exports))
|
||||||
|
|
||||||
|
if changeUnusedUsernames {
|
||||||
|
fmt.Println("cleaning unused usernames")
|
||||||
|
|
||||||
|
tx, err := db.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error starting transaction: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
inactiveUsers, err := db.InactiveUsers(ctx, tx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("getting inactive users: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range inactiveUsers {
|
||||||
|
err = db.UpdateUsername(ctx, tx, u.ID, fmt.Sprintf("inactive-user-%v", u.SnowflakeID))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("changing username for user %v: %v\n", u.SnowflakeID, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("committing transaction: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("changed usernames for %v inactive users\n", len(inactiveUsers))
|
||||||
|
} else {
|
||||||
|
fmt.Println("not cleaning unused usernames")
|
||||||
|
}
|
||||||
|
|
||||||
var users []dbpkg.User
|
var users []dbpkg.User
|
||||||
err = pgxscan.Select(ctx, db, &users, `SELECT * FROM users WHERE
|
err = pgxscan.Select(ctx, db, &users, `SELECT * FROM users WHERE
|
||||||
deleted_at IS NOT NULL AND
|
deleted_at IS NOT NULL AND
|
||||||
|
@ -78,7 +116,7 @@ func run(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
fmt.Println("there are no users pending deletion")
|
fmt.Println("there are no users pending deletion\nfinished cleaning database!")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,5 +170,6 @@ func run(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("deleted %v users!\n", ct.RowsAffected())
|
fmt.Printf("deleted %v users!\n", ct.RowsAffected())
|
||||||
|
fmt.Println("finished cleaning database!")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- 2023-09-09: Add global notices
|
||||||
|
|
||||||
|
-- +migrate Up
|
||||||
|
|
||||||
|
create table notices (
|
||||||
|
id serial primary key,
|
||||||
|
notice text not null,
|
||||||
|
start_time timestamptz not null default now(),
|
||||||
|
end_time timestamptz not null
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +migrate Down
|
||||||
|
|
||||||
|
drop table notices;
|
|
@ -1,15 +1,42 @@
|
||||||
package seeddb
|
package seeddb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Seed struct {
|
||||||
|
Users []SeedUser `yaml:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SeedUser struct {
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
DisplayName *string `yaml:"displayName"`
|
||||||
|
Bio *string `yaml:"bio"`
|
||||||
|
Links []string `yaml:"links"`
|
||||||
|
Names []db.FieldEntry `yaml:"names"`
|
||||||
|
Pronouns []db.PronounEntry `yaml:"pronouns"`
|
||||||
|
Fields []db.Field `yaml:"fields"`
|
||||||
|
Members []SeedMember `yaml:"members"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SeedMember struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
DisplayName *string `yaml:"displayName"`
|
||||||
|
Bio string `yaml:"bio"`
|
||||||
|
Links []string `yaml:"links"`
|
||||||
|
Names []db.FieldEntry `yaml:"names"`
|
||||||
|
Pronouns []db.PronounEntry `yaml:"pronouns"`
|
||||||
|
Fields []db.Field `yaml:"fields"`
|
||||||
|
Members []SeedMember `yaml:"members"`
|
||||||
|
}
|
||||||
|
|
||||||
var Command = &cli.Command{
|
var Command = &cli.Command{
|
||||||
Name: "seed",
|
Name: "seed",
|
||||||
Usage: "Seed the database with test data",
|
Usage: "Seed the database with test data",
|
||||||
|
@ -19,7 +46,7 @@ var Command = &cli.Command{
|
||||||
func run(c *cli.Context) error {
|
func run(c *cli.Context) error {
|
||||||
err := godotenv.Load()
|
err := godotenv.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("error loading .env file:", err)
|
log.Println("error loading .env file:", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,112 +54,94 @@ func run(c *cli.Context) error {
|
||||||
|
|
||||||
pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
|
pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("error opening database:", err)
|
log.Println("error opening database:", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer pool.Close()
|
defer pool.Close()
|
||||||
|
|
||||||
fmt.Println("opened database")
|
log.Println("opened database")
|
||||||
|
|
||||||
pg := &db.DB{Pool: pool}
|
pg := &db.DB{Pool: pool}
|
||||||
|
|
||||||
|
// read seed file
|
||||||
|
seedFile, err := os.ReadFile("seed.yaml")
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error opening seed.yaml:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var seed Seed
|
||||||
|
err = yaml.Unmarshal(seedFile, &seed)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error reading seed.yaml:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
tx, err := pg.Begin(ctx)
|
tx, err := pg.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("error beginning transaction:", err)
|
log.Println("error beginning transaction:", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
u, err := pg.CreateUser(ctx, tx, "test")
|
for i, su := range seed.Users {
|
||||||
if err != nil {
|
u, err := pg.CreateUser(ctx, tx, su.Username)
|
||||||
fmt.Println("error creating user:", err)
|
if err != nil {
|
||||||
return err
|
log.Printf("error creating user #%v/%s: %v", i+1, su.Username, err)
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
_, err = pg.UpdateUser(ctx, tx, u.ID, ptr("testing"), ptr("This is a bio!"), nil, ptr(false), &[]string{"https://pronouns.cc"}, nil, nil, nil)
|
_, err = pg.UpdateUser(ctx, tx, u.ID, su.DisplayName, su.Bio, nil, nil, &su.Links, nil, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("error setting user info:", err)
|
log.Printf("updating user %s: %v", su.Username, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = pg.SetUserNamesPronouns(ctx, tx, u.ID, []db.FieldEntry{
|
err = pg.SetUserNamesPronouns(ctx, tx, u.ID, db.NotNull(su.Names), db.NotNull(su.Pronouns))
|
||||||
{Value: "testing 1", Status: "favourite"},
|
if err != nil {
|
||||||
{Value: "testing 2", Status: "okay"},
|
log.Printf("setting names/pronouns for user %s: %v", su.Username, err)
|
||||||
}, []db.PronounEntry{
|
return err
|
||||||
{Pronouns: "it/it/its/its/itself", DisplayText: ptr("it/its"), Status: "favourite"},
|
}
|
||||||
{Pronouns: "they/them/their/theirs/themself", Status: "okay"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("error setting pronouns:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = pg.SetUserFields(ctx, tx, u.ID, []db.Field{
|
err = pg.SetUserFields(ctx, tx, u.ID, db.NotNull(su.Fields))
|
||||||
{
|
if err != nil {
|
||||||
Name: "Field 1",
|
log.Printf("setting fields for user %s: %v", su.Username, err)
|
||||||
Entries: []db.FieldEntry{
|
return err
|
||||||
{Value: "Favourite 1", Status: "favourite"},
|
}
|
||||||
{Value: "Okay 1", Status: "okay"},
|
|
||||||
{Value: "Jokingly 1", Status: "jokingly"},
|
log.Printf("creating members for user %s", su.Username)
|
||||||
{Value: "Friends only 1", Status: "friends_only"},
|
|
||||||
{Value: "Avoid 1", Status: "avoid"},
|
for _, sm := range su.Members {
|
||||||
},
|
m, err := pg.CreateMember(ctx, tx, u.ID, sm.Name, sm.DisplayName, sm.Bio, db.NotNull(sm.Links))
|
||||||
},
|
if err != nil {
|
||||||
{
|
log.Printf("creating member %s: %v", sm.Name, err)
|
||||||
Name: "Field 2",
|
return err
|
||||||
Entries: []db.FieldEntry{
|
}
|
||||||
{Value: "Favourite 2", Status: "favourite"},
|
|
||||||
{Value: "Okay 2", Status: "okay"},
|
err = pg.SetMemberNamesPronouns(ctx, tx, m.ID, db.NotNull(sm.Names), db.NotNull(sm.Pronouns))
|
||||||
{Value: "Jokingly 2", Status: "jokingly"},
|
if err != nil {
|
||||||
{Value: "Friends only 2", Status: "friends_only"},
|
log.Printf("setting names/pronouns for member %s: %v", sm.Name, err)
|
||||||
{Value: "Avoid 2", Status: "avoid"},
|
return err
|
||||||
},
|
}
|
||||||
},
|
|
||||||
{
|
err = pg.SetMemberFields(ctx, tx, m.ID, db.NotNull(sm.Fields))
|
||||||
Name: "Field 3",
|
if err != nil {
|
||||||
Entries: []db.FieldEntry{
|
log.Printf("setting fields for member %s: %v", sm.Name, err)
|
||||||
{Value: "Favourite 3", Status: "favourite"},
|
return err
|
||||||
{Value: "Okay 3", Status: "okay"},
|
}
|
||||||
{Value: "Jokingly 3", Status: "jokingly"},
|
|
||||||
{Value: "Friends only 3", Status: "friends_only"},
|
log.Printf("created member %s", sm.Name)
|
||||||
{Value: "Avoid 3", Status: "avoid"},
|
}
|
||||||
},
|
|
||||||
},
|
log.Printf("created user %s", su.Username)
|
||||||
{
|
|
||||||
Name: "Field 4",
|
|
||||||
Entries: []db.FieldEntry{
|
|
||||||
{Value: "Favourite 4", Status: "favourite"},
|
|
||||||
{Value: "Okay 4", Status: "okay"},
|
|
||||||
{Value: "Jokingly 4", Status: "jokingly"},
|
|
||||||
{Value: "Friends only 4", Status: "friends_only"},
|
|
||||||
{Value: "Avoid 4", Status: "avoid"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Field 5",
|
|
||||||
Entries: []db.FieldEntry{
|
|
||||||
{Value: "Favourite 5", Status: "favourite"},
|
|
||||||
{Value: "Okay 5", Status: "okay"},
|
|
||||||
{Value: "Jokingly 5", Status: "jokingly"},
|
|
||||||
{Value: "Friends only 5", Status: "friends_only"},
|
|
||||||
{Value: "Avoid 5", Status: "avoid"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("error setting fields:", err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit(ctx)
|
err = tx.Commit(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("error committing transaction:", err)
|
log.Println("error committing transaction:", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Created testing user with ID", u.ID, "and name", u.Username)
|
log.Printf("seeded database with %d users", len(seed.Users))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ptr[T any](v T) *T {
|
|
||||||
return &v
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
users:
|
||||||
|
- username: test1
|
||||||
|
displayName: "Test 1 :3"
|
||||||
|
bio: |
|
||||||
|
*Hiiiiii*
|
||||||
|
I'm a user!
|
||||||
|
links: [https://pronouns.cc, https://codeberg.org/pronounscc/pronouns.cc]
|
||||||
|
names:
|
||||||
|
- value: Test
|
||||||
|
status: favourite
|
||||||
|
- value: Tester
|
||||||
|
status: okay
|
||||||
|
- value: Testington
|
||||||
|
status: avoid
|
||||||
|
pronouns:
|
||||||
|
- pronouns: they/them/their/theirs/themself
|
||||||
|
status: favourite
|
||||||
|
- pronouns: it/it/its/its/itself
|
||||||
|
displayText: it/its
|
||||||
|
status: okay
|
||||||
|
fields:
|
||||||
|
- name: Field 1
|
||||||
|
entries:
|
||||||
|
- value: Entry 1
|
||||||
|
status: favourite
|
||||||
|
- value: Entry 2
|
||||||
|
status: favourite
|
||||||
|
- value: Entry 3
|
||||||
|
status: friends_only
|
||||||
|
- name: Field 2
|
||||||
|
entries:
|
||||||
|
- value: Entry 4
|
||||||
|
status: avoid
|
||||||
|
- value: Entry 4
|
||||||
|
status: okay
|
||||||
|
members:
|
||||||
|
- name: member 1
|
||||||
|
- name: member 2
|
||||||
|
- name: member 3
|
||||||
|
- name: member 4
|
||||||
|
- name: member 5
|
||||||
|
- name: member 6
|
||||||
|
- name: member 7
|
Loading…
Reference in New Issue