merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
121
internal/app/welcomemessages/service.go
Normal file
121
internal/app/welcomemessages/service.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package welcomemessages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
type ChannelAccess interface {
|
||||
ResolveChannel(ctx context.Context, userID, channelID int64) (domain.ChannelView, error)
|
||||
}
|
||||
|
||||
type Option func(*Service)
|
||||
|
||||
func WithClock(now func() time.Time) Option {
|
||||
return func(s *Service) {
|
||||
if now != nil {
|
||||
s.now = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
messages store.WelcomeMessageStore
|
||||
channels ChannelAccess
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewService(messages store.WelcomeMessageStore, channels ChannelAccess, options ...Option) *Service {
|
||||
s := &Service{messages: messages, channels: channels, now: time.Now}
|
||||
for _, option := range options {
|
||||
if option != nil {
|
||||
option(s)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Authorize is the cheap gate RPC uses before resolving upload/media/rich
|
||||
// content. Mutations call authorize again immediately before the store write so
|
||||
// a concurrent demotion cannot turn this preflight into a stale capability.
|
||||
func (s *Service) Authorize(ctx context.Context, userID int64, peer domain.Peer) error {
|
||||
return s.authorize(ctx, userID, peer)
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, userID int64, peer domain.Peer, randomID int64, content domain.WelcomeMessageContent) (domain.WelcomeMessage, bool, error) {
|
||||
if err := s.authorize(ctx, userID, peer); err != nil {
|
||||
return domain.WelcomeMessage{}, false, err
|
||||
}
|
||||
if err := content.Validate(); err != nil {
|
||||
return domain.WelcomeMessage{}, false, err
|
||||
}
|
||||
fingerprint, err := domain.WelcomeCreateFingerprint(peer, userID, randomID, content)
|
||||
if err != nil {
|
||||
return domain.WelcomeMessage{}, false, domain.ErrWelcomeMessageInvalid
|
||||
}
|
||||
return s.messages.CreateWelcomeMessage(ctx, domain.CreateWelcomeMessageRequest{
|
||||
Peer: peer, CreatorUserID: userID, Date: int(s.now().Unix()), RandomID: randomID,
|
||||
Content: content, CreateFingerprint: fingerprint,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Edit(ctx context.Context, userID int64, peer domain.Peer, id int, fields domain.WelcomeMessageEditFields) (domain.WelcomeMessage, error) {
|
||||
if err := s.authorize(ctx, userID, peer); err != nil {
|
||||
return domain.WelcomeMessage{}, err
|
||||
}
|
||||
return s.messages.EditWelcomeMessage(ctx, domain.EditWelcomeMessageRequest{
|
||||
Peer: peer, ID: id, EditDate: int(s.now().Unix()), Fields: fields,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, userID int64, peer domain.Peer, hash int64) (domain.WelcomeMessageList, error) {
|
||||
if err := s.authorize(ctx, userID, peer); err != nil {
|
||||
return domain.WelcomeMessageList{}, err
|
||||
}
|
||||
return s.messages.ListWelcomeMessages(ctx, peer, hash)
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, userID int64, peer domain.Peer, id int) (bool, error) {
|
||||
if err := s.authorize(ctx, userID, peer); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s.messages.DeleteWelcomeMessage(ctx, peer, id)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteAll(ctx context.Context, userID int64, peer domain.Peer) (bool, error) {
|
||||
if err := s.authorize(ctx, userID, peer); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s.messages.DeleteAllWelcomeMessages(ctx, peer)
|
||||
}
|
||||
|
||||
// HasAny is used only after the ordinary full-chat access check has succeeded.
|
||||
// It deliberately does not require manage_welcome_messages so non-admin members
|
||||
// receive the same has_welcome_messages projection as official clients.
|
||||
func (s *Service) HasAny(ctx context.Context, peer domain.Peer) (bool, error) {
|
||||
if s == nil || s.messages == nil || peer.Type != domain.PeerTypeChannel || peer.ID <= 0 {
|
||||
return false, domain.ErrWelcomeMessageInvalid
|
||||
}
|
||||
return s.messages.HasWelcomeMessages(ctx, peer)
|
||||
}
|
||||
|
||||
func (s *Service) authorize(ctx context.Context, userID int64, peer domain.Peer) error {
|
||||
if s == nil || s.messages == nil || s.channels == nil || userID <= 0 ||
|
||||
peer.Type != domain.PeerTypeChannel || peer.ID <= 0 {
|
||||
return domain.ErrWelcomeMessageInvalid
|
||||
}
|
||||
view, err := s.channels.ResolveChannel(ctx, userID, peer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if view.Channel.Monoforum {
|
||||
return domain.ErrWelcomeMessagePeerInvalid
|
||||
}
|
||||
if !view.Self.CanManageWelcomeMessages() {
|
||||
return domain.ErrWelcomeMessageForbidden
|
||||
}
|
||||
return nil
|
||||
}
|
||||
96
internal/app/welcomemessages/service_test.go
Normal file
96
internal/app/welcomemessages/service_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package welcomemessages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
type welcomeStoreSpy struct {
|
||||
creates int
|
||||
}
|
||||
|
||||
func (s *welcomeStoreSpy) CreateWelcomeMessage(_ context.Context, req domain.CreateWelcomeMessageRequest) (domain.WelcomeMessage, bool, error) {
|
||||
s.creates++
|
||||
return domain.WelcomeMessage{
|
||||
ID: 1, Peer: req.Peer, CreatorUserID: req.CreatorUserID, Date: req.Date,
|
||||
RandomID: req.RandomID, Content: req.Content, CreateFingerprint: req.CreateFingerprint, Version: 1,
|
||||
}, true, nil
|
||||
}
|
||||
func (*welcomeStoreSpy) EditWelcomeMessage(context.Context, domain.EditWelcomeMessageRequest) (domain.WelcomeMessage, error) {
|
||||
return domain.WelcomeMessage{}, nil
|
||||
}
|
||||
func (*welcomeStoreSpy) ListWelcomeMessages(context.Context, domain.Peer, int64) (domain.WelcomeMessageList, error) {
|
||||
return domain.WelcomeMessageList{Hash: 1}, nil
|
||||
}
|
||||
func (*welcomeStoreSpy) DeleteWelcomeMessage(context.Context, domain.Peer, int) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
func (*welcomeStoreSpy) DeleteAllWelcomeMessages(context.Context, domain.Peer) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
func (*welcomeStoreSpy) HasWelcomeMessages(context.Context, domain.Peer) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type welcomeChannelAccess struct {
|
||||
view domain.ChannelView
|
||||
err error
|
||||
}
|
||||
|
||||
func (a *welcomeChannelAccess) ResolveChannel(context.Context, int64, int64) (domain.ChannelView, error) {
|
||||
return a.view, a.err
|
||||
}
|
||||
|
||||
func TestServiceRechecksManageWelcomeMessagesPermission(t *testing.T) {
|
||||
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: 77}
|
||||
store := &welcomeStoreSpy{}
|
||||
channels := &welcomeChannelAccess{view: domain.ChannelView{
|
||||
Channel: domain.Channel{ID: peer.ID, Megagroup: true},
|
||||
Self: domain.ChannelMember{
|
||||
ChannelID: peer.ID, UserID: 9, Role: domain.ChannelRoleAdmin,
|
||||
Status: domain.ChannelMemberActive,
|
||||
AdminRights: domain.ChannelAdminRights{ManageWelcomeMessages: true},
|
||||
},
|
||||
}}
|
||||
service := NewService(store, channels, WithClock(func() time.Time { return time.Unix(1700000000, 0) }))
|
||||
message, created, err := service.Create(context.Background(), 9, peer, 1001, domain.WelcomeMessageContent{Message: "hello"})
|
||||
if err != nil || !created || message.Date != 1700000000 || store.creates != 1 {
|
||||
t.Fatalf("authorized create = %+v created=%v calls=%d err=%v", message, created, store.creates, err)
|
||||
}
|
||||
|
||||
channels.view.Self.Status = domain.ChannelMemberLeft
|
||||
if _, _, err := service.Create(context.Background(), 9, peer, 1002, domain.WelcomeMessageContent{Message: "blocked"}); !errors.Is(err, domain.ErrWelcomeMessageForbidden) || store.creates != 1 {
|
||||
t.Fatalf("inactive admin create err=%v calls=%d", err, store.creates)
|
||||
}
|
||||
|
||||
channels.view.Self = domain.ChannelMember{UserID: 9, Role: domain.ChannelRoleCreator, Status: domain.ChannelMemberActive}
|
||||
if _, err := service.List(context.Background(), 9, peer, 0); err != nil {
|
||||
t.Fatalf("creator list: %v", err)
|
||||
}
|
||||
channels.view.Channel.Monoforum = true
|
||||
if _, err := service.List(context.Background(), 9, peer, 0); !errors.Is(err, domain.ErrWelcomeMessagePeerInvalid) {
|
||||
t.Fatalf("monoforum list err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceRejectsOrdinaryMemberAndAllowsJoinedBroadcastAdmin(t *testing.T) {
|
||||
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: 88}
|
||||
store := &welcomeStoreSpy{}
|
||||
channels := &welcomeChannelAccess{view: domain.ChannelView{
|
||||
Channel: domain.Channel{ID: peer.ID, Broadcast: true},
|
||||
Self: domain.ChannelMember{UserID: 10, Role: domain.ChannelRoleMember, Status: domain.ChannelMemberActive},
|
||||
}}
|
||||
service := NewService(store, channels)
|
||||
if _, err := service.DeleteAll(context.Background(), 10, peer); !errors.Is(err, domain.ErrWelcomeMessageForbidden) {
|
||||
t.Fatalf("ordinary member delete-all err=%v", err)
|
||||
}
|
||||
channels.view.Self.Role = domain.ChannelRoleAdmin
|
||||
channels.view.Self.AdminRights.ManageWelcomeMessages = true
|
||||
if ok, err := service.DeleteAll(context.Background(), 10, peer); err != nil || !ok {
|
||||
t.Fatalf("joined broadcast admin delete-all=%v,%v", ok, err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue