fix: sync latest Android and langpack updates
This commit is contained in:
parent
20b388eeaa
commit
4570df5939
20 changed files with 12384 additions and 27 deletions
|
|
@ -139,6 +139,10 @@ Recommended checks:
|
|||
- Check server logs for no new `NOT_IMPLEMENTED`, `Unhandled RPC`, `bad_msg`,
|
||||
panic, or internal errors.
|
||||
|
||||
## Contributors
|
||||
|
||||
- [ajarshia](https://github.com/ajarshia) - Android Persian (`fa`) language pack.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
```text
|
||||
|
|
|
|||
|
|
@ -121,6 +121,10 @@ Start-Process $tdesktop -ArgumentList @("-workdir", "$PWD\.tdata-bob")
|
|||
- 同一账号多 session 登录,确认当前 session 不重复 echo,其它在线 session 能收到 updates。
|
||||
- 检查 server 日志没有新增 `NOT_IMPLEMENTED`、`Unhandled RPC`、`bad_msg`、panic 或 internal error。
|
||||
|
||||
## 贡献者
|
||||
|
||||
- [ajarshia](https://github.com/ajarshia) - Android Persian (`fa`) 语言包。
|
||||
|
||||
## 仓库结构
|
||||
|
||||
```text
|
||||
|
|
|
|||
11655
data/langpack/android/android_fa_v59634849.strings
Normal file
11655
data/langpack/android/android_fa_v59634849.strings
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -128,7 +128,7 @@ func (s *Service) sendServiceBotReply(ctx context.Context, botUserID, userID int
|
|||
RecipientUserID: userID,
|
||||
RandomID: s.botReplyRandomID(),
|
||||
Message: reply.Text,
|
||||
Entities: reply.Entities,
|
||||
Entities: serviceBotReplyEntities(reply.Text, reply.Entities),
|
||||
Date: int(s.now().Unix()),
|
||||
RecipientBlocked: blocked,
|
||||
}); err != nil {
|
||||
|
|
|
|||
182
internal/app/bots/service_bot_entities.go
Normal file
182
internal/app/bots/service_bot_entities.go
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
package bots
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
type serviceBotEntitySpan struct {
|
||||
start int
|
||||
end int
|
||||
}
|
||||
|
||||
func serviceBotReplyEntities(text string, explicit []domain.MessageEntity) []domain.MessageEntity {
|
||||
if text == "" && len(explicit) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := append([]domain.MessageEntity(nil), explicit...)
|
||||
occupied := make([]serviceBotEntitySpan, 0, len(out)+8)
|
||||
for _, entity := range out {
|
||||
if entity.Length <= 0 {
|
||||
continue
|
||||
}
|
||||
occupied = append(occupied, serviceBotEntitySpan{start: entity.Offset, end: entity.Offset + entity.Length})
|
||||
}
|
||||
appendEntity := func(entity domain.MessageEntity) {
|
||||
if entity.Length <= 0 || len(out) >= domain.MaxMessageEntityCount {
|
||||
return
|
||||
}
|
||||
span := serviceBotEntitySpan{start: entity.Offset, end: entity.Offset + entity.Length}
|
||||
if serviceBotSpanOverlaps(span, occupied) {
|
||||
return
|
||||
}
|
||||
out = append(out, entity)
|
||||
occupied = append(occupied, span)
|
||||
}
|
||||
for _, span := range serviceBotURLByteSpans(text) {
|
||||
offset, length := utf16Range(text, span.start, span.end)
|
||||
appendEntity(domain.MessageEntity{Type: domain.MessageEntityURL, Offset: offset, Length: length})
|
||||
}
|
||||
for _, span := range serviceBotCommandByteSpans(text) {
|
||||
offset, length := utf16Range(text, span.start, span.end)
|
||||
appendEntity(domain.MessageEntity{Type: domain.MessageEntityBotCommand, Offset: offset, Length: length})
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
if out[i].Offset != out[j].Offset {
|
||||
return out[i].Offset < out[j].Offset
|
||||
}
|
||||
return out[i].Length < out[j].Length
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func serviceBotURLByteSpans(text string) []serviceBotEntitySpan {
|
||||
var spans []serviceBotEntitySpan
|
||||
for i := 0; i < len(text); {
|
||||
if !strings.HasPrefix(text[i:], "https://") && !strings.HasPrefix(text[i:], "http://") {
|
||||
_, size := utf8.DecodeRuneInString(text[i:])
|
||||
i += size
|
||||
continue
|
||||
}
|
||||
start := i
|
||||
i += len("http://")
|
||||
if strings.HasPrefix(text[start:], "https://") {
|
||||
i = start + len("https://")
|
||||
}
|
||||
for i < len(text) {
|
||||
r, size := utf8.DecodeRuneInString(text[i:])
|
||||
if unicode.IsSpace(r) || r == '<' || r == '>' {
|
||||
break
|
||||
}
|
||||
i += size
|
||||
}
|
||||
end := trimServiceBotURLTrailingPunctuation(text, start, i)
|
||||
if end > start {
|
||||
spans = append(spans, serviceBotEntitySpan{start: start, end: end})
|
||||
}
|
||||
if i == start {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return spans
|
||||
}
|
||||
|
||||
func trimServiceBotURLTrailingPunctuation(text string, start, end int) int {
|
||||
for end > start {
|
||||
r, size := utf8.DecodeLastRuneInString(text[start:end])
|
||||
if !strings.ContainsRune(".,;:!?)]}", r) {
|
||||
break
|
||||
}
|
||||
end -= size
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func serviceBotCommandByteSpans(text string) []serviceBotEntitySpan {
|
||||
var spans []serviceBotEntitySpan
|
||||
for i := 0; i < len(text); {
|
||||
r, size := utf8.DecodeRuneInString(text[i:])
|
||||
if r != '/' || !serviceBotCommandStart(text, i) {
|
||||
i += size
|
||||
continue
|
||||
}
|
||||
start := i
|
||||
i += size
|
||||
commandStartEnd := i
|
||||
for i < len(text) {
|
||||
r, size = utf8.DecodeRuneInString(text[i:])
|
||||
if !serviceBotCommandChar(r) {
|
||||
break
|
||||
}
|
||||
i += size
|
||||
}
|
||||
if i == commandStartEnd {
|
||||
continue
|
||||
}
|
||||
if i < len(text) {
|
||||
r, size = utf8.DecodeRuneInString(text[i:])
|
||||
if r == '@' {
|
||||
mentionEnd := i + size
|
||||
for mentionEnd < len(text) {
|
||||
r, size = utf8.DecodeRuneInString(text[mentionEnd:])
|
||||
if !serviceBotCommandChar(r) {
|
||||
break
|
||||
}
|
||||
mentionEnd += size
|
||||
}
|
||||
if mentionEnd > i+size {
|
||||
i = mentionEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
spans = append(spans, serviceBotEntitySpan{start: start, end: i})
|
||||
}
|
||||
return spans
|
||||
}
|
||||
|
||||
func serviceBotCommandStart(text string, byteIndex int) bool {
|
||||
if byteIndex == 0 {
|
||||
return true
|
||||
}
|
||||
prev, _ := utf8.DecodeLastRuneInString(text[:byteIndex])
|
||||
if prev == ':' || prev == '/' || prev == '@' {
|
||||
return false
|
||||
}
|
||||
return !serviceBotCommandChar(prev)
|
||||
}
|
||||
|
||||
func serviceBotCommandChar(r rune) bool {
|
||||
return r == '_' || ('0' <= r && r <= '9') || ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z')
|
||||
}
|
||||
|
||||
func utf16Range(text string, startByte, endByte int) (int, int) {
|
||||
offset := 0
|
||||
for _, r := range text[:startByte] {
|
||||
offset += utf16RuneLen(r)
|
||||
}
|
||||
length := 0
|
||||
for _, r := range text[startByte:endByte] {
|
||||
length += utf16RuneLen(r)
|
||||
}
|
||||
return offset, length
|
||||
}
|
||||
|
||||
func utf16RuneLen(r rune) int {
|
||||
if r > 0xFFFF {
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func serviceBotSpanOverlaps(span serviceBotEntitySpan, occupied []serviceBotEntitySpan) bool {
|
||||
for _, other := range occupied {
|
||||
if span.start < other.end && other.start < span.end {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
60
internal/app/bots/service_bot_entities_test.go
Normal file
60
internal/app/bots/service_bot_entities_test.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package bots
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestServiceBotReplyEntitiesCommandsAndURLsUseUTF16Offsets(t *testing.T) {
|
||||
text := "🙂 Send /cancel or https://telesrv.net/addstickers/fun_pack."
|
||||
entities := serviceBotReplyEntities(text, nil)
|
||||
|
||||
assertEntity := func(typ domain.MessageEntityType, offset, length int) {
|
||||
t.Helper()
|
||||
for _, entity := range entities {
|
||||
if entity.Type == typ && entity.Offset == offset && entity.Length == length {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("entities %+v missing %s at offset=%d length=%d", entities, typ, offset, length)
|
||||
}
|
||||
|
||||
assertEntity(domain.MessageEntityBotCommand, 8, len("/cancel"))
|
||||
assertEntity(domain.MessageEntityURL, 19, len("https://telesrv.net/addstickers/fun_pack"))
|
||||
}
|
||||
|
||||
func TestServiceBotReplyEntitiesSkipCommandsInsideURLsAndExplicitEntities(t *testing.T) {
|
||||
text := "Token: abc/def\nhttps://telesrv.net/addstickers/fun_pack\nUse /help"
|
||||
entities := serviceBotReplyEntities(text, []domain.MessageEntity{{
|
||||
Type: domain.MessageEntityCode,
|
||||
Offset: len("Token: "),
|
||||
Length: len("abc/def"),
|
||||
}})
|
||||
|
||||
commandCount := 0
|
||||
for _, entity := range entities {
|
||||
if entity.Type == domain.MessageEntityBotCommand {
|
||||
commandCount++
|
||||
}
|
||||
if entity.Type == domain.MessageEntityBotCommand && entity.Offset < len("Token: abc/def\nhttps://telesrv.net/") {
|
||||
t.Fatalf("unexpected command entity inside code/url: %+v in %+v", entity, entities)
|
||||
}
|
||||
}
|
||||
if commandCount != 1 {
|
||||
t.Fatalf("bot command entities = %d in %+v, want only /help", commandCount, entities)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceBotReplyEntitiesIgnoreBareSlashBeforeEmoji(t *testing.T) {
|
||||
entities := serviceBotReplyEntities("not a command /🙂 but /help is", nil)
|
||||
commandCount := 0
|
||||
for _, entity := range entities {
|
||||
if entity.Type == domain.MessageEntityBotCommand {
|
||||
commandCount++
|
||||
}
|
||||
}
|
||||
if commandCount != 1 {
|
||||
t.Fatalf("bot command entities = %d in %+v, want only /help", commandCount, entities)
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,12 @@ func sendMessageToStickers(t *testing.T, svc *Service, messages *memory.MessageS
|
|||
msg.From = domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
||||
msg.Peer = domain.Peer{Type: domain.PeerTypeUser, ID: domain.StickersBotUserID}
|
||||
svc.respondAsStickers(owner.ID, msg)
|
||||
list, err := messages.ListByUser(context.Background(), owner.ID, domain.MessageFilter{
|
||||
return latestStickersReply(t, messages, owner.ID).Body
|
||||
}
|
||||
|
||||
func latestStickersReply(t *testing.T, messages *memory.MessageStore, userID int64) domain.Message {
|
||||
t.Helper()
|
||||
list, err := messages.ListByUser(context.Background(), userID, domain.MessageFilter{
|
||||
HasPeer: true,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.StickersBotUserID},
|
||||
Limit: 100,
|
||||
|
|
@ -44,9 +49,9 @@ func sendMessageToStickers(t *testing.T, svc *Service, messages *memory.MessageS
|
|||
}
|
||||
}
|
||||
if latest.ID == 0 {
|
||||
t.Fatalf("no Stickers reply after message %+v", msg)
|
||||
t.Fatal("no Stickers reply")
|
||||
}
|
||||
return latest.Body
|
||||
return latest
|
||||
}
|
||||
|
||||
func newStickersBotTestService(t *testing.T) (*Service, *memory.UserStore, *memory.BotStore, *memory.MessageStore, *stickersBotFakeCreator, *stickersBotFakeInstaller) {
|
||||
|
|
@ -138,6 +143,10 @@ func TestStickersBotSystemSeedStartAndCancel(t *testing.T) {
|
|||
if reply := sendTextToStickers(t, svc, messages, owner, "/start"); !strings.Contains(reply, "/newpack") || !strings.Contains(reply, "/newemoji") || !strings.Contains(reply, "/addsticker") {
|
||||
t.Fatalf("/start reply = %q, want help text", reply)
|
||||
}
|
||||
startReply := latestStickersReply(t, messages, owner.ID)
|
||||
assertReplyEntityText(t, startReply, domain.MessageEntityBotCommand, "/newpack")
|
||||
assertReplyEntityText(t, startReply, domain.MessageEntityBotCommand, "/newemoji")
|
||||
assertReplyEntityText(t, startReply, domain.MessageEntityBotCommand, "/addsticker")
|
||||
sendTextToStickers(t, svc, messages, owner, "/newpack")
|
||||
if reply := sendTextToStickers(t, svc, messages, owner, "/cancel"); !strings.Contains(reply, "Cancelled") {
|
||||
t.Fatalf("/cancel reply = %q, want cancelled", reply)
|
||||
|
|
@ -190,6 +199,8 @@ func TestStickersBotPublishStickerPack(t *testing.T) {
|
|||
if !strings.Contains(reply, "https://telesrv.net/addstickers/fresh_pack") {
|
||||
t.Fatalf("publish reply = %q, want addstickers link", reply)
|
||||
}
|
||||
publishReply := latestStickersReply(t, messages, owner.ID)
|
||||
assertReplyEntityText(t, publishReply, domain.MessageEntityURL, "https://telesrv.net/addstickers/fresh_pack")
|
||||
if len(creator.created) != 1 {
|
||||
t.Fatalf("created requests = %d, want 1", len(creator.created))
|
||||
}
|
||||
|
|
@ -454,6 +465,22 @@ func botCommandExists(commands []domain.BotCommand, want string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func assertReplyEntityText(t *testing.T, msg domain.Message, typ domain.MessageEntityType, want string) {
|
||||
t.Helper()
|
||||
for _, entity := range msg.Entities {
|
||||
if entity.Type != typ {
|
||||
continue
|
||||
}
|
||||
if entity.Offset < 0 || entity.Length < 0 || entity.Offset+entity.Length > len(msg.Body) {
|
||||
t.Fatalf("entity %+v out of ASCII bounds for %q", entity, msg.Body)
|
||||
}
|
||||
if got := msg.Body[entity.Offset : entity.Offset+entity.Length]; got == want {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("message %q entities %+v missing %s entity for %q", msg.Body, msg.Entities, typ, want)
|
||||
}
|
||||
|
||||
type stickersBotFakeCreator struct {
|
||||
created []domain.CreateStickerSetRequest
|
||||
sets []domain.StickerSet
|
||||
|
|
|
|||
|
|
@ -96,3 +96,27 @@ func TestSeedDirectoryWalksClientSubdirs(t *testing.T) {
|
|||
t.Fatalf("weba pack = %+v", pack)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBundledAndroidPersianLangPackParses(t *testing.T) {
|
||||
path := filepath.Join("..", "..", "..", "data", "langpack", "android", "android_fa_v59634849.strings")
|
||||
pack, err := ParseTDesktopFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("parse bundled android fa pack: %v", err)
|
||||
}
|
||||
if pack.LangPack != "android" || pack.LangCode != "fa" || pack.Version != 59634849 {
|
||||
t.Fatalf("pack meta = %+v, want android/fa v59634849", pack)
|
||||
}
|
||||
if len(pack.Strings) < 10000 {
|
||||
t.Fatalf("strings count = %d, want full android fa pack", len(pack.Strings))
|
||||
}
|
||||
wantPersian := "\u0641\u0627\u0631\u0633\u06cc"
|
||||
for _, item := range pack.Strings {
|
||||
if item.Key == "TranslateLanguageFA" {
|
||||
if item.Value != wantPersian {
|
||||
t.Fatalf("TranslateLanguageFA = %q, want %q", item.Value, wantPersian)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("TranslateLanguageFA not found in bundled android fa pack")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,6 +223,15 @@ func CreatorChannelAdminRights() ChannelAdminRights {
|
|||
}
|
||||
}
|
||||
|
||||
// CreatorProjectionAdminRights returns the full rights clients expect for an
|
||||
// owner while preserving creator-scoped mutable flags.
|
||||
func CreatorProjectionAdminRights(rights ChannelAdminRights) ChannelAdminRights {
|
||||
creatorRights := CreatorChannelAdminRights()
|
||||
creatorRights.Anonymous = rights.Anonymous
|
||||
creatorRights.ManageDirectMessages = rights.ManageDirectMessages
|
||||
return creatorRights
|
||||
}
|
||||
|
||||
// NormalizeFullMegagroupAdminRights fills implicit full-admin bits for megagroups.
|
||||
func NormalizeFullMegagroupAdminRights(ch Channel, rights ChannelAdminRights) ChannelAdminRights {
|
||||
if ch.Megagroup && !ch.Broadcast &&
|
||||
|
|
@ -1235,10 +1244,16 @@ type EditChannelAdminRequest struct {
|
|||
ChannelID int64
|
||||
MemberID int64
|
||||
AdminRights ChannelAdminRights
|
||||
RankSet bool
|
||||
Rank string
|
||||
Date int
|
||||
}
|
||||
|
||||
// HasRank reports whether channels.editAdmin explicitly supplied a rank.
|
||||
func (r EditChannelAdminRequest) HasRank() bool {
|
||||
return r.RankSet || r.Rank != ""
|
||||
}
|
||||
|
||||
// EditChannelAdminResult describes the participant transition.
|
||||
type EditChannelAdminResult struct {
|
||||
Channel Channel
|
||||
|
|
|
|||
|
|
@ -228,6 +228,105 @@ func TestChannelsGetParticipantsHidesAnonymousAdminFromRegularMember(t *testing.
|
|||
}
|
||||
}
|
||||
|
||||
func TestChannelsEditAdminCreatorSelfCanToggleAnonymous(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 41, Phone: "15550002171", FirstName: "Owner"})
|
||||
admin, _ := userStore.Create(ctx, domain.User{AccessHash: 42, Phone: "15550002172", FirstName: "Admin"})
|
||||
channelStore := memory.NewChannelStore()
|
||||
r := New(Config{}, Deps{
|
||||
Users: appusers.NewService(userStore),
|
||||
Channels: appchannels.NewService(channelStore),
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
created, err := r.onMessagesCreateChat(WithUserID(ctx, owner.ID), &tg.MessagesCreateChatRequest{
|
||||
Users: []tg.InputUserClass{&tg.InputUser{UserID: admin.ID, AccessHash: admin.AccessHash}},
|
||||
Title: "Creator Anonymous Group",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create chat: %v", err)
|
||||
}
|
||||
channel := created.Updates.(*tg.Updates).Chats[0].(*tg.Channel)
|
||||
inputChannel := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||||
|
||||
updates, err := r.onChannelsEditAdmin(WithUserID(ctx, owner.ID), &tg.ChannelsEditAdminRequest{
|
||||
Channel: inputChannel,
|
||||
UserID: &tg.InputUser{UserID: owner.ID, AccessHash: owner.AccessHash},
|
||||
AdminRights: tg.ChatAdminRights{
|
||||
Anonymous: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("creator toggles own anonymous admin rights: %v", err)
|
||||
}
|
||||
var participantUpdate *tg.ChannelParticipantCreator
|
||||
for _, update := range updates.(*tg.Updates).Updates {
|
||||
upd, ok := update.(*tg.UpdateChannelParticipant)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if creator, ok := upd.NewParticipant.(*tg.ChannelParticipantCreator); ok && creator.UserID == owner.ID {
|
||||
participantUpdate = creator
|
||||
break
|
||||
}
|
||||
}
|
||||
if participantUpdate == nil {
|
||||
t.Fatalf("editAdmin updates = %+v, want creator participant update", updates.(*tg.Updates).Updates)
|
||||
}
|
||||
if !participantUpdate.AdminRights.Anonymous || !participantUpdate.AdminRights.ChangeInfo || !participantUpdate.AdminRights.AddAdmins {
|
||||
t.Fatalf("creator update rights = %+v, want anonymous plus full creator projection", participantUpdate.AdminRights)
|
||||
}
|
||||
|
||||
participant, err := r.onChannelsGetParticipant(WithUserID(ctx, owner.ID), &tg.ChannelsGetParticipantRequest{
|
||||
Channel: inputChannel,
|
||||
Participant: &tg.InputPeerUser{UserID: owner.ID, AccessHash: owner.AccessHash},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get creator participant: %v", err)
|
||||
}
|
||||
projectedCreator, ok := participant.Participant.(*tg.ChannelParticipantCreator)
|
||||
if !ok {
|
||||
t.Fatalf("participant = %T, want ChannelParticipantCreator", participant.Participant)
|
||||
}
|
||||
if !projectedCreator.AdminRights.Anonymous || !projectedCreator.AdminRights.ChangeInfo || !projectedCreator.AdminRights.AddAdmins {
|
||||
t.Fatalf("projected creator rights = %+v, want anonymous plus full creator projection", projectedCreator.AdminRights)
|
||||
}
|
||||
|
||||
chats, err := r.onChannelsGetChannels(WithUserID(ctx, owner.ID), []tg.InputChannelClass{inputChannel})
|
||||
if err != nil {
|
||||
t.Fatalf("get channels: %v", err)
|
||||
}
|
||||
projectedChannel := chats.(*tg.MessagesChats).Chats[0].(*tg.Channel)
|
||||
rights, ok := projectedChannel.GetAdminRights()
|
||||
if !ok {
|
||||
t.Fatalf("projected channel has no admin rights: %+v", projectedChannel)
|
||||
}
|
||||
if !rights.Anonymous || !rights.ChangeInfo || !rights.AddAdmins {
|
||||
t.Fatalf("projected channel admin rights = %+v, want anonymous plus full creator projection", rights)
|
||||
}
|
||||
|
||||
if _, err := r.onChannelsEditAdmin(WithUserID(ctx, owner.ID), &tg.ChannelsEditAdminRequest{
|
||||
Channel: inputChannel,
|
||||
UserID: &tg.InputUser{UserID: admin.ID, AccessHash: admin.AccessHash},
|
||||
AdminRights: tg.ChatAdminRights{
|
||||
Anonymous: true,
|
||||
ChangeInfo: true,
|
||||
AddAdmins: true,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("promote admin: %v", err)
|
||||
}
|
||||
_, err = r.onChannelsEditAdmin(WithUserID(ctx, admin.ID), &tg.ChannelsEditAdminRequest{
|
||||
Channel: inputChannel,
|
||||
UserID: &tg.InputUser{UserID: owner.ID, AccessHash: owner.AccessHash},
|
||||
AdminRights: tg.ChatAdminRights{
|
||||
ChangeInfo: true,
|
||||
},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "USER_CREATOR") {
|
||||
t.Fatalf("admin edits creator err = %v, want USER_CREATOR", err)
|
||||
}
|
||||
}
|
||||
|
||||
func tgParticipantListHasUser(participants []tg.ChannelParticipantClass, userID int64) bool {
|
||||
for _, participant := range participants {
|
||||
for _, id := range channelParticipantUserRefs(participant) {
|
||||
|
|
|
|||
|
|
@ -402,6 +402,7 @@ func (r *Router) onChannelsEditAdmin(ctx context.Context, req *tg.ChannelsEditAd
|
|||
ChannelID: channelID,
|
||||
MemberID: target.ID,
|
||||
AdminRights: domainChannelAdminRights(req.AdminRights),
|
||||
RankSet: req.Flags.Has(0),
|
||||
Rank: req.Rank,
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -780,10 +780,7 @@ func tgChatAdminRights(rights domain.ChannelAdminRights) tg.ChatAdminRights {
|
|||
}
|
||||
|
||||
func creatorProjectionAdminRights(rights domain.ChannelAdminRights) domain.ChannelAdminRights {
|
||||
creatorRights := domain.CreatorChannelAdminRights()
|
||||
creatorRights.Anonymous = rights.Anonymous
|
||||
creatorRights.ManageDirectMessages = rights.ManageDirectMessages
|
||||
return creatorRights
|
||||
return domain.CreatorProjectionAdminRights(rights)
|
||||
}
|
||||
|
||||
func domainChannelAdminRights(rights tg.ChatAdminRights) domain.ChannelAdminRights {
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@ func (r *Router) langpackLanguages(ctx context.Context, langPack string) []tg.La
|
|||
if langPack == "" {
|
||||
langPack = langPackFromClient(ctx)
|
||||
}
|
||||
_ = langPack
|
||||
return []tg.LangPackLanguage{
|
||||
langPack = strings.ToLower(langPack)
|
||||
languages := []tg.LangPackLanguage{
|
||||
{
|
||||
Official: true,
|
||||
Name: "English",
|
||||
|
|
@ -114,6 +114,20 @@ func (r *Router) langpackLanguages(ctx context.Context, langPack string) []tg.La
|
|||
TranslationsURL: "",
|
||||
},
|
||||
}
|
||||
if langPack == "android" {
|
||||
languages = append(languages, tg.LangPackLanguage{
|
||||
Official: true,
|
||||
Rtl: true,
|
||||
Name: "Persian",
|
||||
NativeName: "فارسی",
|
||||
LangCode: "fa",
|
||||
PluralCode: "fa",
|
||||
StringsCount: 11002,
|
||||
TranslatedCount: 11002,
|
||||
TranslationsURL: "",
|
||||
})
|
||||
}
|
||||
return languages
|
||||
}
|
||||
|
||||
func langPackFromClient(ctx context.Context) string {
|
||||
|
|
@ -124,6 +138,12 @@ func langPackFromClient(ctx context.Context) string {
|
|||
if info.LangPack != "" {
|
||||
return info.LangPack
|
||||
}
|
||||
switch info.ClientType() {
|
||||
case ClientTypeAndroid:
|
||||
return string(ClientTypeAndroid)
|
||||
case ClientTypeTDesktop:
|
||||
return string(ClientTypeTDesktop)
|
||||
}
|
||||
client := strings.ToLower(info.DeviceModel + " " + info.SystemVersion + " " + info.AppVersion)
|
||||
if strings.Contains(client, "android") {
|
||||
return "android"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ func TestLangpackGetLanguagesCurrentAndLegacy(t *testing.T) {
|
|||
if err := (&tg.LangpackGetLanguagesRequest{LangPack: "tdesktop"}).Encode(&in); err != nil {
|
||||
t.Fatalf("encode request: %v", err)
|
||||
}
|
||||
assertLangpackLanguages(t, r, context.Background(), &in)
|
||||
langs := dispatchLangpackLanguages(t, r, context.Background(), &in)
|
||||
assertHasLangpackLanguage(t, langs, "en")
|
||||
assertNoLangpackLanguage(t, langs, "fa")
|
||||
})
|
||||
|
||||
t.Run("legacy android no args", func(t *testing.T) {
|
||||
|
|
@ -30,7 +32,17 @@ func TestLangpackGetLanguagesCurrentAndLegacy(t *testing.T) {
|
|||
AppVersion: "12.7.3",
|
||||
LangCode: "en",
|
||||
})
|
||||
assertLangpackLanguages(t, r, ctx, &in)
|
||||
langs := dispatchLangpackLanguages(t, r, ctx, &in)
|
||||
assertHasLangpackLanguage(t, langs, "en")
|
||||
assertHasLangpackLanguage(t, langs, "fa")
|
||||
})
|
||||
|
||||
t.Run("legacy android from cached client type", func(t *testing.T) {
|
||||
var in bin.Buffer
|
||||
in.PutID(0x800fd57d)
|
||||
ctx := WithClientInfo(context.Background(), ClientInfo{Type: ClientTypeAndroid})
|
||||
langs := dispatchLangpackLanguages(t, r, ctx, &in)
|
||||
assertHasLangpackLanguage(t, langs, "fa")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +70,23 @@ func TestLangpackGetLanguage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLangpackAndroidPersianLanguage(t *testing.T) {
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
lang := r.langpackLanguage(context.Background(), "android", "fa")
|
||||
if lang.LangCode != "fa" || lang.PluralCode != "fa" || lang.Name != "Persian" || lang.NativeName != "فارسی" || !lang.Rtl {
|
||||
t.Fatalf("android fa language = %+v", lang)
|
||||
}
|
||||
|
||||
languages := r.langpackLanguages(androidClientContext(), "")
|
||||
for _, item := range languages {
|
||||
if item.LangCode == "fa" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("android languages = %+v, want fa entry", languages)
|
||||
}
|
||||
|
||||
func TestLegacyLangpackGetLangPack(t *testing.T) {
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
|
|
@ -157,7 +186,7 @@ func TestLegacyAccountRegisterDevice(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func assertLangpackLanguages(t *testing.T, r *Router, ctx context.Context, in *bin.Buffer) {
|
||||
func dispatchLangpackLanguages(t *testing.T, r *Router, ctx context.Context, in *bin.Buffer) []tg.LangPackLanguage {
|
||||
t.Helper()
|
||||
enc, err := r.Dispatch(ctx, [8]byte{}, 0, in)
|
||||
if err != nil {
|
||||
|
|
@ -171,8 +200,25 @@ func assertLangpackLanguages(t *testing.T, r *Router, ctx context.Context, in *b
|
|||
if err := langs.Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if len(langs.Elems) == 0 || langs.Elems[0].LangCode != "en" {
|
||||
t.Fatalf("languages = %+v, want English entry", langs.Elems)
|
||||
return langs.Elems
|
||||
}
|
||||
|
||||
func assertHasLangpackLanguage(t *testing.T, langs []tg.LangPackLanguage, code string) {
|
||||
t.Helper()
|
||||
for _, lang := range langs {
|
||||
if lang.LangCode == code {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("languages = %+v, want %s entry", langs, code)
|
||||
}
|
||||
|
||||
func assertNoLangpackLanguage(t *testing.T, langs []tg.LangPackLanguage, code string) {
|
||||
t.Helper()
|
||||
for _, lang := range langs {
|
||||
if lang.LangCode == code {
|
||||
t.Fatalf("languages = %+v, want no %s entry", langs, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -552,7 +552,8 @@ func adminRightsSubset(want, have domain.ChannelAdminRights) bool {
|
|||
(!want.AddAdmins || have.AddAdmins) &&
|
||||
(!want.ManageCall || have.ManageCall) &&
|
||||
(!want.Anonymous || have.Anonymous) &&
|
||||
(!want.ManageRanks || have.ManageRanks)
|
||||
(!want.ManageRanks || have.ManageRanks) &&
|
||||
(!want.ManageDirectMessages || have.ManageDirectMessages)
|
||||
}
|
||||
|
||||
// checkEditMemberRank validates a rank-only (member tag) edit: creator edits
|
||||
|
|
|
|||
|
|
@ -282,9 +282,6 @@ func (s *ChannelStore) EditChannelAdmin(_ context.Context, req domain.EditChanne
|
|||
if !canAddChannelAdmins(actor) {
|
||||
return domain.EditChannelAdminResult{}, domain.ErrChannelAdminRequired
|
||||
}
|
||||
if actor.Role != domain.ChannelRoleCreator && !adminRightsSubset(req.AdminRights, actor.AdminRights) {
|
||||
return domain.EditChannelAdminResult{}, domain.ErrChannelRightForbidden
|
||||
}
|
||||
previous, ok := s.members[req.ChannelID][req.MemberID]
|
||||
if !ok {
|
||||
previous = domain.ChannelMember{
|
||||
|
|
@ -300,7 +297,41 @@ func (s *ChannelStore) EditChannelAdmin(_ context.Context, req domain.EditChanne
|
|||
}
|
||||
}
|
||||
if previous.Role == domain.ChannelRoleCreator {
|
||||
return domain.EditChannelAdminResult{}, domain.ErrChannelUserCreator
|
||||
if req.MemberID != req.UserID || channel.CreatorUserID != req.UserID || actor.Role != domain.ChannelRoleCreator {
|
||||
return domain.EditChannelAdminResult{}, domain.ErrChannelUserCreator
|
||||
}
|
||||
member := previous
|
||||
member.Status = domain.ChannelMemberActive
|
||||
member.LeftAt = 0
|
||||
member.AdminRights = domain.CreatorProjectionAdminRights(req.AdminRights)
|
||||
if req.HasRank() {
|
||||
member.Rank = req.Rank
|
||||
}
|
||||
s.members[req.ChannelID][req.MemberID] = member
|
||||
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
|
||||
ChannelID: req.ChannelID,
|
||||
UserID: req.UserID,
|
||||
Date: req.Date,
|
||||
Type: domain.ChannelAdminLogParticipantPromote,
|
||||
PrevParticipant: ptrChannelMember(previous),
|
||||
NewParticipant: ptrChannelMember(member),
|
||||
})
|
||||
s.refreshChannelCountsLocked(req.ChannelID)
|
||||
channel = s.channels[req.ChannelID]
|
||||
event := transientChannelParticipantEvent(channel.ID, req.UserID, previous, member, req.Date)
|
||||
recipients := s.activeMemberIDsLocked(req.ChannelID, 0, 0)
|
||||
recipients = append(recipients, req.MemberID)
|
||||
return domain.EditChannelAdminResult{
|
||||
Channel: channel,
|
||||
Previous: previous,
|
||||
Participant: member,
|
||||
Event: event,
|
||||
Recipients: recipients,
|
||||
Date: req.Date,
|
||||
}, nil
|
||||
}
|
||||
if actor.Role != domain.ChannelRoleCreator && !adminRightsSubset(req.AdminRights, actor.AdminRights) {
|
||||
return domain.EditChannelAdminResult{}, domain.ErrChannelRightForbidden
|
||||
}
|
||||
member := previous
|
||||
member.InviterUserID = req.UserID
|
||||
|
|
@ -312,7 +343,9 @@ func (s *ChannelStore) EditChannelAdmin(_ context.Context, req domain.EditChanne
|
|||
}
|
||||
}
|
||||
member.AdminRights = domain.NormalizeFullMegagroupAdminRights(channel, req.AdminRights)
|
||||
member.Rank = req.Rank
|
||||
if req.HasRank() {
|
||||
member.Rank = req.Rank
|
||||
}
|
||||
if zeroChannelAdminRights(req.AdminRights) {
|
||||
member.Role = domain.ChannelRoleMember
|
||||
member.Rank = ""
|
||||
|
|
|
|||
|
|
@ -173,6 +173,68 @@ func TestChannelCreatorLeaveTransfersOwner(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestChannelCreatorCanEditOwnAdminRights(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := NewChannelStore()
|
||||
created, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: 1,
|
||||
Title: "creator self admin",
|
||||
Megagroup: true,
|
||||
MemberUserIDs: []int64{2},
|
||||
Date: 1_700_000_114,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
|
||||
edited, err := store.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
|
||||
UserID: 1,
|
||||
ChannelID: created.Channel.ID,
|
||||
MemberID: 1,
|
||||
AdminRights: domain.ChannelAdminRights{
|
||||
Anonymous: true,
|
||||
},
|
||||
Date: 1_700_000_115,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("creator edits own admin rights: %v", err)
|
||||
}
|
||||
if edited.Participant.Role != domain.ChannelRoleCreator || !edited.Participant.AdminRights.Anonymous || !edited.Participant.AdminRights.ChangeInfo || !edited.Participant.AdminRights.AddAdmins {
|
||||
t.Fatalf("edited creator = %+v, want creator with anonymous plus full creator projection", edited.Participant)
|
||||
}
|
||||
persisted, err := store.GetParticipant(ctx, 1, created.Channel.ID, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("get creator participant: %v", err)
|
||||
}
|
||||
if persisted.Role != domain.ChannelRoleCreator || !persisted.AdminRights.Anonymous || !persisted.AdminRights.ChangeInfo || !persisted.AdminRights.AddAdmins {
|
||||
t.Fatalf("persisted creator = %+v, want creator with anonymous plus full creator projection", persisted)
|
||||
}
|
||||
|
||||
if _, err := store.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
|
||||
UserID: 1,
|
||||
ChannelID: created.Channel.ID,
|
||||
MemberID: 2,
|
||||
AdminRights: domain.ChannelAdminRights{
|
||||
ChangeInfo: true,
|
||||
AddAdmins: true,
|
||||
},
|
||||
Date: 1_700_000_116,
|
||||
}); err != nil {
|
||||
t.Fatalf("promote admin: %v", err)
|
||||
}
|
||||
if _, err := store.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
|
||||
UserID: 2,
|
||||
ChannelID: created.Channel.ID,
|
||||
MemberID: 1,
|
||||
AdminRights: domain.ChannelAdminRights{
|
||||
ChangeInfo: true,
|
||||
},
|
||||
Date: 1_700_000_117,
|
||||
}); !errors.Is(err, domain.ErrChannelUserCreator) {
|
||||
t.Fatalf("admin edits creator err = %v, want ErrChannelUserCreator", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelAdminAndBanDoNotAdvanceChannelPts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := NewChannelStore()
|
||||
|
|
|
|||
|
|
@ -80,6 +80,96 @@ func TestChannelStoreEditAboutPersistsAndChecksPermission(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestChannelStoreCreatorCanEditOwnAdminRights(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
users := NewUserStore(pool)
|
||||
owner, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 141,
|
||||
Phone: "+1888" + suffix + "11",
|
||||
FirstName: "CreatorSelfOwner",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
admin, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 142,
|
||||
Phone: "+1888" + suffix + "12",
|
||||
FirstName: "CreatorSelfAdmin",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create admin: %v", err)
|
||||
}
|
||||
var channelID int64
|
||||
t.Cleanup(func() {
|
||||
if channelID != 0 {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
||||
}
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, admin.ID})
|
||||
})
|
||||
|
||||
channels := NewChannelStore(pool)
|
||||
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: owner.ID,
|
||||
Title: "Creator Self " + suffix,
|
||||
Megagroup: true,
|
||||
MemberUserIDs: []int64{admin.ID},
|
||||
Date: 1700000610,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
channelID = created.Channel.ID
|
||||
|
||||
edited, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
|
||||
UserID: owner.ID,
|
||||
ChannelID: channelID,
|
||||
MemberID: owner.ID,
|
||||
AdminRights: domain.ChannelAdminRights{
|
||||
Anonymous: true,
|
||||
},
|
||||
Date: 1700000611,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("creator edits own admin rights: %v", err)
|
||||
}
|
||||
if edited.Participant.Role != domain.ChannelRoleCreator || !edited.Participant.AdminRights.Anonymous || !edited.Participant.AdminRights.ChangeInfo || !edited.Participant.AdminRights.AddAdmins {
|
||||
t.Fatalf("edited creator = %+v, want creator with anonymous plus full creator projection", edited.Participant)
|
||||
}
|
||||
persisted, err := channels.GetParticipant(ctx, owner.ID, channelID, owner.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get creator participant: %v", err)
|
||||
}
|
||||
if persisted.Role != domain.ChannelRoleCreator || !persisted.AdminRights.Anonymous || !persisted.AdminRights.ChangeInfo || !persisted.AdminRights.AddAdmins {
|
||||
t.Fatalf("persisted creator = %+v, want creator with anonymous plus full creator projection", persisted)
|
||||
}
|
||||
|
||||
if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
|
||||
UserID: owner.ID,
|
||||
ChannelID: channelID,
|
||||
MemberID: admin.ID,
|
||||
AdminRights: domain.ChannelAdminRights{
|
||||
ChangeInfo: true,
|
||||
AddAdmins: true,
|
||||
},
|
||||
Date: 1700000612,
|
||||
}); err != nil {
|
||||
t.Fatalf("promote admin: %v", err)
|
||||
}
|
||||
if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
|
||||
UserID: admin.ID,
|
||||
ChannelID: channelID,
|
||||
MemberID: owner.ID,
|
||||
AdminRights: domain.ChannelAdminRights{
|
||||
ChangeInfo: true,
|
||||
},
|
||||
Date: 1700000613,
|
||||
}); !errors.Is(err, domain.ErrChannelUserCreator) {
|
||||
t.Fatalf("admin edits creator err = %v, want ErrChannelUserCreator", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelStoreAdminLogFiltersAndSearch(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
|
|
@ -783,7 +783,8 @@ func adminRightsSubset(want, have domain.ChannelAdminRights) bool {
|
|||
(!want.AddAdmins || have.AddAdmins) &&
|
||||
(!want.ManageCall || have.ManageCall) &&
|
||||
(!want.Anonymous || have.Anonymous) &&
|
||||
(!want.ManageRanks || have.ManageRanks)
|
||||
(!want.ManageRanks || have.ManageRanks) &&
|
||||
(!want.ManageDirectMessages || have.ManageDirectMessages)
|
||||
}
|
||||
|
||||
// checkEditMemberRank validates a rank-only (member tag) edit: creator edits
|
||||
|
|
|
|||
|
|
@ -35,9 +35,6 @@ func (s *ChannelStore) EditChannelAdmin(ctx context.Context, req domain.EditChan
|
|||
if !canAddChannelAdmins(actor) {
|
||||
return domain.EditChannelAdminResult{}, domain.ErrChannelAdminRequired
|
||||
}
|
||||
if actor.Role != domain.ChannelRoleCreator && !adminRightsSubset(req.AdminRights, actor.AdminRights) {
|
||||
return domain.EditChannelAdminResult{}, domain.ErrChannelRightForbidden
|
||||
}
|
||||
previous, err := s.getChannelMember(ctx, tx, req.ChannelID, req.MemberID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, domain.ErrChannelPrivate) {
|
||||
|
|
@ -56,13 +53,52 @@ func (s *ChannelStore) EditChannelAdmin(ctx context.Context, req domain.EditChan
|
|||
}
|
||||
}
|
||||
if previous.Role == domain.ChannelRoleCreator {
|
||||
return domain.EditChannelAdminResult{}, domain.ErrChannelUserCreator
|
||||
if req.MemberID != req.UserID || channel.CreatorUserID != req.UserID || actor.Role != domain.ChannelRoleCreator {
|
||||
return domain.EditChannelAdminResult{}, domain.ErrChannelUserCreator
|
||||
}
|
||||
member := previous
|
||||
member.Status = domain.ChannelMemberActive
|
||||
member.LeftAt = 0
|
||||
member.AdminRights = domain.CreatorProjectionAdminRights(req.AdminRights)
|
||||
if req.HasRank() {
|
||||
member.Rank = req.Rank
|
||||
}
|
||||
if err := upsertChannelMemberTx(ctx, tx, channel, member); err != nil {
|
||||
return domain.EditChannelAdminResult{}, err
|
||||
}
|
||||
if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{
|
||||
ChannelID: req.ChannelID,
|
||||
UserID: req.UserID,
|
||||
Date: req.Date,
|
||||
Type: domain.ChannelAdminLogParticipantPromote,
|
||||
PrevParticipant: &previous,
|
||||
NewParticipant: &member,
|
||||
}); err != nil {
|
||||
return domain.EditChannelAdminResult{}, err
|
||||
}
|
||||
channel, err = refreshChannelCountsTx(ctx, tx, channel)
|
||||
if err != nil {
|
||||
return domain.EditChannelAdminResult{}, err
|
||||
}
|
||||
event := transientChannelParticipantEvent(channel.ID, req.UserID, previous, member, req.Date)
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return domain.EditChannelAdminResult{}, fmt.Errorf("commit edit channel creator admin: %w", err)
|
||||
}
|
||||
committed = true
|
||||
recipients, _ := s.ListActiveChannelMemberIDs(ctx, req.UserID, req.ChannelID, 0)
|
||||
recipients = append(recipients, req.MemberID)
|
||||
return domain.EditChannelAdminResult{Channel: channel, Previous: previous, Participant: member, Event: event, Recipients: recipients, Date: req.Date}, nil
|
||||
}
|
||||
if actor.Role != domain.ChannelRoleCreator && !adminRightsSubset(req.AdminRights, actor.AdminRights) {
|
||||
return domain.EditChannelAdminResult{}, domain.ErrChannelRightForbidden
|
||||
}
|
||||
member := previous
|
||||
member.InviterUserID = req.UserID
|
||||
member.Status = domain.ChannelMemberActive
|
||||
member.LeftAt = 0
|
||||
member.Rank = req.Rank
|
||||
if req.HasRank() {
|
||||
member.Rank = req.Rank
|
||||
}
|
||||
if previous.Status != domain.ChannelMemberActive {
|
||||
if minPts := channelInitialAvailableMinPts(channel); minPts > member.AvailableMinPts {
|
||||
member.AvailableMinPts = minPts
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue