added ability to disable third-party verification
This commit is contained in:
parent
4f0fa895c1
commit
36350f83dc
17 changed files with 211 additions and 69 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
10
cmd/telesrv-admin/web/dist/assets/index-BUraMCki.js
vendored
Normal file
10
cmd/telesrv-admin/web/dist/assets/index-BUraMCki.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
cmd/telesrv-admin/web/dist/index.html
vendored
2
cmd/telesrv-admin/web/dist/index.html
vendored
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 & {
|
||||||
|
|
|
||||||
|
|
@ -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 SFU(M1+)。SFU 的 liveness reporter 把媒体面存活
|
// 群通话媒体面:内嵌 pion SFU(M1+)。SFU 的 liveness reporter 把媒体面存活
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue