v0.35.0 e86dbd6

pending_issue.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6	"crypto/rand"
7	"encoding/base32"
8	"fmt"
9	"strings"
10	"time"
11	"unicode/utf8"
12)
13
14// Limits of the public issue form (ADR 0031).
15const (
16	// MaxPendingIssues is the most issues awaiting moderation, over every
17	// repository. Past it submissions are refused.
18	MaxPendingIssues = 200
19	// SubmitPerHour and SubmitPerDay are the submissions one client may make
20	// in a sliding hour and a sliding day.
21	SubmitPerHour = 3
22	SubmitPerDay  = 10
23	// BanDuration is how long a rejected submitter stays banned.
24	BanDuration = 30 * 24 * time.Hour
25	// MaxPendingAuthorLen is the longest display name of a submitter, in
26	// code points.
27	MaxPendingAuthorLen = 40
28	// PendingIssueIDLen is the length of a pending issue id: 16 random bytes
29	// in unpadded lowercase base32.
30	PendingIssueIDLen = 26
31	// IPHashLen is the size of the HMAC of a client key.
32	IPHashLen = 32
33)
34
35var pendingIDEncoding = base32.StdEncoding.WithPadding(base32.NoPadding)
36
37// PendingIssueID identifies an issue awaiting moderation. It is random and
38// opaque, not a number: a pending issue has none yet.
39type PendingIssueID string
40
41// NewPendingIssueID returns a fresh random id.
42func NewPendingIssueID() (PendingIssueID, error) {
43	var b [16]byte
44	if _, err := rand.Read(b[:]); err != nil {
45		return "", fmt.Errorf("generate pending issue id: %w", err)
46	}
47	return PendingIssueID(strings.ToLower(pendingIDEncoding.EncodeToString(b[:]))), nil
48}
49
50// ParsePendingIssueID accepts exactly what NewPendingIssueID produces: 26
51// characters of a-z and 2-7. Anything else gives an error wrapping ErrInvalid.
52func ParsePendingIssueID(s string) (PendingIssueID, error) {
53	if len(s) != PendingIssueIDLen {
54		return "", fmt.Errorf("%w: pending issue id", ErrInvalid)
55	}
56	for i := range len(s) {
57		c := s[i]
58		if (c < 'a' || c > 'z') && (c < '2' || c > '7') {
59			return "", fmt.Errorf("%w: pending issue id", ErrInvalid)
60		}
61	}
62	// 26 base32 characters carry 130 bits; the last one must not set the two
63	// spare bits, or the id would not round-trip.
64	raw, err := pendingIDEncoding.DecodeString(strings.ToUpper(s))
65	if err != nil || len(raw) != 16 || strings.ToLower(pendingIDEncoding.EncodeToString(raw)) != s {
66		return "", fmt.Errorf("%w: pending issue id", ErrInvalid)
67	}
68	return PendingIssueID(s), nil
69}
70
71// String returns the id as text.
72func (id PendingIssueID) String() string { return string(id) }
73
74// ParsePendingAuthor validates the optional display name of a submitter: valid
75// UTF-8, one line, at most MaxPendingAuthorLen code points, normalized.
76// The empty name is valid.
77func ParsePendingAuthor(s string) (string, error) {
78	if !utf8.ValidString(s) {
79		return "", &TextError{Reason: "is not valid UTF-8"}
80	}
81	c := cleanText(s, false)
82	if n := utf8.RuneCountInString(c); n > MaxPendingAuthorLen {
83		return "", &TextError{Reason: fmt.Sprintf("is longer than %d characters (%d)", MaxPendingAuthorLen, n)}
84	}
85	return c, nil
86}
87
88// AnonymousAuthor labels an approved issue whose submitter gave no name.
89const AnonymousAuthor = "anonymous"
90
91// ApprovedIssue is the open, external issue a pending submission becomes when
92// the owner approves it at now. The store sets the number. A submitter who gave
93// no name is AnonymousAuthor; the issue keeps the submission time as its
94// creation time.
95func ApprovedIssue(p PendingIssue, now time.Time) Issue {
96	author := p.Author
97	if author == "" {
98		author = AnonymousAuthor
99	}
100	return Issue{
101		Repo: p.Repo, Title: p.Title, Body: p.Body, State: IssueStateOpen,
102		Author: author, CreatedAt: p.CreatedAt, UpdatedAt: now, External: true,
103	}
104}
105
106// PendingIssue is an issue submitted through the public form, awaiting the
107// owner's decision. It has no number: approval gives it one.
108type PendingIssue struct {
109	ID    PendingIssueID
110	Repo  RepoName
111	Title IssueTitle
112	Body  IssueBody
113	// Author is the display name the submitter gave; it may be empty and is
114	// not verified.
115	Author    string
116	CreatedAt time.Time
117	// IPHash is the HMAC-SHA256 of the client key, IPHashLen bytes. It lets
118	// the owner ban a submitter without the server keeping an address.
119	IPHash []byte
120	// Version is set by the store on read; opaque to everyone else.
121	Version uint64
122}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 45

↑↓ moveenter openesc close