added ability to disable third-party verification

This commit is contained in:
onysd 2026-08-06 05:27:48 +03:00
parent 4f0fa895c1
commit 36350f83dc
17 changed files with 211 additions and 69 deletions

View file

@ -234,6 +234,14 @@ TELESRV_VERIFICATION_ALLOW_USER_TARGETS=false
# peer's name) -- a separate mechanism from the checkmark above, not the # peer's name) -- a separate mechanism from the checkmark above, not the
# platform badge. Its tuning lives in the Advanced section below. # platform badge. Its tuning lives in the Advanced section below.
TELESRV_BOT_VERIFICATION_ENABLED=true TELESRV_BOT_VERIFICATION_ENABLED=true
# THIS FEATURE IS NOT FULLY FINISHED AND MAY CAUSE UNSTABLE SERVER BEHAVIOR.
# Hides third-party bot verification instead of removing it: the admin panel
# drops its "Third-party marks" nav entry and refuses every underlying route
# with 404 regardless of session permissions, and the built-in @marksbot
# service bot stops responding to messages entirely. Everything stays wired
# up (nothing is deleted), so setting this to false re-enables it. Default
# true -- leave it alone unless you are specifically testing this feature.
TELESRV_HIDE_THIRD_PARTY_VERIFICATION=true
# Local composite account-rating score (stars/activity/moderation-based). # Local composite account-rating score (stars/activity/moderation-based).
# Shown to every viewer via userFull.stars_rating, not admin-only. Scoring # Shown to every viewer via userFull.stars_rating, not admin-only. Scoring
# weights and recompute timing live in the Advanced section below. # weights and recompute timing live in the Advanced section below.

View file

@ -27,9 +27,24 @@ import (
// journal, the status machine and the optimistic lock are enforced in one place and // journal, the status machine and the optimistic lock are enforced in one place and
// a panel action is indistinguishable from an API one in the audit trail. // a panel action is indistinguishable from an API one in the audit trail.
// requireThirdPartyVerificationVisible refuses every third-party verification
// route while the feature is hidden (TELESRV_HIDE_THIRD_PARTY_VERIFICATION,
// default true), regardless of session permissions -- the feature is not
// fully finished and may cause unstable server behavior, so hiding it is
// enforced here, not just by the panel dropping its nav entry.
func (s *server) requireThirdPartyVerificationVisible(handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if s.cfg.HideThirdPartyVerification {
http.NotFound(w, r)
return
}
handler(w, r)
}
}
// botVerificationRead mounts a route behind a session and botverification.review. // botVerificationRead mounts a route behind a session and botverification.review.
func (s *server) botVerificationRead(handler http.HandlerFunc) http.Handler { func (s *server) botVerificationRead(handler http.HandlerFunc) http.Handler {
return s.requireAuthAPI(s.requirePermission(permissionBotVerificationReview, handler)) return s.requireAuthAPI(s.requirePermission(permissionBotVerificationReview, s.requireThirdPartyVerificationVisible(handler)))
} }
// botVerificationManage mounts a route behind a session and botverification.manage. // botVerificationManage mounts a route behind a session and botverification.manage.
@ -38,7 +53,7 @@ func (s *server) botVerificationRead(handler http.HandlerFunc) http.Handler {
// a verifier and working its queue are different jobs, so an operator may hold // a verifier and working its queue are different jobs, so an operator may hold
// either without the other. // either without the other.
func (s *server) botVerificationManage(handler http.HandlerFunc) http.Handler { func (s *server) botVerificationManage(handler http.HandlerFunc) http.Handler {
return s.requireAuthAPI(s.requirePermission(permissionBotVerificationManage, handler)) return s.requireAuthAPI(s.requirePermission(permissionBotVerificationManage, s.requireThirdPartyVerificationVisible(handler)))
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -76,6 +76,12 @@ type uiConfig struct {
// entry, so introducing the permission model never locks an operator out of a // entry, so introducing the permission model never locks an operator out of a
// panel that worked before. // panel that worked before.
Permissions []string Permissions []string
// HideThirdPartyVerification mirrors config.HideThirdPartyVerification
// (TELESRV_HIDE_THIRD_PARTY_VERIFICATION, default true): while true, every
// botverification.* route refuses with 404 regardless of session
// permissions, and the session/login response tells the frontend to hide
// the "Third-party marks" nav entry and its routes.
HideThirdPartyVerification bool
} }
// loadConfig 通过 internal/config.Load() 加载 .env 配置文件与环境变量, // loadConfig 通过 internal/config.Load() 加载 .env 配置文件与环境变量,
@ -103,14 +109,15 @@ func loadConfig() (uiConfig, error) {
sum := sha256.Sum256([]byte(appCfg.AdminSessionKey)) sum := sha256.Sum256([]byte(appCfg.AdminSessionKey))
return uiConfig{ return uiConfig{
Addr: appCfg.AdminUIAddr, Addr: appCfg.AdminUIAddr,
PostgresDSN: appCfg.PostgresDSN, PostgresDSN: appCfg.PostgresDSN,
AdminAPIURL: adminAPIURL(adminAPIAddr), AdminAPIURL: adminAPIURL(adminAPIAddr),
AdminAPIToken: appCfg.AdminAPIToken, AdminAPIToken: appCfg.AdminAPIToken,
Password: appCfg.AdminUIPassword, Password: appCfg.AdminUIPassword,
Token: appCfg.AdminUIToken, Token: appCfg.AdminUIToken,
SessionKey: sum[:], SessionKey: sum[:],
Permissions: appCfg.AdminUIPermissions, Permissions: appCfg.AdminUIPermissions,
HideThirdPartyVerification: appCfg.HideThirdPartyVerification,
}, nil }, nil
} }

View file

@ -251,9 +251,10 @@ func (s *server) handleAPILogin(w http.ResponseWriter, r *http.Request) {
}) })
setCSRFCookie(w, csrfToken, sessionTTL) setCSRFCookie(w, csrfToken, sessionTTL)
writeJSON(w, http.StatusOK, map[string]any{ writeJSON(w, http.StatusOK, map[string]any{
"actor": "admin", "actor": "admin",
"permissions": permissions.List(), "permissions": permissions.List(),
"csrf_token": csrfToken, "csrf_token": csrfToken,
"hide_third_party_verification": s.cfg.HideThirdPartyVerification,
}) })
} }
@ -277,8 +278,9 @@ func (s *server) handleAPILogout(w http.ResponseWriter, _ *http.Request) {
// than letting them walk into a 403. // than letting them walk into a 403.
func (s *server) handleSession(w http.ResponseWriter, r *http.Request) { func (s *server) handleSession(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{ writeJSON(w, http.StatusOK, map[string]any{
"actor": actorFromContext(r.Context()), "actor": actorFromContext(r.Context()),
"permissions": permissionsFromContext(r.Context()).List(), "permissions": permissionsFromContext(r.Context()).List(),
"hide_third_party_verification": s.cfg.HideThirdPartyVerification,
}) })
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -23,7 +23,7 @@
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-CP1sn_7z.js"></script> <script type="module" crossorigin src="/assets/index-BUraMCki.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-XySYVUb7.css"> <link rel="stylesheet" crossorigin href="/assets/index-XySYVUb7.css">
</head> </head>
<body> <body>

View file

@ -41,7 +41,7 @@ export function App() {
} }
return ( return (
<PermissionsProvider permissions={session.permissions ?? []}> <PermissionsProvider permissions={session.permissions ?? []} hideThirdPartyVerification={session.hide_third_party_verification ?? true}>
<Shell actor={session.actor} route={route} navigate={navigate} onLogout={() => setSession(null)}> <Shell actor={session.actor} route={route} navigate={navigate} onLogout={() => setSession(null)}>
<Routes route={route} navigate={navigate} /> <Routes route={route} navigate={navigate} />
</Shell> </Shell>

View file

@ -19,7 +19,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState, type ReactNode } from "react"; import { useEffect, useState, type ReactNode } from "react";
import { api } from "../api"; import { api } from "../api";
import { permissionBotVerificationReview, permissionVerificationReview, useCan } from "../permissions"; import { permissionBotVerificationReview, permissionVerificationReview, useCan, useThirdPartyVerificationHidden } from "../permissions";
import { type Navigate, type RouteState, routeTitle } from "../routing"; import { type Navigate, type RouteState, routeTitle } from "../routing";
import { ThemeSwitch } from "../theme"; import { ThemeSwitch } from "../theme";
import { AppLink } from "./AppLink"; import { AppLink } from "./AppLink";
@ -58,6 +58,9 @@ export function Shell({
// Same reasoning for the third-party queue, which has its own right: the two // Same reasoning for the third-party queue, which has its own right: the two
// sections are granted independently, so one entry can be visible without the other. // sections are granted independently, so one entry can be visible without the other.
const canReviewBotVerification = useCan(permissionBotVerificationReview); const canReviewBotVerification = useCan(permissionBotVerificationReview);
// Third-party verification is additionally hidden by default (not fully
// finished) regardless of what the session was granted -- see permissions.tsx.
const thirdPartyVerificationHidden = useThirdPartyVerificationHidden();
const messagesActive = route.path.startsWith("/messages"); const messagesActive = route.path.startsWith("/messages");
const [messagesOpen, setMessagesOpen] = useState(messagesActive); const [messagesOpen, setMessagesOpen] = useState(messagesActive);
@ -92,7 +95,7 @@ export function Shell({
{canReviewVerification && ( {canReviewVerification && (
<NavLink icon={<BadgeCheck size={16} />} href="/verification" route={route} navigate={navigate}>{"Verification"}</NavLink> <NavLink icon={<BadgeCheck size={16} />} href="/verification" route={route} navigate={navigate}>{"Verification"}</NavLink>
)} )}
{canReviewBotVerification && ( {canReviewBotVerification && !thirdPartyVerificationHidden && (
<NavLink icon={<Stamp size={16} />} href="/bot-verification" route={route} navigate={navigate}>{"Third-party marks"}</NavLink> <NavLink icon={<Stamp size={16} />} href="/bot-verification" route={route} navigate={navigate}>{"Third-party marks"}</NavLink>
)} )}
<NavLink icon={<AtSign size={16} />} href="/collectible-usernames" route={route} navigate={navigate}>{"NFT Usernames"}</NavLink> <NavLink icon={<AtSign size={16} />} href="/collectible-usernames" route={route} navigate={navigate}>{"NFT Usernames"}</NavLink>

View file

@ -27,6 +27,7 @@ import { VerificationDetailPage } from "./VerificationDetailPage";
import { VerificationPage } from "./VerificationPage"; import { VerificationPage } from "./VerificationPage";
import { import {
PermissionGate, PermissionGate,
ThirdPartyVerificationHiddenGate,
permissionBotVerificationReview, permissionBotVerificationReview,
permissionVerificationReview permissionVerificationReview
} from "../permissions"; } from "../permissions";
@ -45,16 +46,20 @@ export function Routes({ route, navigate }: { route: RouteState; navigate: Navig
const botVerificationRequestID = route.path.match(/^\/bot-verification\/(\d+)$/)?.[1]; const botVerificationRequestID = route.path.match(/^\/bot-verification\/(\d+)$/)?.[1];
if (botVerificationRequestID) { if (botVerificationRequestID) {
return ( return (
<PermissionGate permission={permissionBotVerificationReview}> <ThirdPartyVerificationHiddenGate>
<BotVerificationRequestPage id={botVerificationRequestID} navigate={navigate} /> <PermissionGate permission={permissionBotVerificationReview}>
</PermissionGate> <BotVerificationRequestPage id={botVerificationRequestID} navigate={navigate} />
</PermissionGate>
</ThirdPartyVerificationHiddenGate>
); );
} }
if (route.path === "/bot-verification") { if (route.path === "/bot-verification") {
return ( return (
<PermissionGate permission={permissionBotVerificationReview}> <ThirdPartyVerificationHiddenGate>
<BotVerificationPage navigate={navigate} /> <PermissionGate permission={permissionBotVerificationReview}>
</PermissionGate> <BotVerificationPage navigate={navigate} />
</PermissionGate>
</ThirdPartyVerificationHiddenGate>
); );
} }
// The detail match has to be tested before the exact "/verification" branch, and // The detail match has to be tested before the exact "/verification" branch, and

View file

@ -17,20 +17,32 @@ export const permissionBotVerificationManage = "botverification.manage";
// section the session may not use is hidden instead of rendered into a 403. This // section the session may not use is hidden instead of rendered into a 403. This
// is a convenience for the operator, not a security boundary: every route is // is a convenience for the operator, not a security boundary: every route is
// checked again server-side. // checked again server-side.
const PermissionsContext = createContext<readonly string[]>([]); type SessionFlags = {
permissions: readonly string[];
// Mirrors AdminSession.hide_third_party_verification. Deliberately NOT folded
// into the permission list: it applies regardless of what the session was
// granted (even "*"), because the feature is not fully finished rather than
// merely restricted.
hideThirdPartyVerification: boolean;
};
const PermissionsContext = createContext<SessionFlags>({ permissions: [], hideThirdPartyVerification: true });
export function PermissionsProvider({ export function PermissionsProvider({
permissions, permissions,
hideThirdPartyVerification = true,
children children
}: { }: {
permissions: readonly string[]; permissions: readonly string[];
hideThirdPartyVerification?: boolean;
children: ReactNode; children: ReactNode;
}) { }) {
return <PermissionsContext.Provider value={permissions}>{children}</PermissionsContext.Provider>; const value = useMemo(() => ({ permissions, hideThirdPartyVerification }), [permissions, hideThirdPartyVerification]);
return <PermissionsContext.Provider value={value}>{children}</PermissionsContext.Provider>;
} }
export function usePermissions(): { permissions: readonly string[]; can: (permission: string) => boolean } { export function usePermissions(): { permissions: readonly string[]; can: (permission: string) => boolean } {
const permissions = useContext(PermissionsContext); const { permissions } = useContext(PermissionsContext);
return useMemo( return useMemo(
() => ({ () => ({
permissions, permissions,
@ -44,6 +56,13 @@ export function useCan(permission: string): boolean {
return usePermissions().can(permission); return usePermissions().can(permission);
} }
// useThirdPartyVerificationHidden reports the server's
// TELESRV_HIDE_THIRD_PARTY_VERIFICATION setting (default true). Unlike
// useCan, this is never overridden by a "*" session -- see SessionFlags.
export function useThirdPartyVerificationHidden(): boolean {
return useContext(PermissionsContext).hideThirdPartyVerification;
}
// PermissionGate is what a direct URL hits: without the right the operator gets // PermissionGate is what a direct URL hits: without the right the operator gets
// an explanation naming the missing permission, not an empty table that looks // an explanation naming the missing permission, not an empty table that looks
// like "no data". // like "no data".
@ -70,3 +89,26 @@ export function PermissionDenied({ permission }: { permission: string }) {
</PageFrame> </PageFrame>
); );
} }
// ThirdPartyVerificationHiddenGate is what a direct URL to a third-party
// verification page hits while the feature is hidden -- distinct from
// PermissionGate because no permission grant (not even "*") changes this.
export function ThirdPartyVerificationHiddenGate({ children }: { children: ReactNode }) {
const hidden = useThirdPartyVerificationHidden();
if (!hidden) {
return <>{children}</>;
}
return (
<PageFrame title={"Feature hidden"} eyebrow={"Console / Third-party marks"}>
<Alert>{"Third-party bot verification is hidden on this server (TELESRV_HIDE_THIRD_PARTY_VERIFICATION=true)."}</Alert>
<section className="section-block">
<div className="entity-head">
<div>
<div className="entity-title"><ShieldOff size={16} /> {"Not fully finished"}</div>
<div className="entity-subtitle">{"This feature may cause unstable server behavior and is hidden by default. Set TELESRV_HIDE_THIRD_PARTY_VERIFICATION=false to re-enable it."}</div>
</div>
</div>
</section>
</PageFrame>
);
}

View file

@ -718,6 +718,13 @@ export type AdminSession = {
actor: string; actor: string;
// The right set the signed session was issued with; ["*"] means everything. // The right set the signed session was issued with; ["*"] means everything.
permissions?: string[] | null; permissions?: string[] | null;
// Mirrors the server's TELESRV_HIDE_THIRD_PARTY_VERIFICATION (default true):
// while true, the panel drops the "Third-party marks" nav entry and its
// routes, regardless of what permissions the session carries -- the feature
// is not fully finished. The server also refuses the underlying routes with
// 404, so this is a UI convenience on top of a real enforcement, not the
// enforcement itself.
hide_third_party_verification?: boolean;
}; };
export type AdminLoginResult = AdminSession & { export type AdminLoginResult = AdminSession & {

View file

@ -1006,7 +1006,8 @@ func run(logger *zap.Logger) error {
botsapp.WithUserStickerSets(accountService), botsapp.WithUserStickerSets(accountService),
botsapp.WithTelegramLogin(telegramLoginService), botsapp.WithTelegramLogin(telegramLoginService),
botsapp.WithDialogRateLimiter(rateLimiter, cfg.VerificationBotRateLimit, cfg.VerificationBotRateWindow), botsapp.WithDialogRateLimiter(rateLimiter, cfg.VerificationBotRateLimit, cfg.VerificationBotRateWindow),
botsapp.WithPublicBaseURL(cfg.PublicBaseURL)) botsapp.WithPublicBaseURL(cfg.PublicBaseURL),
botsapp.WithHideThirdPartyVerification(cfg.HideThirdPartyVerification))
groupCallStore := postgres.NewGroupCallStore(pool) groupCallStore := postgres.NewGroupCallStore(pool)
groupCallsService := groupcallsapp.NewService(groupCallStore, groupcallsapp.WithPublicBaseURL(cfg.PublicBaseURL)) groupCallsService := groupcallsapp.NewService(groupCallStore, groupcallsapp.WithPublicBaseURL(cfg.PublicBaseURL))
// 群通话媒体面:内嵌 pion SFUM1+。SFU 的 liveness reporter 把媒体面存活 // 群通话媒体面:内嵌 pion SFUM1+。SFU 的 liveness reporter 把媒体面存活

View file

@ -88,8 +88,14 @@ func (s *Service) HandlesBot(botUserID int64) bool {
return false return false
} }
switch botUserID { switch botUserID {
case domain.VerifierBotUserID:
// @marksbot fronts THIRD-PARTY verification, which is hidden by default
// (config.HideThirdPartyVerification) because the feature is not fully
// finished -- while hidden, the bot doesn't exist as far as the message
// pipeline is concerned.
return !s.hideThirdPartyVerification
case domain.BotFatherUserID, domain.StickersBotUserID, domain.ChatBotUserID, case domain.BotFatherUserID, domain.StickersBotUserID, domain.ChatBotUserID,
domain.VerifyBotUserID, domain.VerifierBotUserID: domain.VerifyBotUserID:
return true return true
default: default:
return false return false

View file

@ -119,6 +119,11 @@ type Service struct {
now func() time.Time now func() time.Time
chatBotStreamThrottle time.Duration chatBotStreamThrottle time.Duration
publicBaseURL string publicBaseURL string
// hideThirdPartyVerification mirrors config.HideThirdPartyVerification: while
// true, HandlesBot refuses VerifierBotUserID so @marksbot never answers a
// message. The feature is not fully finished and defaults to hidden; see the
// config field's doc comment.
hideThirdPartyVerification bool
// dialogLimiter bounds how often one applicant can drive a service-bot dialog. // dialogLimiter bounds how often one applicant can drive a service-bot dialog.
// The verification service already rate-limits application creation; this is the // The verification service already rate-limits application creation; this is the
// separate bound on dialog traffic itself, so a script cannot spin the state // separate bound on dialog traffic itself, so a script cannot spin the state
@ -234,6 +239,15 @@ func WithCustomVerification(v customVerifications) Option {
} }
} }
// WithHideThirdPartyVerification mirrors config.HideThirdPartyVerification:
// while true, HandlesBot refuses VerifierBotUserID, so @marksbot never
// receives or answers a message.
func WithHideThirdPartyVerification(hidden bool) Option {
return func(s *Service) {
s.hideThirdPartyVerification = hidden
}
}
// WithVerifierTargets injects the directory of an applicant's own peers used by // WithVerifierTargets injects the directory of an applicant's own peers used by
// @verifierbot's subject picker. It is optional: with nothing injected the bot // @verifierbot's subject picker. It is optional: with nothing injected the bot
// falls back to the official verification service's EligibleTargets, which // falls back to the official verification service's EligibleTargets, which

View file

@ -415,6 +415,28 @@ func TestVerifierBotStartWithoutVerifierStatusIsHonest(t *testing.T) {
} }
} }
// TestVerifierBotHiddenByThirdPartyVerificationFlag proves
// config.HideThirdPartyVerification actually silences @marksbot: with the
// option set, HandlesBot must refuse the bot's id entirely, so
// OnPrivateMessage never even reaches the dialog logic -- not just an error
// reply, no reply at all, matching every other unhandled bot id.
func TestVerifierBotHiddenByThirdPartyVerificationFlag(t *testing.T) {
cv := newFakeCustomVerification()
svc, users, messages := newVerifierBotTestService(t, cv, WithHideThirdPartyVerification(true))
owner := newOwner(t, users, "+7201")
if svc.HandlesBot(domain.VerifierBotUserID) {
t.Fatal("service should refuse @verifierbot while third-party verification is hidden")
}
svc.OnPrivateMessage(context.Background(), domain.VerifierBotUserID, domain.Message{
From: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID},
Body: "/start",
})
if replies := verifierReplies(t, messages, owner.ID); len(replies) != 0 {
t.Fatalf("hidden @verifierbot replied: %+v", replies)
}
}
func TestVerifierBotStartWithActiveVerifierShowsCompanyAndMark(t *testing.T) { func TestVerifierBotStartWithActiveVerifierShowsCompanyAndMark(t *testing.T) {
cv := newFakeCustomVerification().activated() cv := newFakeCustomVerification().activated()
svc, users, messages := newVerifierBotTestService(t, cv) svc, users, messages := newVerifierBotTestService(t, cv)

View file

@ -548,6 +548,15 @@ type Config struct {
// verifier bots. 0 for either disables the budget. // verifier bots. 0 for either disables the budget.
BotVerificationRequestRateLimit int BotVerificationRequestRateLimit int
BotVerificationRequestRateWindow time.Duration BotVerificationRequestRateWindow time.Duration
// HideThirdPartyVerification hides third-party bot verification instead of
// removing it: the admin panel drops its "Third-party marks" nav entry and
// refuses every botverification.* route with 404 (regardless of session
// permissions), and the built-in @marksbot service bot stops responding to
// messages entirely. Defaults to true because this feature is NOT FULLY
// FINISHED and may cause unstable server behavior -- it stays wired up
// (nothing is deleted) so it can be re-enabled later, but it should not be
// exposed on a deployment that isn't specifically testing it.
HideThirdPartyVerification bool
// CollectibleUsernameURLTemplate is the landing URL recorded on a minted // CollectibleUsernameURLTemplate is the landing URL recorded on a minted
// collectible username when the mint request carries no explicit URL. // collectible username when the mint request carries no explicit URL.
@ -766,35 +775,35 @@ func Load() (Config, error) {
RedisPassword: envOr("TELESRV_REDIS_PASSWORD", ""), RedisPassword: envOr("TELESRV_REDIS_PASSWORD", ""),
RedisDB: envIntOr("TELESRV_REDIS_DB", 0), RedisDB: envIntOr("TELESRV_REDIS_DB", 0),
DevAuthCode: envOr("TELESRV_DEV_AUTH_CODE", "12345"), DevAuthCode: envOr("TELESRV_DEV_AUTH_CODE", "12345"),
AuthCodeTTL: envDurationOr("TELESRV_AUTH_CODE_TTL", 5*time.Minute), AuthCodeTTL: envDurationOr("TELESRV_AUTH_CODE_TTL", 5*time.Minute),
PhoneCodeLength: envIntOr("TELESRV_PHONE_CODE_LENGTH", 5), PhoneCodeLength: envIntOr("TELESRV_PHONE_CODE_LENGTH", 5),
AuthCodeMaxAttempts: envIntOr("TELESRV_AUTH_CODE_MAX_ATTEMPTS", 5), AuthCodeMaxAttempts: envIntOr("TELESRV_AUTH_CODE_MAX_ATTEMPTS", 5),
AuthCodePhoneRateLimit: envIntOr("TELESRV_AUTH_CODE_PHONE_RATE_LIMIT", 5), AuthCodePhoneRateLimit: envIntOr("TELESRV_AUTH_CODE_PHONE_RATE_LIMIT", 5),
AuthCodeAuthKeyRateLimit: envIntOr("TELESRV_AUTH_CODE_AUTH_KEY_RATE_LIMIT", 20), AuthCodeAuthKeyRateLimit: envIntOr("TELESRV_AUTH_CODE_AUTH_KEY_RATE_LIMIT", 20),
AuthCodeRateWindow: envDurationOr("TELESRV_AUTH_CODE_RATE_WINDOW", 10*time.Minute), AuthCodeRateWindow: envDurationOr("TELESRV_AUTH_CODE_RATE_WINDOW", 10*time.Minute),
LoginEmailEnable: envBoolOr("TELESRV_LOGIN_EMAIL_ENABLE", false), LoginEmailEnable: envBoolOr("TELESRV_LOGIN_EMAIL_ENABLE", false),
LoginEmailRequireSetup: envBoolOr("TELESRV_LOGIN_EMAIL_REQUIRE_SETUP", false), LoginEmailRequireSetup: envBoolOr("TELESRV_LOGIN_EMAIL_REQUIRE_SETUP", false),
EmailSignupEnable: envBoolOr("TELESRV_EMAIL_SIGNUP_ENABLE", false), EmailSignupEnable: envBoolOr("TELESRV_EMAIL_SIGNUP_ENABLE", false),
EmailSignupPhonePrefixes: envListOr("TELESRV_EMAIL_SIGNUP_PHONE_PREFIXES", []string{"888"}), EmailSignupPhonePrefixes: envListOr("TELESRV_EMAIL_SIGNUP_PHONE_PREFIXES", []string{"888"}),
LoginEmailCodeLength: envIntOr("TELESRV_LOGIN_EMAIL_CODE_LENGTH", 6), LoginEmailCodeLength: envIntOr("TELESRV_LOGIN_EMAIL_CODE_LENGTH", 6),
PhoneCodeDeliveryProvider: strings.ToLower(strings.TrimSpace(envOr("TELESRV_PHONE_CODE_DELIVERY_PROVIDER", "development"))), PhoneCodeDeliveryProvider: strings.ToLower(strings.TrimSpace(envOr("TELESRV_PHONE_CODE_DELIVERY_PROVIDER", "development"))),
EmailCodeDeliveryProvider: strings.ToLower(strings.TrimSpace(envOr("TELESRV_EMAIL_CODE_DELIVERY_PROVIDER", "smtp"))), EmailCodeDeliveryProvider: strings.ToLower(strings.TrimSpace(envOr("TELESRV_EMAIL_CODE_DELIVERY_PROVIDER", "smtp"))),
OTPWebhookURL: envOr("TELESRV_OTP_WEBHOOK_URL", ""), OTPWebhookURL: envOr("TELESRV_OTP_WEBHOOK_URL", ""),
OTPWebhookSecret: envOr("TELESRV_OTP_WEBHOOK_SECRET", ""), OTPWebhookSecret: envOr("TELESRV_OTP_WEBHOOK_SECRET", ""),
OTPWebhookTimeout: envDurationOr("TELESRV_OTP_WEBHOOK_TIMEOUT", 5*time.Second), OTPWebhookTimeout: envDurationOr("TELESRV_OTP_WEBHOOK_TIMEOUT", 5*time.Second),
SMTPHost: envOr("TELESRV_SMTP_HOST", ""), SMTPHost: envOr("TELESRV_SMTP_HOST", ""),
SMTPPort: envIntOr("TELESRV_SMTP_PORT", 587), SMTPPort: envIntOr("TELESRV_SMTP_PORT", 587),
SMTPUsername: envOr("TELESRV_SMTP_USERNAME", ""), SMTPUsername: envOr("TELESRV_SMTP_USERNAME", ""),
SMTPPassword: envOr("TELESRV_SMTP_PASSWORD", ""), SMTPPassword: envOr("TELESRV_SMTP_PASSWORD", ""),
SMTPFrom: envOr("TELESRV_SMTP_FROM", ""), SMTPFrom: envOr("TELESRV_SMTP_FROM", ""),
SMTPFromName: envOr("TELESRV_SMTP_FROM_NAME", "OwpenGram"), SMTPFromName: envOr("TELESRV_SMTP_FROM_NAME", "OwpenGram"),
SMTPTLSMode: strings.ToLower(strings.TrimSpace(envOr("TELESRV_SMTP_TLS", "starttls"))), SMTPTLSMode: strings.ToLower(strings.TrimSpace(envOr("TELESRV_SMTP_TLS", "starttls"))),
SMTPTimeout: envDurationOr("TELESRV_SMTP_TIMEOUT", 10*time.Second), SMTPTimeout: envDurationOr("TELESRV_SMTP_TIMEOUT", 10*time.Second),
LangPackSeedDir: envOr("TELESRV_LANGPACK_SEED_DIR", "data/langpack"), LangPackSeedDir: envOr("TELESRV_LANGPACK_SEED_DIR", "data/langpack"),
OfficialGiftsDir: envOr("TELESRV_OFFICIAL_GIFTS_DIR", "data/official-gifts"), OfficialGiftsDir: envOr("TELESRV_OFFICIAL_GIFTS_DIR", "data/official-gifts"),
StarGiftTONStartingGrant: envInt64Or("TELESRV_STARGIFT_TON_STARTING_GRANT", 10_000_000_000), StarGiftTONStartingGrant: envInt64Or("TELESRV_STARGIFT_TON_STARTING_GRANT", 10_000_000_000),
BlobDir: envOr("TELESRV_BLOB_DIR", "data/blobs"), BlobDir: envOr("TELESRV_BLOB_DIR", "data/blobs"),
// s3 (MinIO by default, see deploy/docker-compose.yml's minio service) is // s3 (MinIO by default, see deploy/docker-compose.yml's minio service) is
// the default blob backend; localfs remains fully supported as an // the default blob backend; localfs remains fully supported as an
// explicit opt-in (TELESRV_BLOB_BACKEND=localfs). // explicit opt-in (TELESRV_BLOB_BACKEND=localfs).
@ -937,6 +946,7 @@ func Load() (Config, error) {
// verifier bots, and filing with a second company is not a retry of the first. // verifier bots, and filing with a second company is not a retry of the first.
BotVerificationRequestRateLimit: envIntOr("TELESRV_BOT_VERIFICATION_REQUEST_RATE_LIMIT", 5), BotVerificationRequestRateLimit: envIntOr("TELESRV_BOT_VERIFICATION_REQUEST_RATE_LIMIT", 5),
BotVerificationRequestRateWindow: envDurationOr("TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW", 24*time.Hour), BotVerificationRequestRateWindow: envDurationOr("TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW", 24*time.Hour),
HideThirdPartyVerification: envBoolOr("TELESRV_HIDE_THIRD_PARTY_VERIFICATION", true),
GroupCallCheckTTL: envDurationOr("TELESRV_GROUPCALL_CHECK_TTL", 45*time.Second), GroupCallCheckTTL: envDurationOr("TELESRV_GROUPCALL_CHECK_TTL", 45*time.Second),
GroupCallSweepInterval: envDurationOr("TELESRV_GROUPCALL_SWEEP_INTERVAL", 10*time.Second), GroupCallSweepInterval: envDurationOr("TELESRV_GROUPCALL_SWEEP_INTERVAL", 10*time.Second),