备份
This commit is contained in:
parent
d1d95618ad
commit
16c261bd36
@ -63,7 +63,7 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
l := subscribe.NewSubscribeLogic(c, svcCtx)
|
||||
resp, err := l.Handler(&req)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Internal Server")
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
c.Header("subscription-userinfo", resp.Header)
|
||||
|
||||
@ -88,7 +88,7 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty
|
||||
code := random.Key(6, 0)
|
||||
taskPayload.Type = queue.EmailTypeVerify
|
||||
taskPayload.Email = req.Email
|
||||
taskPayload.Subject = "Verification code"
|
||||
taskPayload.Subject = "登录验证"
|
||||
|
||||
expireTime := l.svcCtx.Config.VerifyCode.ExpireTime
|
||||
if expireTime == 0 {
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
@ -42,6 +43,7 @@ func NewBindEmailWithVerificationLogic(ctx context.Context, svcCtx *svc.ServiceC
|
||||
// - *types.BindEmailWithVerificationResponse: 包含绑定结果、消息、token、用户ID
|
||||
// - error: 发生错误时返回具体错误
|
||||
func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.BindEmailWithVerificationRequest) (*types.BindEmailWithVerificationResponse, error) {
|
||||
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
||||
// 获取当前用户
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
@ -231,6 +233,17 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
||||
// return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "创建邮箱用户设备记录失败")
|
||||
// }
|
||||
}
|
||||
|
||||
// 清除邮箱用户的缓存,确保更新(如邀请人、设备列表等)可见
|
||||
userToClear := &user.User{Id: emailUserId}
|
||||
// 添加当前邮箱做为 AuthMethod 以便 BatchClearRelatedCache 能清除相关索引缓存
|
||||
// 注意:虽然 email 映射未变,但清除是一个好习惯,且 BatchClearRelatedCache 依赖 AuthMethods 来清除 email 缓存 key
|
||||
userToClear.AuthMethods = []user.AuthMethods{
|
||||
{AuthType: "email", AuthIdentifier: req.Email},
|
||||
}
|
||||
if err := l.svcCtx.UserModel.BatchClearRelatedCache(l.ctx, userToClear); err != nil {
|
||||
l.Errorw("清理邮箱用户缓存失败", logger.Field("error", err.Error()), logger.Field("user_id", emailUserId))
|
||||
}
|
||||
// 4. 生成新的JWT token
|
||||
token, err := l.generateTokenForUser(emailUserId, deviceIdentifier)
|
||||
if err != nil {
|
||||
|
||||
@ -147,7 +147,7 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon
|
||||
|
||||
// 开始数据库事务
|
||||
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
||||
// 1. 预先查找该用户下的所有设备记录 (因为稍后要全删)
|
||||
// 1. 预先查找该用户下的所有设备记录 (因为稍后要迁移)
|
||||
var userDevices []user.Device
|
||||
if err := tx.Where("user_id = ?", currentUser.Id).Find(&userDevices).Error; err != nil {
|
||||
l.Errorw("查询用户设备列表失败", logger.Field("user_id", currentUser.Id), logger.Field("error", err.Error()))
|
||||
@ -159,17 +159,52 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon
|
||||
l.Infow("未识别到当前设备 ID,将执行全量注销并尝试迁移所有已知设备", logger.Field("user_id", currentUser.Id), logger.Field("found_devices", len(userDevices)))
|
||||
}
|
||||
|
||||
// 2. 无条件执行全量删除 (清理旧账号数据)
|
||||
l.Infow("执行账号全量注销-清理旧数据", logger.Field("user_id", currentUser.Id), logger.Field("device_count", len(userDevices)))
|
||||
l.Infow("执行账号全量注销-迁移设备并删除旧数据", logger.Field("user_id", currentUser.Id), logger.Field("device_count", len(userDevices)))
|
||||
|
||||
// 删除所有认证方式
|
||||
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.AuthMethods{}).Error; err != nil {
|
||||
return errors.Wrap(err, "删除认证方式失败")
|
||||
// 2. 循环为每个设备创建新用户并迁移记录 (保留设备ID)
|
||||
for _, dev := range userDevices {
|
||||
// A. 创建新匿名用户
|
||||
newUser, err := l.createAnonymousUser(tx)
|
||||
if err != nil {
|
||||
l.Errorw("为设备分配新用户主体失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
// B. 迁移设备记录 (Update user_id)
|
||||
if err := tx.Model(&user.Device{}).Where("id = ?", dev.Id).Update("user_id", newUser.Id).Error; err != nil {
|
||||
l.Errorw("迁移设备记录失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error()))
|
||||
return errors.Wrap(err, "迁移设备记录失败")
|
||||
}
|
||||
|
||||
// C. 迁移设备认证方式 (Update user_id)
|
||||
if err := tx.Model(&user.AuthMethods{}).
|
||||
Where("user_id = ? AND auth_type = ? AND auth_identifier = ?", currentUser.Id, "device", dev.Identifier).
|
||||
Update("user_id", newUser.Id).Error; err != nil {
|
||||
l.Errorw("迁移设备认证失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error()))
|
||||
return errors.Wrap(err, "迁移设备认证失败")
|
||||
}
|
||||
|
||||
// 如果是当前请求的设备,记录其新 UserID 返回给前端
|
||||
if dev.Id == currentDeviceId || dev.Identifier == l.getIdentifierByDeviceID(userDevices, currentDeviceId) {
|
||||
newUserId = newUser.Id
|
||||
}
|
||||
l.Infow("旧设备已迁移至新匿名账号",
|
||||
logger.Field("old_user_id", currentUser.Id),
|
||||
logger.Field("new_user_id", newUser.Id),
|
||||
logger.Field("device_id", dev.Id),
|
||||
logger.Field("identifier", dev.Identifier))
|
||||
}
|
||||
|
||||
// 删除所有设备记录
|
||||
// 3. 删除旧账号的剩余数据
|
||||
// 删除剩余的认证方式 (排除已迁移的device类型,剩下的如email/mobile等)
|
||||
// 注意:刚才已经把由currentUser拥有的device类型auth都迁移走了,所以这里直接删剩下的即可
|
||||
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.AuthMethods{}).Error; err != nil {
|
||||
return errors.Wrap(err, "删除剩余认证方式失败")
|
||||
}
|
||||
|
||||
// 设备记录已经全部迁移,理论上 user_id = currentUser.Id 的 device 应该没了,但为了保险可以删一下(或者是0)
|
||||
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.Device{}).Error; err != nil {
|
||||
return errors.Wrap(err, "删除设备记录失败")
|
||||
return errors.Wrap(err, "删除残留设备记录失败")
|
||||
}
|
||||
|
||||
// 删除所有订阅
|
||||
@ -182,25 +217,6 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon
|
||||
return errors.Wrap(err, "删除用户失败")
|
||||
}
|
||||
|
||||
// 3. 循环为每个设备重新创建匿名用户 (分配账号,避免重新注册领试用)
|
||||
for _, dev := range userDevices {
|
||||
// 为该设备创建新用户
|
||||
newUser, err := l.registerUserAndDevice(tx, dev.Identifier, dev.Ip, dev.UserAgent)
|
||||
if err != nil {
|
||||
l.Errorw("为旧设备分配新账号失败", logger.Field("identifier", dev.Identifier), logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是当前请求的设备,记录其新 UserID 返回给前端
|
||||
if dev.Id == currentDeviceId || dev.Identifier == l.getIdentifierByDeviceID(userDevices, currentDeviceId) {
|
||||
newUserId = newUser.Id
|
||||
}
|
||||
l.Infow("旧设备已迁移至新匿名账号", logger.Field("old_user_id", currentUser.Id), logger.Field("new_user_id", newUser.Id), logger.Field("identifier", dev.Identifier))
|
||||
}
|
||||
|
||||
// 如果循环结束还没找到当前设备的新ID (极端情况,比如currentDeviceId不在列表中),这里可能需要处理
|
||||
// 通常第一步查询 userDevices 应该包含 currentDeviceId,除非并发删除
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -324,3 +340,25 @@ func (l *DeleteAccountLogic) registerUserAndDevice(tx *gorm.DB, identifier, ip,
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
// createAnonymousUser 创建一个新的匿名用户主体 (仅User表)
|
||||
func (l *DeleteAccountLogic) createAnonymousUser(tx *gorm.DB) (*user.User, error) {
|
||||
// 1. 创建新用户
|
||||
userInfo := &user.User{
|
||||
Salt: "default",
|
||||
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
|
||||
}
|
||||
if err := tx.Create(userInfo).Error; err != nil {
|
||||
l.Errorw("failed to create user", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user failed: %v", err)
|
||||
}
|
||||
|
||||
// 2. 更新推荐码
|
||||
userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id)
|
||||
if err := tx.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil {
|
||||
l.Errorw("failed to update refer code", logger.Field("user_id", userInfo.Id), logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err)
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
@ -97,19 +97,23 @@ func TestDeleteAccount_Guest_SingleDevice(t *testing.T) {
|
||||
// Assertions for Guest User (Should be DELETED)
|
||||
// Because 1 auth (device) and 1 device count -> isMainAccount = false
|
||||
|
||||
// Check Old User deleted
|
||||
var userCount int64
|
||||
db.Model(&user.User{}).Where("refer_code = ?", "ref1").Count(&userCount)
|
||||
assert.Equal(t, int64(0), userCount, "Old User (by refer code) should be deleted")
|
||||
|
||||
// Old device record (ID 10) should be gone
|
||||
var deviceCount int64
|
||||
db.Model(&user.Device{}).Where("id = ?", 10).Count(&deviceCount)
|
||||
assert.Equal(t, int64(0), deviceCount, "Old device record should be deleted")
|
||||
// Device record (ID 10) should PRESERVED but have a NEW user_id
|
||||
var updatedDevice user.Device
|
||||
err = db.Model(&user.Device{}).Where("id = ?", 10).First(&updatedDevice).Error
|
||||
assert.NoError(t, err, "Device record should still exist")
|
||||
assert.NotEqual(t, int64(1), updatedDevice.UserId, "Device should have a new user ID")
|
||||
assert.Equal(t, "device1_id", updatedDevice.Identifier, "Device identifier should remain unchanged")
|
||||
|
||||
// Check if new user created for the device
|
||||
var newDeviceCount int64
|
||||
db.Model(&user.Device{}).Where("identifier = ?", "device1_id").Count(&newDeviceCount)
|
||||
assert.Equal(t, int64(1), newDeviceCount, "New device record should be created")
|
||||
// Check AuthMethod updated
|
||||
var updatedAuth user.AuthMethods
|
||||
err = db.Where("auth_type = ? AND auth_identifier = ?", "device", "device1_id").First(&updatedAuth).Error
|
||||
assert.NoError(t, err, "AuthMethod should still exist")
|
||||
assert.Equal(t, updatedDevice.UserId, updatedAuth.UserId, "AuthMethod should link to new user ID")
|
||||
}
|
||||
|
||||
func TestDeleteAccount_User_WithEmail(t *testing.T) {
|
||||
@ -162,14 +166,19 @@ func TestDeleteAccount_User_WithEmail(t *testing.T) {
|
||||
db.Model(&user.User{}).Where("id = ?", 2).Count(&userCount)
|
||||
assert.Equal(t, int64(0), userCount, "User should be deleted")
|
||||
|
||||
var deviceCount int64
|
||||
db.Model(&user.Device{}).Where("id = ?", 20).Count(&deviceCount)
|
||||
assert.Equal(t, int64(0), deviceCount, "Old device record should be deleted")
|
||||
// Device record (ID 20) should PRESERVED with NEW user_id
|
||||
var updatedDevice user.Device
|
||||
err = db.Model(&user.Device{}).Where("id = ?", 20).First(&updatedDevice).Error
|
||||
assert.NoError(t, err, "Device record should still exist")
|
||||
assert.NotEqual(t, int64(2), updatedDevice.UserId, "Device should have a new user ID")
|
||||
|
||||
var authDeviceCount int64
|
||||
db.Model(&user.AuthMethods{}).Where("user_id = ? AND auth_type = 'device'", 2).Count(&authDeviceCount)
|
||||
assert.Equal(t, int64(0), authDeviceCount, "Device auth should be removed")
|
||||
// Device Auth should PRESERVED with NEW user_id
|
||||
var updatedAuthDevice user.AuthMethods
|
||||
err = db.Model(&user.AuthMethods{}).Where("auth_type = 'device' AND auth_identifier = 'device2_id'").First(&updatedAuthDevice).Error
|
||||
assert.NoError(t, err, "Device auth should still exist")
|
||||
assert.Equal(t, updatedDevice.UserId, updatedAuthDevice.UserId)
|
||||
|
||||
// Email Auth should be REMOVED
|
||||
var authEmailCount int64
|
||||
db.Model(&user.AuthMethods{}).Where("user_id = ? AND auth_type = 'email'", 2).Count(&authEmailCount)
|
||||
assert.Equal(t, int64(0), authEmailCount, "Email auth should be removed")
|
||||
@ -232,34 +241,20 @@ func TestDeleteAccount_User_MultiDevice(t *testing.T) {
|
||||
db.Model(&user.User{}).Where("id = ?", 3).Count(&userCount)
|
||||
assert.Equal(t, int64(0), userCount, "Old User should be deleted")
|
||||
|
||||
// Old device records should be deleted
|
||||
var device1Count int64
|
||||
db.Model(&user.Device{}).Where("id = ?", 31).Count(&device1Count)
|
||||
assert.Equal(t, int64(0), device1Count, "Old device 1 record should be deleted")
|
||||
// Device 1 (ID 31) should be PRESERVED
|
||||
var updatedDev1 user.Device
|
||||
err = db.Model(&user.Device{}).Where("id = ?", 31).First(&updatedDev1).Error
|
||||
assert.NoError(t, err, "Device 1 should still exist")
|
||||
assert.NotEqual(t, int64(3), updatedDev1.UserId, "Device 1 should have new UserID")
|
||||
|
||||
var device2Count int64
|
||||
db.Model(&user.Device{}).Where("id = ?", 32).Count(&device2Count)
|
||||
assert.Equal(t, int64(0), device2Count, "Old device 2 record should be deleted")
|
||||
// Device 2 (ID 32) should be PRESERVED
|
||||
var updatedDev2 user.Device
|
||||
err = db.Model(&user.Device{}).Where("id = ?", 32).First(&updatedDev2).Error
|
||||
assert.NoError(t, err, "Device 2 should still exist")
|
||||
assert.NotEqual(t, int64(3), updatedDev2.UserId, "Device 2 should have new UserID")
|
||||
|
||||
// NEW Device records should be created
|
||||
var newDevice1Count int64
|
||||
db.Model(&user.Device{}).Where("identifier = ?", "device3_1").Count(&newDevice1Count)
|
||||
assert.Equal(t, int64(1), newDevice1Count, "New Device 1 should be created")
|
||||
|
||||
var newDevice2Count int64
|
||||
db.Model(&user.Device{}).Where("identifier = ?", "device3_2").Count(&newDevice2Count)
|
||||
assert.Equal(t, int64(1), newDevice2Count, "New Device 2 should be created")
|
||||
|
||||
// Verify they are anonymous (different users, assuming registerUserAndDevice creates new user each time)
|
||||
var newDev1 user.Device
|
||||
db.Where("identifier = ?", "device3_1").First(&newDev1)
|
||||
assert.NotEqual(t, int64(3), newDev1.UserId, "New Device 1 should have new UserID")
|
||||
|
||||
var newDev2 user.Device
|
||||
db.Where("identifier = ?", "device3_2").First(&newDev2)
|
||||
assert.NotEqual(t, int64(3), newDev2.UserId, "New Device 2 should have new UserID")
|
||||
|
||||
assert.NotEqual(t, newDev1.UserId, newDev2.UserId, "Devices should have independent user accounts")
|
||||
// Verify they are independent users
|
||||
assert.NotEqual(t, updatedDev1.UserId, updatedDev2.UserId, "Devices should have independent user accounts")
|
||||
}
|
||||
|
||||
func TestDeleteAccount_MissingDeviceID(t *testing.T) {
|
||||
@ -298,13 +293,9 @@ func TestDeleteAccount_MissingDeviceID(t *testing.T) {
|
||||
db.Model(&user.User{}).Where("refer_code = ?", "ref4").Count(&userCount)
|
||||
assert.Equal(t, int64(0), userCount, "User should be deleted even without device context")
|
||||
|
||||
// Old device should be deleted
|
||||
var deviceCount int64
|
||||
db.Model(&user.Device{}).Where("id = ?", 40).Count(&deviceCount)
|
||||
assert.Equal(t, int64(0), deviceCount, "Old device record should be deleted")
|
||||
|
||||
// New device should be created for the orphaned device
|
||||
var newDeviceCount int64
|
||||
db.Model(&user.Device{}).Where("identifier = ?", "device4_id").Count(&newDeviceCount)
|
||||
assert.Equal(t, int64(1), newDeviceCount, "Device should be reset to new account even if caller didn't specify deviceID")
|
||||
// Device record (ID 40) should be PRESERVED
|
||||
var updatedDevice user.Device
|
||||
err = db.Model(&user.Device{}).Where("id = ?", 40).First(&updatedDevice).Error
|
||||
assert.NoError(t, err, "Device record should still exist")
|
||||
assert.NotEqual(t, int64(4), updatedDevice.UserId, "Device should have a new user ID")
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
@ -37,6 +38,7 @@ type CacheKeyPayload struct {
|
||||
}
|
||||
|
||||
func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error {
|
||||
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email)
|
||||
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
||||
if err != nil {
|
||||
|
||||
18
pkg/aes/manual_decrypt_test.go
Normal file
18
pkg/aes/manual_decrypt_test.go
Normal file
@ -0,0 +1,18 @@
|
||||
package pkgaes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestManualDecrypt(t *testing.T) {
|
||||
cipherText := "rLuw+6cV+o3+pVoMdeZ0vOqoRaRvMpUV7VNgEXY9qYFOdGPZ5eQ6KashmOI1d7B6lzbYa0ccqOGFBM2Xfon4GzF/WrYf+jyWD673UIWGiQt4QOsUBZ7k7X1wMHYXsZGaau4mv0YD/8b5raY6s/QNh5mdihXTsdsZ1PIPmpmoiYVMUcOl1WUfoSXg/iSB5aX64Rb9NvPeRFExoo22A+rPpP3n1txMOecmDBBaoCwEr5lUF5I53d/DaZxjzB0BJ9RcA0jaaecuvDG7QJ6n7kVFtWlB+OEBmtklN17a9Bh0m+9DOB9axu3FjBsOaterDa0ufJtfyW/jPvCgKZKclzNS8xhrZrDY9BUt8kIpPRTi6974q8rayvl/ISxQihOm/FiJ+x/zEr6hLekFhXvlDPcV5lyzT6wjUEldkM0u3Ldiqdv3e0eYUyqoaTjnJCjlSfkb2wKX14bn984tYK5IfU6OjLCEUSiAFSzeHtEmpfb+861sJq/EJep7TeEsUqJZNRY2KUAawUjnAtKSlX7kHjvZGFicZqlQUGcha9CPSOpwnGeZz51q5JXJo1H7CqnYGyZZZrIkB+qi4ZK0EGkO0Mm/cLun5a1sWdkgfQixQW4jKRnjrdohAlbLV4AC9tUODUzgl1Ot+7xP2+zo4SbOGQ8zE+sthtBme0NeMHjW00magCHJbpV+bnVZHr3jGpQQUMAdNAyRpQdIn3Nitv3Hun/HLU3EhT38dIBHGkx47RMN0NKkKcePqN3ImIdqsM1jR+GnK5oM2qUlra2tk06bKHMAOo8csmIMIwwh7yhI0bz+UaMDQjPf6/YdYNQaB40vAokbjxC1pEWDvpSR+QSn7NnXzLZf5iwTvkErwWolbJbJCV7YA5zq1PBNVkSY5d3lj0iVe9/oDqcnDWOFgXrbZ1+QowceJSumEXna7M5RMp7tBJUxlyTag7z1jKVBqWv7ydWNQnBT+MpzHLzXOdAuClrKwrYgeaXafqsTZvsNGA7Jtfr+eWIfifT1f/6yqiOa90ZqPv1dmpSIgOTA9NpOPvChQ1VicC9SiS/q0lD9/ZD5H9PvmFylo4DGKGmpEXrnOSy2770WCmNJmjuxf68NBcR/6mN9JBd7XnS+BjGXymybMIVZvy/dsC6zU2AKSolSNjeraP3zNl1fOQNNFDDnd+y/z4RHgbRvGg/BkYygx+sz0Fc87miccLIG2fIaJeGNSg+VFSlWlAFmhv07hZk6k3g/+J+3u1jUiCtQfAoUcE2f18OtdOCSib49e63uCH1NQIHp"
|
||||
keyStr := "c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx"
|
||||
ivStr := "188ec64c36b50c13"
|
||||
|
||||
plaintext, err := Decrypt(cipherText, keyStr, ivStr)
|
||||
if err != nil {
|
||||
t.Fatalf("Decryption failed: %v", err)
|
||||
}
|
||||
fmt.Printf("\nDEC_START\n%s\nDEC_END\n", plaintext)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user