Files
llmgw/internal/web/licences.go
Ilker ManapandClaude Opus 5.5 4bbac0833d
CI / test (push) Successful in 11m28s
A person leaves licences out, and no key of theirs reaches a model under one
A game sold in the EU cannot ship what a licence excluding the EU made. On
the profile page a person leaves out licences one by one, or by rule:
every licence forbidding commercial use, every licence granting nothing in
a region. The rules cover models added later. The choice reaches every
principal through the resolved limits, so permits() leaves such models out
of /v1/models, requests, jobs, fallbacks and the portal's pickers, and a
request for one is refused 403 licence_excluded saying which choice did.

Kept in user_excluded_licences (migration 0046), copied with the store and
gone with the person.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 16:10:22 +02:00

143 lines
4.4 KiB
Go

package web
import (
"context"
"net/http"
"regexp"
"slices"
"strings"
)
// LicenceChoice is one licence the catalog's models are under, as the
// profile page offers it.
type LicenceChoice struct {
// Key is what is stored when it is ticked: the licence's id, else its
// name, lowercased; "unspecified" for models whose entry names none.
Key string
Name, URL string
// Commercial is "yes" or "no" as the licence says, empty when it does
// not say.
Commercial string
ExcludedRegions []string
Models []string
// Excluded is whether the person has left it out.
Excluded bool
}
// LicenceChoices is the licences on offer, and what the rules can be about.
type LicenceChoices struct {
Licences []LicenceChoice
// Regions are those some licence in the catalog grants nothing in.
Regions []string
// AnyNonCommercial is whether some licence forbids commercial use.
AnyNonCommercial bool
// What the person has chosen, for the rules.
NonCommercial bool
ExcludedRegions []string
}
// LicencePrefs keeps what a person has said not to use.
type LicencePrefs interface {
UserExcludedLicences(ctx context.Context, userID string) ([]string, error)
SetUserExcludedLicences(ctx context.Context, userID string, licences []string) error
}
// Rule words, matched by the router.
const (
licenceRuleNonCommercial = "rule:non-commercial"
licenceRuleRegion = "rule:region:"
)
var regionCode = regexp.MustCompile(`^[A-Z]{2}$`)
// SetLicences offers the choice of licences on the profile page: what the
// catalog's models are under, and where the choices are kept.
func (p *AccountPages) SetLicences(choices func() LicenceChoices, prefs LicencePrefs) {
p.licenceChoices, p.licencePrefs = choices, prefs
}
// fillLicences puts the choice, and what the person has made of it, on the
// profile page; nothing when it is not offered.
func (p *AccountPages) fillLicences(ctx context.Context, userID string, d *profileData) {
if p.licenceChoices == nil || p.licencePrefs == nil {
return
}
c := p.licenceChoices()
ex, err := p.licencePrefs.UserExcludedLicences(ctx, userID)
if err != nil {
p.log.Error("could not read a person's licence choices", "err", err)
}
for i := range c.Licences {
c.Licences[i].Excluded = slices.ContainsFunc(ex, func(e string) bool { return strings.EqualFold(e, c.Licences[i].Key) })
}
for _, e := range ex {
switch {
case e == licenceRuleNonCommercial:
c.NonCommercial = true
case strings.HasPrefix(e, licenceRuleRegion):
c.ExcludedRegions = append(c.ExcludedRegions, strings.TrimPrefix(e, licenceRuleRegion))
}
}
// A region the person excluded that no licence in the catalog names yet
// is still offered, ticked: it is there for the model added next.
for _, reg := range c.ExcludedRegions {
if !slices.Contains(c.Regions, reg) {
c.Regions = append(c.Regions, reg)
}
}
slices.Sort(c.Regions)
d.Licences = &c
}
func (p *AccountPages) setLicences(w http.ResponseWriter, r *http.Request) {
noStore(w)
sess, user, ok := p.session(r)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
if p.licencePrefs == nil {
http.NotFound(w, r)
return
}
if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
var ex []string
for _, k := range r.Form["licence"] {
if k = strings.TrimSpace(k); k != "" && !strings.HasPrefix(k, "rule:") {
ex = append(ex, strings.ToLower(k))
}
}
if r.FormValue("non_commercial") != "" {
ex = append(ex, licenceRuleNonCommercial)
}
regions := r.Form["region"]
regions = append(regions, strings.FieldsFunc(r.FormValue("regions_other"), func(c rune) bool {
return c == ',' || c == ' '
})...)
l := langOf(r)
for _, reg := range regions {
reg = strings.ToUpper(strings.TrimSpace(reg))
if reg == "" {
continue
}
if !regionCode.MatchString(reg) {
p.render(w, r, sess, user, profileData{LicenceError: l.T("profile.licences.err_region", reg)})
return
}
ex = append(ex, licenceRuleRegion+reg)
}
if err := p.licencePrefs.SetUserExcludedLicences(r.Context(), user.ID, ex); err != nil {
p.log.Error("could not keep a person's licence choices", "err", err)
p.render(w, r, sess, user, profileData{LicenceError: l.T("profile.err.try_again")})
return
}
p.render(w, r, sess, user, profileData{LicencesSaved: true})
}