chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
711
cmd/telesrv-admin/server.go
Normal file
711
cmd/telesrv-admin/server.go
Normal file
|
|
@ -0,0 +1,711 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/admin"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
//go:embed web/dist
|
||||
var webDist embed.FS
|
||||
|
||||
type server struct {
|
||||
cfg uiConfig
|
||||
read *readStore
|
||||
web fs.FS
|
||||
webServer http.Handler
|
||||
}
|
||||
|
||||
func newServer(cfg uiConfig, read *readStore) (*server, error) {
|
||||
web, err := fs.Sub(webDist, "web/dist")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &server{
|
||||
cfg: cfg,
|
||||
read: read,
|
||||
web: web,
|
||||
webServer: http.FileServer(http.FS(web)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) routes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("POST /api/login", s.handleAPILogin)
|
||||
mux.HandleFunc("POST /api/logout", s.handleAPILogout)
|
||||
mux.Handle("GET /api/session", s.requireAuthAPI(http.HandlerFunc(s.handleSession)))
|
||||
mux.Handle("GET /api/accounts", s.requireAuthAPI(http.HandlerFunc(s.handleAccountsAPI)))
|
||||
mux.Handle("GET /api/accounts/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleAccountDetailAPI)))
|
||||
mux.Handle("GET /api/channels", s.requireAuthAPI(http.HandlerFunc(s.handleChannelsAPI)))
|
||||
mux.Handle("GET /api/channels/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleChannelDetailAPI)))
|
||||
mux.Handle("GET /api/messages", s.requireAuthAPI(http.HandlerFunc(s.handleMessagesAPI)))
|
||||
mux.Handle("GET /api/messages/detail", s.requireAuthAPI(http.HandlerFunc(s.handleMessageDetailAPI)))
|
||||
mux.Handle("GET /api/messages/groups", s.requireAuthAPI(http.HandlerFunc(s.handleGroupMessagesAPI)))
|
||||
mux.Handle("GET /api/messages/groups/detail", s.requireAuthAPI(http.HandlerFunc(s.handleGroupMessageDetailAPI)))
|
||||
mux.Handle("POST /api/actions/freeze-send", s.requireAuthAPI(http.HandlerFunc(s.handleFreezeSendAPI)))
|
||||
mux.Handle("POST /api/actions/grant-premium", s.requireAuthAPI(http.HandlerFunc(s.handleGrantPremiumAPI)))
|
||||
mux.Handle("POST /api/actions/set-verified", s.requireAuthAPI(http.HandlerFunc(s.handleSetVerifiedAPI)))
|
||||
mux.Handle("POST /api/actions/set-channel-verified", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelVerifiedAPI)))
|
||||
mux.Handle("POST /api/actions/revoke-sessions", s.requireAuthAPI(http.HandlerFunc(s.handleRevokeSessionsAPI)))
|
||||
mux.Handle("POST /api/actions/delete-messages", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteMessagesAPI)))
|
||||
mux.Handle("POST /api/actions/delete-history", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteHistoryAPI)))
|
||||
mux.HandleFunc("/api/", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeAPIError(w, http.StatusNotFound, "api route not found")
|
||||
})
|
||||
mux.HandleFunc("/", s.handleApp)
|
||||
return mux
|
||||
}
|
||||
|
||||
type actorKey struct{}
|
||||
|
||||
func (s *server) requireAuthAPI(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie(sessionCookieName)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusUnauthorized, "not authenticated")
|
||||
return
|
||||
}
|
||||
claims, ok := verifySession(s.cfg.SessionKey, cookie.Value, time.Now())
|
||||
if !ok {
|
||||
clearSessionCookie(w)
|
||||
writeAPIError(w, http.StatusUnauthorized, "not authenticated")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), actorKey{}, claims.Actor)))
|
||||
})
|
||||
}
|
||||
|
||||
func actorFromContext(ctx context.Context) string {
|
||||
if actor, ok := ctx.Value(actorKey{}).(string); ok && actor != "" {
|
||||
return actor
|
||||
}
|
||||
return "admin"
|
||||
}
|
||||
|
||||
func (s *server) handleApp(w http.ResponseWriter, r *http.Request) {
|
||||
clean := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
|
||||
if clean != "." && clean != "" {
|
||||
if info, err := fs.Stat(s.web, clean); err == nil && !info.IsDir() {
|
||||
s.webServer.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
r2 := r.Clone(r.Context())
|
||||
r2.URL.Path = "/"
|
||||
s.webServer.ServeHTTP(w, r2)
|
||||
}
|
||||
|
||||
type loginRequest struct {
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
func (s *server) handleAPILogin(w http.ResponseWriter, r *http.Request) {
|
||||
var req loginRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeAPIError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if !s.validSecret(req.Secret) {
|
||||
writeAPIError(w, http.StatusUnauthorized, "invalid credential")
|
||||
return
|
||||
}
|
||||
value, err := signSession(s.cfg.SessionKey, sessionClaims{
|
||||
Actor: "admin",
|
||||
Exp: time.Now().Add(12 * time.Hour).Unix(),
|
||||
Nonce: newCommandID("sess"),
|
||||
})
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: value,
|
||||
Path: "/",
|
||||
MaxAge: int((12 * time.Hour).Seconds()),
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]any{"actor": "admin"})
|
||||
}
|
||||
|
||||
func (s *server) validSecret(secret string) bool {
|
||||
if s.cfg.Password != "" && subtle.ConstantTimeCompare([]byte(secret), []byte(s.cfg.Password)) == 1 {
|
||||
return true
|
||||
}
|
||||
if s.cfg.Token != "" && subtle.ConstantTimeCompare([]byte(secret), []byte(s.cfg.Token)) == 1 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *server) handleAPILogout(w http.ResponseWriter, _ *http.Request) {
|
||||
clearSessionCookie(w)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
func (s *server) handleSession(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"actor": actorFromContext(r.Context())})
|
||||
}
|
||||
|
||||
func (s *server) handleAccountsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if s.read == nil {
|
||||
writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured")
|
||||
return
|
||||
}
|
||||
q := r.URL.Query().Get("q")
|
||||
beforeID, _ := parseInt64(r.URL.Query().Get("before_id"))
|
||||
beforeActiveUS, _ := parseInt64(r.URL.Query().Get("before_active_us"))
|
||||
limit, _ := parseInt(r.URL.Query().Get("limit"))
|
||||
rows := []AccountRow{}
|
||||
hasMore := false
|
||||
var err error
|
||||
if strings.TrimSpace(q) != "" {
|
||||
rows, err = s.read.SearchAccounts(r.Context(), q)
|
||||
} else {
|
||||
rows, hasMore, err = s.read.ListAccounts(r.Context(), beforeActiveUS, beforeID, limit)
|
||||
}
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
nextBeforeID := int64(0)
|
||||
nextBeforeActiveUS := int64(0)
|
||||
if hasMore && len(rows) > 0 {
|
||||
last := rows[len(rows)-1]
|
||||
nextBeforeID = last.ID
|
||||
nextBeforeActiveUS = last.LastActiveAt.UnixMicro()
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = accountListDefaultLimit
|
||||
}
|
||||
if limit > accountListMaxLimit {
|
||||
limit = accountListMaxLimit
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"query": q,
|
||||
"limit": limit,
|
||||
"rows": rows,
|
||||
"has_more": hasMore,
|
||||
"next_before_id": nextBeforeID,
|
||||
"next_before_active_us": nextBeforeActiveUS,
|
||||
"listing": strings.TrimSpace(q) == "",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *server) handleAccountDetailAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if s.read == nil {
|
||||
writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured")
|
||||
return
|
||||
}
|
||||
userID, err := parseInt64(r.PathValue("id"))
|
||||
if err != nil || userID <= 0 {
|
||||
writeAPIError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
detail, err := s.read.AccountDetail(r.Context(), userID)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, detail)
|
||||
}
|
||||
|
||||
func (s *server) handleChannelsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if s.read == nil {
|
||||
writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured")
|
||||
return
|
||||
}
|
||||
q := r.URL.Query().Get("q")
|
||||
beforeID, _ := parseInt64(r.URL.Query().Get("before_id"))
|
||||
beforeUpdatedUS, _ := parseInt64(r.URL.Query().Get("before_updated_us"))
|
||||
limit, _ := parseInt(r.URL.Query().Get("limit"))
|
||||
rows := []ChannelRow{}
|
||||
hasMore := false
|
||||
var err error
|
||||
if strings.TrimSpace(q) != "" {
|
||||
rows, err = s.read.SearchChannels(r.Context(), q)
|
||||
} else {
|
||||
rows, hasMore, err = s.read.ListChannels(r.Context(), beforeUpdatedUS, beforeID, limit)
|
||||
}
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
nextBeforeID := int64(0)
|
||||
nextBeforeUpdatedUS := int64(0)
|
||||
if hasMore && len(rows) > 0 {
|
||||
last := rows[len(rows)-1]
|
||||
nextBeforeID = last.ID
|
||||
nextBeforeUpdatedUS = last.UpdatedAt.UnixMicro()
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = channelListDefaultLimit
|
||||
}
|
||||
if limit > channelListMaxLimit {
|
||||
limit = channelListMaxLimit
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"query": q,
|
||||
"limit": limit,
|
||||
"rows": rows,
|
||||
"has_more": hasMore,
|
||||
"next_before_id": nextBeforeID,
|
||||
"next_before_updated_us": nextBeforeUpdatedUS,
|
||||
"listing": strings.TrimSpace(q) == "",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *server) handleChannelDetailAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if s.read == nil {
|
||||
writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured")
|
||||
return
|
||||
}
|
||||
channelID, err := parseInt64(r.PathValue("id"))
|
||||
if err != nil || channelID <= 0 {
|
||||
writeAPIError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
detail, err := s.read.ChannelDetail(r.Context(), channelID)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, detail)
|
||||
}
|
||||
|
||||
func (s *server) handleMessagesAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if s.read == nil {
|
||||
writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured")
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
owner, _ := parseInt64(q.Get("owner_user_id"))
|
||||
peer, _ := parseInt64(q.Get("peer_id"))
|
||||
beforeDate, _ := parseInt64(q.Get("before_date"))
|
||||
beforeID, _ := parseInt(q.Get("before_id"))
|
||||
limit, _ := parseInt(q.Get("limit"))
|
||||
rows := []MessageRow{}
|
||||
var err error
|
||||
if owner > 0 && peer > 0 {
|
||||
rows, err = s.read.ListMessages(r.Context(), owner, peer, beforeDate, beforeID, limit)
|
||||
}
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"owner_user_id": owner,
|
||||
"peer_id": peer,
|
||||
"before_date": beforeDate,
|
||||
"before_id": beforeID,
|
||||
"limit": limit,
|
||||
"rows": rows,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *server) handleMessageDetailAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if s.read == nil {
|
||||
writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured")
|
||||
return
|
||||
}
|
||||
owner, err1 := parseInt64(r.URL.Query().Get("owner_user_id"))
|
||||
msgID, err2 := parseInt(r.URL.Query().Get("msg_id"))
|
||||
if err1 != nil || err2 != nil || owner <= 0 || msgID <= 0 {
|
||||
writeAPIError(w, http.StatusBadRequest, "invalid owner/msg_id")
|
||||
return
|
||||
}
|
||||
detail, err := s.read.MessageDetail(r.Context(), owner, msgID)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, detail)
|
||||
}
|
||||
|
||||
func (s *server) handleGroupMessagesAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if s.read == nil {
|
||||
writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured")
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
channelID, _ := parseInt64(q.Get("channel_id"))
|
||||
beforeDate, _ := parseInt64(q.Get("before_date"))
|
||||
beforeID, _ := parseInt(q.Get("before_id"))
|
||||
limit, _ := parseInt(q.Get("limit"))
|
||||
rows := []GroupMessageRow{}
|
||||
var err error
|
||||
if channelID > 0 {
|
||||
rows, err = s.read.ListGroupMessages(r.Context(), channelID, beforeDate, beforeID, limit)
|
||||
}
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if limit <= 0 || limit > messagePageLimit {
|
||||
limit = messagePageLimit
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"channel_id": channelID,
|
||||
"before_date": beforeDate,
|
||||
"before_id": beforeID,
|
||||
"limit": limit,
|
||||
"rows": rows,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *server) handleGroupMessageDetailAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if s.read == nil {
|
||||
writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured")
|
||||
return
|
||||
}
|
||||
channelID, err1 := parseInt64(r.URL.Query().Get("channel_id"))
|
||||
msgID, err2 := parseInt(r.URL.Query().Get("msg_id"))
|
||||
if err1 != nil || err2 != nil || channelID <= 0 || msgID <= 0 {
|
||||
writeAPIError(w, http.StatusBadRequest, "invalid channel_id/msg_id")
|
||||
return
|
||||
}
|
||||
detail, err := s.read.GroupMessageDetail(r.Context(), channelID, msgID)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, detail)
|
||||
}
|
||||
|
||||
type freezeSendAPIRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Reason string `json:"reason"`
|
||||
Confirm bool `json:"confirm"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Frozen bool `json:"frozen"`
|
||||
}
|
||||
|
||||
func (s *server) handleFreezeSendAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var body freezeSendAPIRequest
|
||||
if !decodeAction(w, r, &body) {
|
||||
return
|
||||
}
|
||||
req := admin.SetSendFrozenRequest{
|
||||
CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "freeze-send"),
|
||||
UserID: body.UserID,
|
||||
Frozen: body.Frozen,
|
||||
}
|
||||
result, err := s.callAdminAPI(r.Context(), "/v1/accounts/freeze-send", req)
|
||||
writeCommandResultAPI(w, result, err)
|
||||
}
|
||||
|
||||
type grantPremiumAPIRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Reason string `json:"reason"`
|
||||
Confirm bool `json:"confirm"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Months int `json:"months"`
|
||||
}
|
||||
|
||||
func (s *server) handleGrantPremiumAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var body grantPremiumAPIRequest
|
||||
if !decodeAction(w, r, &body) {
|
||||
return
|
||||
}
|
||||
req := admin.GrantPremiumRequest{
|
||||
CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "grant-premium"),
|
||||
UserID: body.UserID,
|
||||
Months: body.Months,
|
||||
}
|
||||
result, err := s.callAdminAPI(r.Context(), "/v1/accounts/grant-premium", req)
|
||||
writeCommandResultAPI(w, result, err)
|
||||
}
|
||||
|
||||
type setVerifiedAPIRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Reason string `json:"reason"`
|
||||
Confirm bool `json:"confirm"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
func (s *server) handleSetVerifiedAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var body setVerifiedAPIRequest
|
||||
if !decodeAction(w, r, &body) {
|
||||
return
|
||||
}
|
||||
req := admin.SetVerifiedRequest{
|
||||
CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-verified"),
|
||||
UserID: body.UserID,
|
||||
Verified: body.Verified,
|
||||
}
|
||||
result, err := s.callAdminAPI(r.Context(), "/v1/accounts/set-verified", req)
|
||||
writeCommandResultAPI(w, result, err)
|
||||
}
|
||||
|
||||
type setChannelVerifiedAPIRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Reason string `json:"reason"`
|
||||
Confirm bool `json:"confirm"`
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
func (s *server) handleSetChannelVerifiedAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var body setChannelVerifiedAPIRequest
|
||||
if !decodeAction(w, r, &body) {
|
||||
return
|
||||
}
|
||||
req := admin.SetChannelVerifiedRequest{
|
||||
CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-channel-verified"),
|
||||
ChannelID: body.ChannelID,
|
||||
Verified: body.Verified,
|
||||
}
|
||||
result, err := s.callAdminAPI(r.Context(), "/v1/channels/set-verified", req)
|
||||
writeCommandResultAPI(w, result, err)
|
||||
}
|
||||
|
||||
type revokeSessionsAPIRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Reason string `json:"reason"`
|
||||
Confirm bool `json:"confirm"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Hash int64 `json:"hash"`
|
||||
KeepHash int64 `json:"keep_hash"`
|
||||
RevokeAll bool `json:"revoke_all"`
|
||||
}
|
||||
|
||||
func (s *server) handleRevokeSessionsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var body revokeSessionsAPIRequest
|
||||
if !decodeAction(w, r, &body) {
|
||||
return
|
||||
}
|
||||
req := admin.RevokeSessionsRequest{
|
||||
CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "revoke-sessions"),
|
||||
UserID: body.UserID,
|
||||
Hash: body.Hash,
|
||||
KeepHash: body.KeepHash,
|
||||
RevokeAll: body.RevokeAll,
|
||||
}
|
||||
result, err := s.callAdminAPI(r.Context(), "/v1/accounts/revoke-sessions", req)
|
||||
writeCommandResultAPI(w, result, err)
|
||||
}
|
||||
|
||||
type deleteMessagesAPIRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Reason string `json:"reason"`
|
||||
Confirm bool `json:"confirm"`
|
||||
OwnerUserID int64 `json:"owner_user_id"`
|
||||
PeerID int64 `json:"peer_id"`
|
||||
IDs []int `json:"ids"`
|
||||
Revoke bool `json:"revoke"`
|
||||
}
|
||||
|
||||
func (s *server) handleDeleteMessagesAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var body deleteMessagesAPIRequest
|
||||
if !decodeAction(w, r, &body) {
|
||||
return
|
||||
}
|
||||
req := admin.DeletePrivateMessagesRequest{
|
||||
CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "delete-messages"),
|
||||
OwnerUserID: body.OwnerUserID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: body.PeerID},
|
||||
IDs: body.IDs,
|
||||
Revoke: body.Revoke,
|
||||
}
|
||||
result, err := s.callAdminAPI(r.Context(), "/v1/messages/delete", req)
|
||||
writeCommandResultAPI(w, result, err)
|
||||
}
|
||||
|
||||
type deleteHistoryAPIRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Reason string `json:"reason"`
|
||||
Confirm bool `json:"confirm"`
|
||||
OwnerUserID int64 `json:"owner_user_id"`
|
||||
PeerID int64 `json:"peer_id"`
|
||||
MaxID int `json:"max_id"`
|
||||
MinDate int `json:"min_date"`
|
||||
MaxDate int `json:"max_date"`
|
||||
MaxBatches int `json:"max_batches"`
|
||||
JustClear bool `json:"just_clear"`
|
||||
Revoke bool `json:"revoke"`
|
||||
}
|
||||
|
||||
func (s *server) handleDeleteHistoryAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var body deleteHistoryAPIRequest
|
||||
if !decodeAction(w, r, &body) {
|
||||
return
|
||||
}
|
||||
req := admin.DeletePrivateHistoryRequest{
|
||||
CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "delete-history"),
|
||||
OwnerUserID: body.OwnerUserID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: body.PeerID},
|
||||
MaxID: body.MaxID,
|
||||
MinDate: body.MinDate,
|
||||
MaxDate: body.MaxDate,
|
||||
JustClear: body.JustClear,
|
||||
Revoke: body.Revoke,
|
||||
MaxBatches: body.MaxBatches,
|
||||
}
|
||||
result, err := s.callAdminAPI(r.Context(), "/v1/messages/delete-history", req)
|
||||
writeCommandResultAPI(w, result, err)
|
||||
}
|
||||
|
||||
func (s *server) commandMetaFromAPI(r *http.Request, commandID, reason string, confirm bool, prefix string) admin.CommandMeta {
|
||||
commandID = strings.TrimSpace(commandID)
|
||||
if confirm && strings.HasPrefix(commandID, "dry-") {
|
||||
commandID = ""
|
||||
}
|
||||
dryRun := !confirm
|
||||
if commandID == "" {
|
||||
scope := "dry"
|
||||
if !dryRun {
|
||||
scope = "exec"
|
||||
}
|
||||
commandID = newCommandID(scope + "-" + prefix)
|
||||
}
|
||||
return admin.CommandMeta{
|
||||
CommandID: commandID,
|
||||
Actor: actorFromContext(r.Context()),
|
||||
Reason: reason,
|
||||
DryRun: dryRun,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) callAdminAPI(ctx context.Context, apiPath string, payload any) (admin.CommandResult, error) {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return admin.CommandResult{}, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.cfg.AdminAPIURL+apiPath, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return admin.CommandResult{}, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+s.cfg.AdminAPIToken)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return admin.CommandResult{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
var result admin.CommandResult
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
return result, fmt.Errorf("admin api %s: status=%d body=%s", apiPath, resp.StatusCode, string(raw))
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
if result.Error == "" {
|
||||
result.Error = resp.Status
|
||||
}
|
||||
return result, errors.New(result.Error)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func decodeAction(w http.ResponseWriter, r *http.Request, dst any) bool {
|
||||
if err := decodeJSON(r, dst); err != nil {
|
||||
writeAPIError(w, http.StatusBadRequest, err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func decodeJSON(r *http.Request, dst any) error {
|
||||
defer r.Body.Close()
|
||||
dec := json.NewDecoder(io.LimitReader(r.Body, 1<<20))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeCommandResultAPI(w http.ResponseWriter, result admin.CommandResult, err error) {
|
||||
if err != nil {
|
||||
if result.Status == "" {
|
||||
result.Status = "failed"
|
||||
}
|
||||
if result.Message == "" {
|
||||
result.Message = "command failed"
|
||||
}
|
||||
if result.Error == "" {
|
||||
result.Error = err.Error()
|
||||
}
|
||||
writeJSON(w, http.StatusBadGateway, result)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func writeAPIError(w http.ResponseWriter, status int, message string) {
|
||||
writeJSON(w, status, map[string]any{"error": message})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, value any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
|
||||
func parseInt64(v string) (int64, error) {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return 0, nil
|
||||
}
|
||||
return strconv.ParseInt(v, 10, 64)
|
||||
}
|
||||
|
||||
func parseInt(v string) (int, error) {
|
||||
n, err := parseInt64(v)
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
func boolValue(v bool) string {
|
||||
if v {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
func displayPhone(v string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" || strings.HasPrefix(v, "+") {
|
||||
return v
|
||||
}
|
||||
for _, r := range v {
|
||||
if r < '0' || r > '9' {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "+" + v
|
||||
}
|
||||
|
||||
func channelKind(ch ChannelRow) string {
|
||||
if ch.Broadcast && !ch.Megagroup {
|
||||
return "频道"
|
||||
}
|
||||
if ch.Megagroup {
|
||||
if ch.Forum {
|
||||
return "超级群/论坛"
|
||||
}
|
||||
return "超级群"
|
||||
}
|
||||
return "频道/群"
|
||||
}
|
||||
|
||||
func errString(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue