Compare commits
2 commits
6af4576fed
...
4d3c6beddf
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d3c6beddf | |||
| c39011e542 |
5 changed files with 3861 additions and 5 deletions
119
docs/deep-links.md
Normal file
119
docs/deep-links.md
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
# Client deep links
|
||||||
|
|
||||||
|
The desktop client (`owpengram-desktop-client`, a Telegram Desktop fork) resolves
|
||||||
|
`tg://<path>` URIs and `https://t.me/<path>` links through the same handler table:
|
||||||
|
`Telegram/SourceFiles/core/local_url_handlers.cpp`. Both schemes are normalized to the
|
||||||
|
same `<path>?<query>` form before dispatch, so every entry below works as either
|
||||||
|
`tg://join?invite=...` or `t.me/join/...` (the client also accepts the shorter
|
||||||
|
`t.me/+...` / `t.me/joinchat/...` aliases, which get rewritten to `join?invite=`
|
||||||
|
before reaching this table).
|
||||||
|
|
||||||
|
Patterns are matched in order top to bottom; the first regex match wins.
|
||||||
|
|
||||||
|
## Path handlers
|
||||||
|
|
||||||
|
| Path | Handler | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `join?invite=<hash>` | `JoinGroupByHash` | Join a chat/channel via invite link hash. |
|
||||||
|
| `addlist?slug=<slug>` | `JoinFilterBySlug` | Import a shareable chat folder. |
|
||||||
|
| `addstickers?set=<name>` / `addemoji?set=<name>` | `ShowStickerSet` | Open a sticker or custom-emoji set for install. |
|
||||||
|
| `addtheme?slug=<slug>` | `ShowTheme` | Open a shared theme for install. |
|
||||||
|
| `addstyle?slug=<slug>` | `ShowAiStyle` | Open a shared AI compose style. |
|
||||||
|
| `setlanguage?lang=<code>` | `SetLanguage` | Switch/install the app's interface language. |
|
||||||
|
| `msg_url?<params>` | `ShareUrl` | Open the "share to chat" composer for an external URL. |
|
||||||
|
| `confirmphone?<params>` | `ConfirmPhone` | Confirm a phone number transfer/login code sent by SMS. |
|
||||||
|
| `socks?<params>` | `ApplySocksProxy` | Add/apply a SOCKS5 proxy config. |
|
||||||
|
| `proxy?<params>` | `ApplyMtprotoProxy` | Add/apply an MTProto proxy config. |
|
||||||
|
| `webproxy?<params>` | `ApplyWebProxy` | Add/apply a web proxy config. |
|
||||||
|
| `passport?<params>` | `ShowPassport` | Telegram Passport authorization request. |
|
||||||
|
| `bg?<params>` | `ShowWallPaper` | Open a shared chat wallpaper for install. |
|
||||||
|
| `resolve?<params>` | `ResolveUsernameOrPhone` | Resolve `@username`/phone to a peer; see [resolve parameters](#resolve-parameters) below. This is the most common deep link kind. |
|
||||||
|
| `privatepost?<params>` | `ResolvePrivatePost` | Open a specific message inside a private channel by internal channel id + message id. |
|
||||||
|
| `test_chat_theme?<params>` | `ResolveTestChatTheme` | Preview a chat theme without installing it. |
|
||||||
|
| `invoice?<params>` | `ResolveInvoice` | Open a payment invoice (Telegram Payments / Stars). |
|
||||||
|
| `premium_offer[?<params>]` | `ResolvePremiumOffer` | Open the Premium subscription offer screen. |
|
||||||
|
| `premium_multigift?<params>` | `ResolvePremiumMultigift` | Open the bulk/multi-recipient Premium gifting screen. |
|
||||||
|
| `login?code=<n>` | `ResolveLoginCode` | Apply a login confirmation code (QR/code login flow). |
|
||||||
|
| `boost?<params>` | `ResolveBoost` | Open the "boost this channel" flow. |
|
||||||
|
| `message?slug=<slug>` | `ResolveChatLink` | Open a pre-composed shareable chat/business link. |
|
||||||
|
| `stars_topup?<params>` | `ResolveTopUp` | Open the Telegram Stars top-up screen. |
|
||||||
|
| `nft?slug=<slug>` | `ResolveUniqueGift` | Open a collectible/unique gift by slug. |
|
||||||
|
| `stargift_auction?slug=<slug>` | `ResolveGiftAuction` | Open a Star Gift auction listing. |
|
||||||
|
| `call?slug=<slug>` | `ResolveConferenceCall` | Join a group/conference call by invite slug. |
|
||||||
|
| `stars[?<params>]` | `ResolveStarsSettings` | Open Telegram Stars balance/settings. |
|
||||||
|
| `ton[?<params>]` / `grams[?<params>]` | `ResolveTonSettings` | Open TON wallet settings. |
|
||||||
|
| `oauth?<params>` | `ResolveOAuth` | Third-party OAuth authorization via Telegram. |
|
||||||
|
| *(anything else)* | `HandleUnknown` | Fallback — shown as an unrecognized-link message. |
|
||||||
|
|
||||||
|
## Resolve parameters
|
||||||
|
|
||||||
|
`resolve?domain=<username>` (or the bare `t.me/<username>` shorthand, which the
|
||||||
|
client rewrites to this form) accepts additional query parameters that change what
|
||||||
|
opens once the peer is resolved:
|
||||||
|
|
||||||
|
| Parameter | Effect |
|
||||||
|
|---|---|
|
||||||
|
| `domain` | The `@username` to resolve (required unless using the bare `t.me/<username>` form). |
|
||||||
|
| `phone` | Resolve by phone number instead of username. |
|
||||||
|
| `appname` | Open a Telegram Mini App by name instead of the peer's chat. |
|
||||||
|
| `startapp` | Start-parameter passed into a Mini App launch. |
|
||||||
|
| `attach` | Open the target's attach-menu bot alongside the chat. |
|
||||||
|
| `start` | Start-parameter for a bot (`/start <token>`). |
|
||||||
|
| `startgroup` | Add a bot to a group with this start token (bot-in-group flow). |
|
||||||
|
| `startchannel` | Add a bot to a channel with this start token. |
|
||||||
|
| `post` | Jump to a specific message id in the resolved peer. |
|
||||||
|
| `story` | Open a specific story id from the resolved peer. |
|
||||||
|
| `comment` | Jump to a specific comment/reply message id (discussion groups). |
|
||||||
|
| `topic` | Open a specific forum topic id. |
|
||||||
|
| `thread` | Open a specific reply thread/topic id (older alias of `topic`). |
|
||||||
|
| `game` | Launch a specific inline game short name from a bot. |
|
||||||
|
| `text` | Pre-fill the message composer with this text. |
|
||||||
|
| `voicechat` / `videochat` / `livestream` | Join the peer's active voice chat / video chat / live stream by hash. |
|
||||||
|
|
||||||
|
## Internal-only links
|
||||||
|
|
||||||
|
`InternalUrlHandlers()` in the same file (`internal:<path>`) covers links the app
|
||||||
|
generates for its own in-app UI (e.g. `internal:edit_peer/<id>`,
|
||||||
|
`internal:common_groups/<id>`, `internal:collectible_username/<name>@<id>`). These
|
||||||
|
are not meant to be shared externally and are not part of the public deep-link
|
||||||
|
surface — they only work as links the client itself produces inside its own UI.
|
||||||
|
|
||||||
|
## OwpenGram (`owpg://`) links
|
||||||
|
|
||||||
|
This fork adds a second scheme, handled in `Telegram/SourceFiles/core/application.cpp`
|
||||||
|
(`Application::openLocalUrl` / `Application::openOwpengramUrl`), for operating across
|
||||||
|
multiple OwpenGram-compatible backends rather than only the official Telegram DCs.
|
||||||
|
|
||||||
|
| Form | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `owpg://addserver?host=<host>&port=<port>` | Opens `AddServerBox` pre-filled with just the address. **Deliberately carries nothing else** — no name, description, RSA key, or DC id. |
|
||||||
|
| `owpg://oauth?<params>` / `owpg://resolve?<params>` | Rewritten to the equivalent `tg://oauth` / `tg://resolve` command and dispatched on the current session — lets those two flows work on an OwpenGram account, not just official Telegram. |
|
||||||
|
| `owpg://<host>/<rest>` | Routes a normal `t.me`-style path (`<rest>` = `+<invite>`, `<username>`, etc.) to whichever logged-in account is on that server's `<host>`, bypassing the official-Telegram-only guard. If no account on the device is logged into `<host>`, the client shows an inform box naming the server instead of silently failing. |
|
||||||
|
|
||||||
|
### Why `addserver` only takes host+port
|
||||||
|
|
||||||
|
The RSA public key, main DC id, display name, and description are **never** accepted
|
||||||
|
from the link itself — see the comment above `kOwpgAddServerRe` in `application.cpp`.
|
||||||
|
Letting a link author supply the key/identity directly would be a MITM vector: whoever
|
||||||
|
crafts the link would control what the victim ends up trusting as that server's
|
||||||
|
identity, regardless of who actually operates the host. Only host+port travel in the
|
||||||
|
link; everything else is fetched live and is exactly as trustworthy as the TCP
|
||||||
|
connection to that host:port already is.
|
||||||
|
|
||||||
|
Once `AddServerBox` has host+port, it calls `Owpengram::FetchServerInfo(host, port, …)`,
|
||||||
|
which does a plain HTTP `GET` against the server's own well-known endpoints on the
|
||||||
|
same port:
|
||||||
|
|
||||||
|
| Endpoint | Returns |
|
||||||
|
|---|---|
|
||||||
|
| `GET <host>:<port>/owpengram/server-info` | RSA public key (PEM), main DC id, and optional operator-set name/description/icon-presence flag. |
|
||||||
|
| `GET <host>:<port>/owpengram/server-icon` | Raw icon bytes, only fetched when `server-info` reported `hasIcon: true`. |
|
||||||
|
|
||||||
|
`RefreshServersInfo()` re-polls `server-info`/`server-icon` for every already-added
|
||||||
|
custom server to keep a cosmetic name/icon change in sync — it never touches host,
|
||||||
|
port, RSA key, DC id, or the server's local id, for the same MITM reason as above (see
|
||||||
|
the doc comment on `RefreshServersInfo` in `owpengram_servers.h`).
|
||||||
|
|
||||||
|
This also means the RSA-key-in-a-URL scheme discussed earlier in this doc's history
|
||||||
|
is intentionally **not** how this client adds a server — that was a plausible-looking
|
||||||
|
design that this project already considered and rejected in favor of fetch-after-add.
|
||||||
3628
docs/openapi.yaml
Normal file
3628
docs/openapi.yaml
Normal file
File diff suppressed because it is too large
Load diff
73
docs/reserved-usernames.md
Normal file
73
docs/reserved-usernames.md
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# Suggested reserved usernames
|
||||||
|
|
||||||
|
A starter blocklist for `TELESRV_RESERVED_USERNAMES` (see
|
||||||
|
`internal/config/config.go`, `ReservedUsernames`), beyond the shipped
|
||||||
|
default (`owpengram, admin, administrator, support, staff, moderator,
|
||||||
|
official, root, owner`). The mechanism blocks these, case-insensitively,
|
||||||
|
from self-service `account.updateUsername` / `channels.updateUsername` —
|
||||||
|
an operator can still assign one deliberately from the admin panel, since
|
||||||
|
that's not the squatting this exists to stop.
|
||||||
|
|
||||||
|
Organized by why each entry is worth blocking. Trim or extend per your own
|
||||||
|
deployment — none of this is enforced by the server itself, it's just a
|
||||||
|
starting list to feed into the env var.
|
||||||
|
|
||||||
|
## Official-sounding / staff-impersonation
|
||||||
|
|
||||||
|
Names that would read as "this account speaks for the operator" if a
|
||||||
|
random user grabbed them.
|
||||||
|
|
||||||
|
```text
|
||||||
|
telegram, telegramofficial, telegramsupport, telegramteam, telegramstaff,
|
||||||
|
telegramhq, telegramnews, telegramnotifications, telegramverify, telegrambot,
|
||||||
|
telegramservice, security, securityteam, verify, verification, verified,
|
||||||
|
helpdesk, helpcenter, customerservice, customersupport, servicedesk, service,
|
||||||
|
system, systemmessage, systembot, notifications, notification, alerts,
|
||||||
|
announcements, announcement, newsroom, press, pressoffice, spambot, antispam,
|
||||||
|
abuse, abusereport, reports, legal, compliance, billing, payments, finance,
|
||||||
|
accounts, accountservices, recovery, unlock, restore, backup, headquarters,
|
||||||
|
staffteam, modteam, moderation, moderators, founder, cofounder, management
|
||||||
|
```
|
||||||
|
|
||||||
|
## Founders / executives (real people this server is not affiliated with)
|
||||||
|
|
||||||
|
Blocks an account from presenting as Telegram's actual leadership.
|
||||||
|
|
||||||
|
```text
|
||||||
|
durov, pavel, paveldurov, pdurov, nikolai, nikolaidurov, ndurov, durovpavel,
|
||||||
|
telegramceo, telegramfounder
|
||||||
|
```
|
||||||
|
|
||||||
|
## Crypto / scam-bait
|
||||||
|
|
||||||
|
The classic "free crypto giveaway" impersonation targets — official-sounding
|
||||||
|
project/exchange names, plus generic scam-pattern handles.
|
||||||
|
|
||||||
|
```text
|
||||||
|
bitcoin, ethereum, toncoin, tonwallet, tonfoundation, crypto, cryptowallet,
|
||||||
|
wallet, airdrop, giveaway, bonus, freecoins, claim, claimnow, binance,
|
||||||
|
coinbase, kraken, metamask, trustwallet, ledger, opensea, uniswap, elonmusk,
|
||||||
|
tesla, spacex, cryptosupport, walletsupport, tonsupport, binancesupport,
|
||||||
|
investment, investmentteam, forextrading, tradingbot, signals, cryptosignals
|
||||||
|
```
|
||||||
|
|
||||||
|
## Suggested env var
|
||||||
|
|
||||||
|
Comma-separated, matching `envListOr`'s parsing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TELESRV_RESERVED_USERNAMES=owpengram,admin,administrator,support,staff,moderator,official,root,owner,telegram,telegramofficial,telegramsupport,telegramteam,telegramstaff,telegramhq,telegramnews,telegramnotifications,telegramverify,telegrambot,telegramservice,security,securityteam,verify,verification,verified,helpdesk,helpcenter,customerservice,customersupport,servicedesk,service,system,systemmessage,systembot,notifications,notification,alerts,announcements,announcement,newsroom,press,pressoffice,spambot,antispam,abuse,abusereport,reports,legal,compliance,billing,payments,finance,accounts,accountservices,recovery,unlock,restore,backup,headquarters,staffteam,modteam,moderation,moderators,founder,cofounder,management,durov,paveldurov,pdurov,nikolai,nikolaidurov,ndurov,durovpavel,telegramceo,telegramfounder,bitcoin,ethereum,toncoin,tonwallet,tonfoundation,crypto,cryptowallet,wallet,airdrop,giveaway,bonus,freecoins,claim,claimnow,binance,coinbase,kraken,metamask,trustwallet,ledger,opensea,uniswap,elonmusk,tesla,spacex,cryptosupport,walletsupport,tonsupport,binancesupport,investment,investmentteam,forextrading,tradingbot,signals,cryptosignals
|
||||||
|
```
|
||||||
|
|
||||||
|
## What this doesn't cover
|
||||||
|
|
||||||
|
- **Collectible usernames** (`internal/admin/service.go` collectible-username
|
||||||
|
actions) go through the admin panel/Admin API directly and aren't gated
|
||||||
|
by this list at all — an operator minting one bypasses it on purpose.
|
||||||
|
- **Prefix/substring squatting** (`telegram_real`, `xtelegram`, `te1egram`)
|
||||||
|
— the blocklist is an exact-match (case-insensitive) list, not a pattern
|
||||||
|
matcher, so lookalike spellings need their own entries if you want them
|
||||||
|
blocked too.
|
||||||
|
- This is a *username* blocklist only — it says nothing about display
|
||||||
|
names, bios, or profile photos, which can impersonate just as easily and
|
||||||
|
aren't restricted by `TELESRV_RESERVED_USERNAMES` at all.
|
||||||
|
|
@ -635,11 +635,19 @@ func (s *Service) AdminSetSettings(ctx context.Context, channelID int64, patch d
|
||||||
return s.channels.SetChannelAdminSettings(ctx, channelID, patch)
|
return s.channels.SetChannelAdminSettings(ctx, channelID, patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminSetUsername force-sets or clears a channel username through the admin path.
|
// AdminSetUsername force-sets or clears a channel username through the admin
|
||||||
|
// path. Unlike UpdateUsername it does not consult config.ReservedUsernames --
|
||||||
|
// an operator who deliberately reserved a word still needs to be able to hand
|
||||||
|
// it to a specific channel -- but format/length validity is still enforced,
|
||||||
|
// just against the lower minChannelUsernameLenAdmin floor.
|
||||||
func (s *Service) AdminSetUsername(ctx context.Context, channelID int64, username string) (domain.Channel, error) {
|
func (s *Service) AdminSetUsername(ctx context.Context, channelID int64, username string) (domain.Channel, error) {
|
||||||
if s == nil || s.channels == nil || channelID == 0 {
|
if s == nil || s.channels == nil || channelID == 0 {
|
||||||
return domain.Channel{}, domain.ErrChannelInvalid
|
return domain.Channel{}, domain.ErrChannelInvalid
|
||||||
}
|
}
|
||||||
|
username = normalizeChannelUsername(username)
|
||||||
|
if username != "" && !validChannelUsernameMinLen(username, minChannelUsernameLenAdmin) {
|
||||||
|
return domain.Channel{}, domain.ErrUsernameInvalid
|
||||||
|
}
|
||||||
return s.channels.SetChannelUsernameAdmin(ctx, channelID, username)
|
return s.channels.SetChannelUsernameAdmin(ctx, channelID, username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2520,8 +2528,20 @@ func normalizeChannelUsername(username string) string {
|
||||||
return strings.TrimSpace(username)
|
return strings.TrimSpace(username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// minChannelUsernameLenAdmin is the floor for an operator-assigned channel
|
||||||
|
// username via AdminSetUsername (the admin API/panel) -- shorter than what
|
||||||
|
// self-service channels.updateUsername allows, mirroring
|
||||||
|
// users.minUsernameLenAdmin: a deliberate short handle assigned by an
|
||||||
|
// operator is not the squatting the higher self-service minimum guards
|
||||||
|
// against.
|
||||||
|
const minChannelUsernameLenAdmin = 3
|
||||||
|
|
||||||
func validChannelUsername(username string) bool {
|
func validChannelUsername(username string) bool {
|
||||||
if len(username) < 5 || len(username) > 32 {
|
return validChannelUsernameMinLen(username, 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validChannelUsernameMinLen(username string, minLen int) bool {
|
||||||
|
if len(username) < minLen || len(username) > 32 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for i := 0; i < len(username); i++ {
|
for i := 0; i < len(username); i++ {
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,12 @@ func WithReservedUsernames(names []string) Option {
|
||||||
|
|
||||||
const (
|
const (
|
||||||
minUsernameLen = 5
|
minUsernameLen = 5
|
||||||
|
// minUsernameLenAdmin is the floor for an operator-assigned username via
|
||||||
|
// UpdateUsernameAdmin (the admin API/panel) -- shorter than what self-service
|
||||||
|
// account.updateUsername allows, since a deliberate short handle assigned by
|
||||||
|
// an operator (e.g. a 3-4 char official/brand account) is not the squatting
|
||||||
|
// the higher self-service minimum guards against.
|
||||||
|
minUsernameLenAdmin = 3
|
||||||
maxUsernameLen = 32
|
maxUsernameLen = 32
|
||||||
maxProfileNameRunes = 64
|
maxProfileNameRunes = 64
|
||||||
// bio 长度双档,对齐 appConfig about_length_limit_default=70 /
|
// bio 长度双档,对齐 appConfig about_length_limit_default=70 /
|
||||||
|
|
@ -383,7 +389,13 @@ func (s *Service) updateUsername(ctx context.Context, userID int64, username str
|
||||||
return s.projectOne(ctx, self.ID, self)
|
return s.projectOne(ctx, self.ID, self)
|
||||||
}
|
}
|
||||||
if username != "" {
|
if username != "" {
|
||||||
if !validUsername(username) || (enforceReserved && s.reserved.Contains(username)) {
|
minLen := minUsernameLen
|
||||||
|
if !enforceReserved {
|
||||||
|
// enforceReserved=false is exactly the admin-bypass path
|
||||||
|
// (UpdateUsernameAdmin) -- see minUsernameLenAdmin's doc comment.
|
||||||
|
minLen = minUsernameLenAdmin
|
||||||
|
}
|
||||||
|
if !validUsernameMinLen(username, minLen) || (enforceReserved && s.reserved.Contains(username)) {
|
||||||
return domain.User{}, domain.ErrUsernameInvalid
|
return domain.User{}, domain.ErrUsernameInvalid
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
|
|
@ -1040,7 +1052,11 @@ func normalizeUsername(username string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func validUsername(username string) bool {
|
func validUsername(username string) bool {
|
||||||
if len(username) < minUsernameLen || len(username) > maxUsernameLen {
|
return validUsernameMinLen(username, minUsernameLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validUsernameMinLen(username string, minLen int) bool {
|
||||||
|
if len(username) < minLen || len(username) > maxUsernameLen {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for i := 0; i < len(username); i++ {
|
for i := 0; i < len(username); i++ {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue