Commit 65297deb authored by hujiebin's avatar hujiebin

feat:剩下groupMic了

parent d8f57d9c
......@@ -119,6 +119,7 @@ const (
const DefaultMsgParallelSize = 20
const (
CREATE_GROUP_MAX_ATTEMPT = 10
OldGroupNamePrefix = "@TGS#"
NewGroupNamePrefix = "HTGS#"
OverseaGroupNamePrefix = NewGroupNamePrefix + "a"
)
......
package mgr_e
import "git.hilo.cn/hilo-common/resource/mysql"
type MgrType mysql.Type
const (
//主账号
MainAccountMgrType MgrType = 1
//次账号
SubAccountMgrType MgrType = 2
)
type ReportPageType mysql.Type
const (
VedioPageType ReportPageType = 1
UserInfoPageType ReportPageType = 2
SessionPageType ReportPageType = 3
MatchHistroyPageType ReportPageType = 4
GroupPageType ReportPageType = 5
)
type ReportStatus mysql.Type
const (
//未处理
NoDealReportStatus ReportStatus = 1
//已处理
HasDealReportStatus ReportStatus = 2
)
type ReportReasonType mysql.Type
const (
//语言骚扰
VerbalHarassment ReportReasonType = 1
Nudity ReportReasonType = 2
IndecentMsg ReportReasonType = 3
IndecentPic ReportReasonType = 4
//11.在多个房间骚扰 12.侵犯他人隐私 13.色情暴力内容 14.损害官方利益
)
//用户状态
type UserStatus mysql.Type
const (
//正常
NomalUserStatus UserStatus = 1
//冻结
FreezeUserStatus UserStatus = 2
)
//报告处理类型
type MgrDealType = mysql.Type
const (
//忽略
IgnoreDealType MgrDealType = 1
//冻结24小时
FreezeDealType24 MgrDealType = 2
//冻结72小时
FreezeDealType72 MgrDealType = 3
//冻结30小时
FreezeDealType720 MgrDealType = 4
//冻结永久
FreezeDealTypeForever MgrDealType = 10
//解除封禁
CancelFreezeDealType MgrDealType = 11
//删除图片
DelUserAvatar MgrDealType = 12
//冻结7天(168小时)
FreezeDealType168 MgrDealType = 13
)
var MgrDealTypeDesc = map[MgrDealType]string{
FreezeDealType24: "封锁24小时",
FreezeDealType72: "封锁72小时",
FreezeDealType168: "封锁7天",
FreezeDealType720: "封锁30天",
FreezeDealTypeForever: "永久封锁",
}
//报告处理原因类型
type MgrDealReasonType = mysql.Type
const (
//无
None MgrDealReasonType = 1
//色情性
Pornographic MgrDealReasonType = 2
//侮辱谩骂
Insult MgrDealReasonType = 3
//不尊重宗教
Religion MgrDealReasonType = 4
//恐怖活动
Terrorism MgrDealReasonType = 5
//违法违规
BreakingLaw MgrDealReasonType = 6
//宣传其它APP
OtherApp MgrDealReasonType = 7
// 在多个房间骚扰
RoomBother MgrDealReasonType = 11
// 侵犯他人隐私
PrivacyBad MgrDealReasonType = 12
// 色情暴力内容
SexViolence MgrDealReasonType = 13
// 损害官方利益
OfficialEffect MgrDealReasonType = 14
)
var MgrDealReasonTypeDesc = map[MgrDealReasonType]string{
Pornographic: "色情性",
Insult: "侮辱谩骂",
Religion: "不尊重宗教",
Terrorism: "恐怖活动",
BreakingLaw: "违法违规",
OtherApp: "宣传其它APP",
RoomBother: "在多个房间骚扰",
PrivacyBad: "侵犯他人隐私",
SexViolence: "色情暴力内容",
OfficialEffect: "损害官方利益",
}
type MgrOriginType = mysql.Type
const (
//报告
ReportOriginType MgrOriginType = 1
//用户页面
UserOriginType MgrOriginType = 2
)
//管理人钻石发放单子状态
type MgrSendDiamondBillStatus = mysql.Type
const (
NoSend MgrSendDiamondBillStatus = 1
HasSend MgrSendDiamondBillStatus = 2
)
//管理人发钻石详情状态
type MgrSendDiamondBillDetailType = mysql.Type
const (
//活动扶持
ActivitySupport MgrSendDiamondBillDetailType = 1
//活动奖励
ActivityAward MgrSendDiamondBillDetailType = 2
)
type MgrSendDiamondBillDetailStatus = mysql.Type
const (
//还没发奖
NoSendDetailStatus MgrSendDiamondBillDetailStatus = 1
//发奖成功
SendSuccessDetailStatus MgrSendDiamondBillDetailStatus = 2
//发奖失败
SendFailDetailStatus MgrSendDiamondBillDetailStatus = 3
)
type OpenScreenType = mysql.Type
const (
CommonOpenScreenType OpenScreenType = 0
CpScreenType OpenScreenType = 1
WeekScreenType OpenScreenType = 2
)
type TypeMgrImeiLog = mysql.Type
const (
AddMgrImeiLog TypeMgrImeiLog = 1
DelMgrImeiLog TypeMgrImeiLog = 2
)
var OperationLogKey = "operationLogKey"
package mgr_m
import (
"git.hilo.cn/hilo-common/domain"
"git.hilo.cn/hilo-common/resource/mysql"
"gorm.io/gorm"
)
type MgrOperationModuleUrl struct {
mysql.Entity
ModuleId mysql.ID
Method string
FullPath string
}
type MgrOperationLog struct {
mysql.Entity
ModuleId mysql.ID
ModuleUrlId mysql.ID
Content string
Description string
MediaUrls string
TargetUid mysql.ID
OperUid mysql.ID
MgrId mysql.ID
}
// 根据path获取操作模块
func GetOperationModuleUrl(model *domain.Model, method, fullPath string) MgrOperationModuleUrl {
var config MgrOperationModuleUrl
if err := model.Db.Model(MgrOperationModuleUrl{}).Where("method = ? AND full_path = ?", method, fullPath).First(&config).Error; err != nil {
if err != gorm.ErrRecordNotFound {
model.Log.Errorf("GetOperationModuleUrl fail:%v", err)
}
}
return config
}
// 保存操作日志
func SaveOperationLog(model *domain.Model, log MgrOperationLog) error {
err := model.Db.Create(&log).Error
return err
}
package user_m
import (
"git.hilo.cn/hilo-common/domain"
"git.hilo.cn/hilo-common/resource/mysql"
"time"
)
type UserGlobalBroadcast struct {
mysql.Entity
*domain.Model `gorm:"-"`
UserId mysql.ID
Msg mysql.Str
GroupId mysql.Str
Status mysql.UserYesNo
}
type UserGlobalBroadcastProhibit struct {
mysql.Entity
*domain.Model `gorm:"-"`
UserId mysql.ID
UserGlobalBroadcastId mysql.ID
}
//
type UserGlobalBroadcastLimit struct {
mysql.Entity
*domain.Model `gorm:"-"`
UserId mysql.ID
EndTime *time.Time
}
//管理全球广播的人
type GlobalBroadcastManager struct {
mysql.Entity
*domain.Model `gorm:"-"`
UserId mysql.ID
}
package group_s
import (
"git.hilo.cn/hilo-common/domain"
"git.hilo.cn/hilo-common/resource/mysql"
"hilo-group/_const/enum/country_e"
"hilo-group/domain/model/country_m"
"hilo-group/domain/model/user_m"
"hilo-group/myerr/bizerr"
)
// 检查是否App管理员
// 1. 全球广播管理员 gm
// 2. 国家管理员 cm
func (s *GroupService) CheckAppManager(userId mysql.ID) (gm bool, cm bool, err error) {
var n int64
if err := mysql.Db.Model(&user_m.GlobalBroadcastManager{}).Where(&user_m.GlobalBroadcastManager{UserId: userId}).Count(&n).Error; err != nil {
return false, false, err
}
if n > 0 {
gm = true
}
var _model = domain.CreateModel(s.svc.CtxAndDb)
countryManager, err := country_m.GetCountryMgr(_model, userId)
if err != nil {
return false, false, err
}
if countryManager != nil && countryManager.Role == country_e.CountryMgrManager {
cm = true
}
return
}
// 检查是否有重置/删除权限
// conditions
// 1. userId是国家管理员
// 2. userId和resetUserId是同国
// 3. resetUserId财富等级小于5级
// return
// true: 有权限
// err: 无权限的err
func (s *GroupService) CheckCountryManagerPermission(userId, resetUserId mysql.ID) (bool, error) {
var _model = domain.CreateModel(s.svc.CtxAndDb)
countryManager, err := country_m.GetCountryMgr(_model, userId)
if err != nil {
return false, err
}
if countryManager == nil || countryManager.Role != country_e.CountryMgrManager {
return false, bizerr.ManagerNoPermission
}
user, err := user_m.GetUser(_model, resetUserId)
if err != nil {
return false, err
}
grade, _, err := user_m.GetWealthGrade(_model, resetUserId)
if err != nil {
return false, err
}
if grade >= 5 || countryManager.Country != user.Country {
return false, bizerr.ManagerNoUserPermission
}
return true, nil
}
......@@ -30,10 +30,11 @@ var (
ResHeadwearDiamondNoUse = myerr.NewBusinessCode(5003, "Headwear can not buy", myerr.BusinessData{}) //头饰不能买
ResPropertyDiamondNoUse = myerr.NewBusinessCode(5004, "Property can not buy", myerr.BusinessData{}) //头饰不能买
UserMedalThresholdLimit = myerr.NewBusinessCode(9006, "勋章条件未达到", myerr.BusinessData{})
UserHeadwearHasEnd = myerr.NewBusinessCode(9014, "用户头饰已经过期, 不能赠送", myerr.BusinessData{})
UserPropertyHasEnd = myerr.NewBusinessCode(9015, "用户座驾已经过期, 不能赠送", myerr.BusinessData{})
EditCd = myerr.NewBusinessCode(9017, "not allow to edit", myerr.BusinessData{}) // 编辑cd中
UserMedalThresholdLimit = myerr.NewBusinessCode(9006, "勋章条件未达到", myerr.BusinessData{})
UserGlobalBroadcastManagerNo = myerr.NewBusinessCode(9008, "不是全球广播管理人", myerr.BusinessData{})
UserHeadwearHasEnd = myerr.NewBusinessCode(9014, "用户头饰已经过期, 不能赠送", myerr.BusinessData{})
UserPropertyHasEnd = myerr.NewBusinessCode(9015, "用户座驾已经过期, 不能赠送", myerr.BusinessData{})
EditCd = myerr.NewBusinessCode(9017, "not allow to edit", myerr.BusinessData{}) // 编辑cd中
// 麦位
GroupMicNoPermission = myerr.NewBusinessCode(12000, "Mic has no permission to mic", myerr.BusinessData{}) // 麦位没有操作的权限
......@@ -78,6 +79,8 @@ var (
// 超级管理人
OfficialStaffLimit = myerr.NewBusinessCode(22001, "Operation failed", myerr.BusinessData{})
GamingCannotKick = myerr.NewBusinessCode(27050, "The game has already started and the user cannot be kicked out", myerr.BusinessData{}) // 游戏已经开始,不能踢出用户
ManagerNoPermission = myerr.NewBusinessCode(27001, "没有权限进行重置删除操作", myerr.BusinessData{})
ManagerNoUserPermission = myerr.NewBusinessCode(27002, "没有权限对该用户进行重置删除操作", myerr.BusinessData{})
GamingCannotKick = myerr.NewBusinessCode(27050, "The game has already started and the user cannot be kicked out", myerr.BusinessData{}) // 游戏已经开始,不能踢出用户
)
......@@ -1482,3 +1482,224 @@ func SendTextMsg(c *gin.Context) (*mycontext.MyContext, error) {
resp.ResponseOk(c, failedCount)
return myContext, nil
}
// @Tags 群组
// @Summary 复制群到海外版
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId formData string false "群ID"
// @Param size formData uint false "批量单位"
// @Param limit formData uint false "批量数"
// @Success 200 {object} []string
// @Router /v1/imGroup/upgrade [put]
func UpgradeGroup(c *gin.Context) (*mycontext.MyContext, error) {
myContext := mycontext.CreateMyContext(c.Keys)
userId, err := req.GetUserId(c)
if err != nil {
return myContext, err
}
if !user_m.IsSuperUser(userId) {
return myContext, bizerr.NoPrivileges
}
model := domain.CreateModelContext(myContext)
failed := make([]string, 0)
groupId := c.PostForm("groupId")
if len(groupId) > 0 {
gi, err := group_m.GetGroupInfo(model, groupId)
if err != nil {
return myContext, err
}
if gi == nil {
return myContext, bizerr.GroupNotFound
}
// fixme:
if gi.Type == group_e.OverseaRoom {
model.Log.Infof("UpgradeGroup, no need to upgrade %s", groupId)
} else {
if err = upgradeRoom(model, gi); err != nil {
failed = append(failed, gi.ImGroupId)
}
}
} else {
size, err := strconv.Atoi(c.PostForm("size"))
if err != nil || size == 0 {
return myContext, bizerr.InvalidParameter
} else {
limit, _ := strconv.Atoi(c.PostForm("limit"))
g := group_m.GroupInfo{Type: group_e.LocalRoom}
rec, err := g.FindAllGroups(model.Db)
if err != nil {
return myContext, err
}
index := 0
for start := 0; start < len(rec) && index < limit; start += size {
end := start + size
if end >= len(rec) {
end = len(rec)
}
go func(index int, groups []group_m.GroupInfo) {
model.Log.Infof("Rountine %d process %d", index, len(groups))
for i, g := range groups {
model.Log.Infof("Routine %d upgrading %s", index, g.ImGroupId)
if err = upgradeRoom(model, &g); err != nil {
model.Log.Errorf("Upgrade %s failed", g.ImGroupId)
}
if i > 0 && i%50 == 0 {
time.Sleep(time.Second)
}
}
}(index, rec[start:end])
index++
}
}
}
resp.ResponseOk(c, failed)
return myContext, nil
}
func upgradeRoom(model *domain.Model, gi *group_m.GroupInfo) error {
if strings.HasPrefix(gi.TxGroupId, group_e.OverseaGroupNamePrefix) {
// 已经是新版,无需处理
return nil
}
if strings.HasPrefix(gi.TxGroupId, group_e.OldGroupNamePrefix) {
// 升级群号,然后两边都升级
newGroupId := group_e.NewGroupNamePrefix + gi.TxGroupId[len(group_e.OldGroupNamePrefix):]
// 避免TX报错
if len(gi.Name) <= 0 {
gi.Name = "empty"
}
txGroupId, err := tencentyun.CreateGroup(gi.Name, newGroupId)
if err != nil {
return err
}
g := group_m.GroupInfo{
TxGroupId: txGroupId,
Type: group_e.OverseaRoom,
}
db := g.Update(model, gi.ImGroupId, []string{"tx_group_id", "type"})
if db.Error != nil {
return err
}
tencentyun.DestroyGroup(gi.TxGroupId, true)
} else {
// 在海外建一个同样的群即可
// 避免TX报错
if len(gi.Name) <= 0 {
gi.Name = "empty"
}
_, err := tencentyun.CreateGroup(gi.Name, gi.TxGroupId)
if err != nil {
return err
}
// 更新type即可
g := group_m.GroupInfo{
Type: group_e.OverseaRoom,
}
db := g.Update(model, gi.ImGroupId, []string{"type"})
if db.Error != nil {
return err
}
}
// 删除旧群
return nil
}
// @Tags 群组
// @Summary 降级直播间为会议模式
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId formData string false "群ID"
// @Success 200
// @Router /v1/imGroup/downgrade [put]
func DowngradeGroup(c *gin.Context) (*mycontext.MyContext, error) {
myContext := mycontext.CreateMyContext(c.Keys)
userId, err := req.GetUserId(c)
if err != nil {
return myContext, err
}
if !user_m.IsSuperUser(userId) {
return myContext, bizerr.NoPrivileges
}
model := domain.CreateModelContext(myContext)
groupId := c.PostForm("groupId")
if len(groupId) <= 0 {
return myContext, bizerr.InvalidParameter
}
gi, err := group_m.GetGroupInfo(model, groupId)
if err != nil {
return myContext, err
}
if gi == nil {
return myContext, bizerr.GroupNotFound
}
if gi.Type == 2 {
model.Log.Infof("DowngradeGroup, no need to downgrade %s", groupId)
} else {
if err = downgradeRoom(myContext, gi); err != nil {
return myContext, err
}
}
resp.ResponseOk(c, nil)
return myContext, nil
}
func downgradeRoom(myContext *mycontext.MyContext, gi *group_m.GroupInfo) error {
// 删除旧直播间
err := tencentyun.DestroyGroup(gi.TxGroupId, false)
if err != nil {
return err
}
// 恢复会议群
groupId, err := tencentyun.CreateGroup(gi.Name, "")
if err != nil {
return err
}
g := group_m.GroupInfo{
ImGroupId: groupId,
TxGroupId: groupId,
Type: group_e.LocalRoom,
Code: gi.Code + "#",
OriginCode: gi.OriginCode,
Owner: gi.Owner,
Name: gi.Name,
Introduction: gi.Introduction,
Notification: gi.Notification,
FaceUrl: gi.FaceUrl,
Country: gi.Country,
ChannelId: gi.ChannelId,
MicOn: gi.MicOn,
LoadHistory: gi.LoadHistory,
MicNumType: gi.MicNumType,
TouristMic: 1,
TouristSendMsg: 1,
TouristSendPic: 1,
}
if err := group_s.NewGroupService(myContext).CreateGroup(g.Owner, &g); err != nil {
// 回滚,删除刚刚建立的TX群组
tencentyun.DestroyGroup(groupId, false)
return err
}
return nil
}
......@@ -6,14 +6,17 @@ import (
"git.hilo.cn/hilo-common/mycontext"
"git.hilo.cn/hilo-common/resource/mysql"
"git.hilo.cn/hilo-common/rpc"
"git.hilo.cn/hilo-common/sdk/tencentyun"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"hilo-group/_const/enum/group_e"
"hilo-group/_const/enum/mgr_e"
"hilo-group/_const/enum/msg_e"
"hilo-group/cv/user_cv"
"hilo-group/domain/cache/group_c"
"hilo-group/domain/model/game_m"
"hilo-group/domain/model/group_m"
"hilo-group/domain/model/mgr_m"
"hilo-group/domain/model/noble_m"
"hilo-group/domain/model/res_m"
"hilo-group/domain/model/user_m"
......@@ -804,3 +807,117 @@ func SetWelcomeText(c *gin.Context) (*mycontext.MyContext, error) {
resp.ResponseOk(c, nil)
return myContext, nil
}
// @Tags 群组
// @Summary 设置群成员上限(obsolete)
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId formData string true "群组ID"
// @Param limit formData int true "成员上限"
// @Success 200
// @Router /v1/imGroup/memberLimit [put]
func SetGroupMemberLimit(c *gin.Context) (*mycontext.MyContext, error) {
myContext := mycontext.CreateMyContext(c.Keys)
groupId := c.PostForm("groupId")
if len(groupId) <= 0 {
return myContext, bizerr.ParaMissing
}
slimit := c.PostForm("limit")
limit, err := strconv.Atoi(slimit)
if err != nil || limit <= 0 {
return myContext, bizerr.InvalidParameter
}
model := domain.CreateModelContext(myContext)
groupInfo, err := group_m.GetInfoByTxGroupId(model, groupId)
if err == nil && groupInfo != nil {
err = tencentyun.SetGroupMaxMemberNum(groupInfo.ImGroupId, uint(limit))
if err != nil {
model.Log.Warn("SetGroupMemberLimit failed for ", groupId)
}
} else {
model.Log.Warn("Skip group ", groupId)
}
resp.ResponseOk(c, nil)
return myContext, nil
}
// @Tags 群组
// @Summary 社区管理员重置群信息
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param externalId formData string true "externalId"
// @Success 200
// @Router /v1/imGroup/info/reset [put]
func ResetGroupInfo(c *gin.Context) (*mycontext.MyContext, error) {
myContext := mycontext.CreateMyContext(c.Keys)
userId, err := req.GetUserId(c)
if err != nil {
return myContext, err
}
externalId := c.PostForm("externalId")
if len(externalId) <= 0 {
return myContext, bizerr.ParaMissing
}
model := domain.CreateModelContext(myContext)
//isMgr, err := user_m.IsCommunityManager(model.Db, userId)
//if err != nil {
// return myContext, err
//}
//if !isMgr {
// return myContext, bizerr.UserGlobalBroadcastManagerNo
//}
globalManager, countryManager, err := group_s.NewGroupService(myContext).CheckAppManager(userId)
if err != nil {
return myContext, err
}
if !globalManager && !countryManager {
return myContext, bizerr.UserGlobalBroadcastManagerNo
}
user, err := user_m.GetUserByExtId(model, externalId)
if err != nil {
return myContext, err
}
if user == nil || user.ID <= 0 {
return myContext, bizerr.ExternalIdNoExist
}
gi, err := group_m.FindGroupByOwner(model, user.ID)
if err != nil {
return myContext, err
}
if len(gi) <= 0 {
return myContext, bizerr.GroupNotFound
}
// 额外判断国家管理员权限
if !globalManager && countryManager {
if isManager, _ := group_s.NewGroupService(myContext).CheckCountryManagerPermission(userId, user.ID); !isManager {
return myContext, bizerr.UserGlobalBroadcastManagerNo
}
}
if err = group_m.ResetGroupInfo(model, gi[0].ImGroupId, gi[0].Code); err != nil {
return myContext, err
}
c.Set(mgr_e.OperationLogKey, mgr_m.MgrOperationLog{
Content: "管理员重置群信息",
TargetUid: user.ID,
OperUid: userId,
})
resp.ResponseOk(c, nil)
return myContext, nil
}
package route
import (
"git.hilo.cn/hilo-common/domain"
"git.hilo.cn/hilo-common/mylogrus"
"github.com/gin-gonic/gin"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
"hilo-group/_const/enum/mgr_e"
_ "hilo-group/docs"
"hilo-group/domain/model/mgr_m"
"hilo-group/route/group_power_r"
"hilo-group/route/group_r"
)
......@@ -64,10 +68,10 @@ func InitRouter() *gin.Engine {
imGroup.POST("/support/award/:groupId", wrapper(group_r.TakeSupportAward))
//
//// 操作类,普通用户不用
//imGroup.PUT("/memberLimit", wrapper(SetGroupMemberLimit))
//imGroup.PUT("/info/reset", wrapper(ResetGroupInfo), OperationLog)
//imGroup.PUT("/upgrade", wrapper(UpgradeGroup))
//imGroup.PUT("/downgrade", wrapper(DowngradeGroup))
imGroup.PUT("/memberLimit", wrapper(group_r.SetGroupMemberLimit))
imGroup.PUT("/info/reset", wrapper(group_r.ResetGroupInfo), OperationLog)
imGroup.PUT("/upgrade", wrapper(group_r.UpgradeGroup))
imGroup.PUT("/downgrade", wrapper(group_r.DowngradeGroup))
//
//imGroup.GET("/mic/all", wrapper(GroupMicAllInfoFive))
//imGroup.GET("/mic/all/type", wrapper(GroupMicAllInfoTen))
......@@ -124,3 +128,24 @@ func InitRouter() *gin.Engine {
}
return r
}
// 操作日志
func OperationLog(c *gin.Context) {
mylogrus.MyLog.Infof("%v-%v", c.Request.Method, c.FullPath())
// 处理请求
c.Next()
model := domain.CreateModelNil()
operConfig := mgr_m.GetOperationModuleUrl(model, c.Request.Method, c.FullPath())
if operConfig.ID > 0 {
// 需要记录操作日志
if data, ok := c.Get(mgr_e.OperationLogKey); ok {
if log, ok := data.(mgr_m.MgrOperationLog); ok {
log.ModuleId = operConfig.ModuleId
log.ModuleUrlId = operConfig.ID
if err := mgr_m.SaveOperationLog(model, log); err != nil {
model.Log.Error("SaveOperationLog fail:%v", err.Error())
}
}
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment