Merge branch 'refs/heads/feature-im-gitea'
This commit is contained in:
commit
b726b2c8f4
53
dao/im.go
53
dao/im.go
|
|
@ -17,6 +17,7 @@ type Message struct {
|
|||
|
||||
type Group struct {
|
||||
gorm.Model
|
||||
AuthID int `gorm:"column:auth_id"`
|
||||
GroupName string `gorm:"column:group_name"`
|
||||
GroupInfo string `gorm:"column:group_info"`
|
||||
GroupType int `gorm:"column:group_type"`
|
||||
|
|
@ -65,6 +66,18 @@ func GetMsgUserByIndex(from_user_id, to_user_id, msg_type, index, status int) ([
|
|||
return msgs, res.Error
|
||||
}
|
||||
|
||||
type GroupMessage struct {
|
||||
Message
|
||||
UserName string `gorm:"column:name" json:"name"`
|
||||
}
|
||||
|
||||
func GetMsgGroupByIndex(group_id, index int) ([]GroupMessage, error) {
|
||||
var msgs []GroupMessage
|
||||
res := DB.Debug().Raw("select messages.*,users.name from messages join users on messages.from_user_id = users.id where type = 2 and group_id = ? order by created_at desc limit ?", group_id, 20*index).Scan(&msgs)
|
||||
return msgs, res.Error
|
||||
|
||||
}
|
||||
|
||||
// 获取邀请消息
|
||||
func GetFriendGroupReq(user_id int) ([]Message, error) {
|
||||
var msgs []Message
|
||||
|
|
@ -125,6 +138,14 @@ func FindMessageByID(id uint) []Message {
|
|||
return msgs
|
||||
}
|
||||
|
||||
// 通过id查找消息(包括name)
|
||||
func FindMessageByID2(id uint) []GroupMessage {
|
||||
var msgs []GroupMessage
|
||||
DB.Debug().Raw("select messages.*,users.name from messages join users on messages.from_user_id = users.id where messages.id = ?", id).Scan(&msgs)
|
||||
return msgs
|
||||
|
||||
}
|
||||
|
||||
// 更新消息状态
|
||||
func UpdateMessageStatus(id uint, status int) error {
|
||||
res := DB.Debug().Model(&Message{}).Where("id = ?", id).Update("status", status)
|
||||
|
|
@ -133,7 +154,7 @@ func UpdateMessageStatus(id uint, status int) error {
|
|||
|
||||
// 创建群聊,需要事务
|
||||
func CreateGroup(groupName, groupInfo, groupType, groupIcon string, user_id int) (error, uint) {
|
||||
group := Group{GroupName: groupName, GroupInfo: groupInfo, GroupType: proto.MSG_TYPE_GROUP, GroupIcon: groupIcon}
|
||||
group := Group{GroupName: groupName, GroupInfo: groupInfo, GroupType: proto.MSG_TYPE_GROUP, GroupIcon: groupIcon, AuthID: user_id}
|
||||
//开启事务
|
||||
tx := DB.Begin()
|
||||
if err := tx.Create(&group); err.Error != nil {
|
||||
|
|
@ -160,10 +181,10 @@ func FindGroupUser(user_id, group_id int) []GroupUser {
|
|||
}
|
||||
|
||||
// 加入群聊
|
||||
func JoinGroup(group_id, user_id int) error {
|
||||
func JoinGroup(group_id, user_id int) (error, uint) {
|
||||
groupUser := GroupUser{GroupID: group_id, UserID: user_id}
|
||||
res := DB.Debug().Create(&groupUser)
|
||||
return res.Error
|
||||
return res.Error, groupUser.ID
|
||||
}
|
||||
|
||||
// 退出群聊
|
||||
|
|
@ -186,16 +207,32 @@ type FriendRet struct {
|
|||
|
||||
func FindFriends(user_id int) []FriendRet {
|
||||
var friends []FriendRet
|
||||
DB.Debug().Raw("select users.id, users.name, users.email from users join friends on users.id = friends.friend_id where friends.user_id = ?", user_id).Scan(&friends)
|
||||
DB.Debug().Raw("select users.id, users.name, users.email from users join friends on users.id = friends.friend_id where friends.user_id = ? and friends.deleted_at is null", user_id).Scan(&friends)
|
||||
return friends
|
||||
}
|
||||
func GetGroups(user_id int) []Group {
|
||||
var groups []Group
|
||||
DB.Debug().Where("auth_id = ?", user_id).Find(&groups)
|
||||
return groups
|
||||
}
|
||||
|
||||
func FindGroups(user_id int) []Group {
|
||||
var groups []Group
|
||||
DB.Debug().Raw("select groups.* from groups join group_users on groups.id = group_users.group_id where group_users.user_id = ?", user_id).Scan(&groups)
|
||||
DB.Debug().Raw("select groups.* from groups join group_users on groups.id = group_users.group_id where group_users.user_id = ? and group_users.deleted_at is null", user_id).Scan(&groups)
|
||||
return groups
|
||||
}
|
||||
|
||||
func FindGroupByID(group_id int) []Group {
|
||||
var groups []Group
|
||||
DB.Debug().Where("id = ?", group_id).Find(&groups)
|
||||
return groups
|
||||
}
|
||||
|
||||
func UpdateGroup(group_id int, groupName, groupInfo, groupIcon string) error {
|
||||
res := DB.Debug().Model(&Group{}).Where("id = ?", group_id).Updates(map[string]interface{}{"group_name": groupName, "group_info": groupInfo, "group_icon": groupIcon})
|
||||
return res.Error
|
||||
}
|
||||
|
||||
type FriendRequest struct {
|
||||
ID int `json:"id"`
|
||||
IMID int `json:"im_id"`
|
||||
|
|
@ -209,3 +246,9 @@ func GetFriendRequest(user_id int) []FriendRequest {
|
|||
DB.Debug().Raw("select users.id,users.name,users.email,users.age,messages.id as im_id from users join messages on users.id = messages.from_user_id where messages.to_user_id = ? and messages.type = ? and status = ?", user_id, proto.MSG_TYPE_FRIEND, 0).Scan(&users)
|
||||
return users
|
||||
}
|
||||
|
||||
func FindGroupUsers(group_id int) []GroupUser {
|
||||
var groupUsers []GroupUser
|
||||
DB.Debug().Where("group_id = ?", group_id).Find(&groupUsers)
|
||||
return groupUsers
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@ func FindUserByID(id int) []proto.User {
|
|||
DB.Debug().Where("id = ?", id).First(&users)
|
||||
return users
|
||||
}
|
||||
func FindUserByID2(id int) User {
|
||||
var user User
|
||||
DB.Debug().Where("id = ?", id).First(&user)
|
||||
return user
|
||||
}
|
||||
|
||||
func FindUserByUserID(id int) User {
|
||||
var user User
|
||||
|
|
@ -56,7 +61,7 @@ func FindUserByName(name string) User {
|
|||
// 根据name模糊查询,邮箱也是,不查询密码
|
||||
func FindUserByNameLike(name string) []proto.User {
|
||||
var users []proto.User
|
||||
DB.Debug().Where("name LIKE ? OR email LIKE ?", "%"+name+"%", "%"+name+"%").Find(&users)
|
||||
DB.Debug().Where("name LIKE ? OR email LIKE ?", "%"+name+"%", "%"+name+"%").Find(&users).Limit(32)
|
||||
return users
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
type SMessage struct {
|
||||
To_user_id int `json:"to_user_id" form:"to_user_id"`
|
||||
Type int `json:"type" form:"type"`
|
||||
GroupID int `json:"group_id" form:"group_id"`
|
||||
Msg string `json:"msg" form:"msg"`
|
||||
}
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ type Message struct {
|
|||
ID int `json:"id" form:"id" `
|
||||
To_user_id int `json:"to_user_id" form:"to_user_id" `
|
||||
From_user_id int `json:"from_user_id" form:"from_user_id" `
|
||||
GroupID int `json:"group_id" form:"group_id"`
|
||||
Index int `json:"index" form:"index" `
|
||||
Type int `json:"type" form:"type" `
|
||||
}
|
||||
|
|
@ -65,11 +67,13 @@ func SetUpIMGroup(router *gin.Engine) {
|
|||
//接受邀请,确认好友关系
|
||||
imGroup.POST("/accept_invite", AcceptInvite)
|
||||
imGroup.POST("/create_group", CreateGroup)
|
||||
imGroup.POST("/get_group", GetGroups)
|
||||
imGroup.GET("/sse_msg", ServerSendMsg)
|
||||
imGroup.GET("/ws_v2", ServerSsendMsgV2)
|
||||
imGroup.POST("/get_friend_list", GetFriendList) //获取好友列表,包括群聊
|
||||
//获取好友请求
|
||||
imGroup.POST("/get_friend_request", GetFriendRequest)
|
||||
imGroup.POST("/del_friend_or_group", DelFriendOrGroup)
|
||||
}
|
||||
func generateRandomHexString(length int) (string, error) {
|
||||
bytes := make([]byte, length/2) // 16字节的字符串需要32个十六进制字符,即16个字节
|
||||
|
|
@ -79,6 +83,49 @@ func generateRandomHexString(length int) (string, error) {
|
|||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
func GetGroups(c *gin.Context) {
|
||||
id, _ := c.Get("id")
|
||||
user_id := int(id.(float64))
|
||||
data := service.GetGroups(user_id)
|
||||
c.JSON(http.StatusOK, gin.H{"code": proto.SuccessCode, "data": data, "message": "success"})
|
||||
|
||||
}
|
||||
|
||||
func DelFriendOrGroup(c *gin.Context) {
|
||||
var req Message
|
||||
user_id, _ := c.Get("id")
|
||||
cid := int(user_id.(float64))
|
||||
if err := c.ShouldBind(&req); err == nil {
|
||||
if req.Type == 1 {
|
||||
if req.To_user_id == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"error": "parameter error", "code": proto.ParameterError, "message": "failed"})
|
||||
return
|
||||
}
|
||||
err2 := service.DelFriendService(req.To_user_id, cid)
|
||||
if err2 == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": proto.SuccessCode, "message": "success"})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"error": err2.Error(), "code": proto.OperationFailed, "message": "failed"})
|
||||
}
|
||||
} else if req.Type == 2 {
|
||||
if req.GroupID == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"error": "parameter error", "code": proto.ParameterError, "message": "failed"})
|
||||
return
|
||||
}
|
||||
err2 := service.QuitGroupService(cid, req.GroupID)
|
||||
if err2 == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": proto.SuccessCode, "message": "success"})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"error": err2.Error(), "code": proto.OperationFailed, "message": "failed"})
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"error": "parameter error", "code": proto.ParameterError, "message": "failed"})
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"error": err.Error(), "code": proto.ParameterError, "message": "failed"})
|
||||
}
|
||||
}
|
||||
|
||||
func GetFriendList(c *gin.Context) {
|
||||
id, _ := c.Get("id")
|
||||
user_id := int(id.(float64))
|
||||
|
|
@ -91,13 +138,24 @@ func GetMessage(c *gin.Context) {
|
|||
id := int(user_id.(float64))
|
||||
//解析参数
|
||||
if err := c.ShouldBind(&req); err == nil {
|
||||
fmt.Println(req)
|
||||
msgs, err2 := service.GetMsgUserByIndexService(req.From_user_id, id, req.Index, req.Type, req.From_user_id)
|
||||
|
||||
if req.Type == 2 {
|
||||
msgs, err2 := dao.GetMsgGroupByIndex(req.GroupID, req.Index)
|
||||
if err2 == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": proto.SuccessCode, "data": msgs, "message": "success"})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"error": err2.Error(), "code": proto.OperationFailed, "message": "failed"})
|
||||
}
|
||||
|
||||
} else {
|
||||
msgs, err2 := service.GetMsgUserByIndexService(req.From_user_id, id, req.Index, req.Type, req.From_user_id, req.GroupID)
|
||||
if err2 == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": proto.SuccessCode, "data": msgs, "message": "success"})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"error": err2.Error(), "code": proto.OperationFailed, "message": "failed"})
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"error": err.Error(), "code": proto.ParameterError, "message": "failed"})
|
||||
}
|
||||
|
|
@ -149,7 +207,7 @@ func SendMessage(c *gin.Context) {
|
|||
if err := c.ShouldBind(&req); err == nil {
|
||||
var err2 error
|
||||
var mid uint
|
||||
err2, mid = service.CreateGeneralMessageService(id, req.To_user_id, req.Type, req.Msg)
|
||||
err2, mid = service.CreateGeneralMessageService(id, req.To_user_id, req.Type, req.GroupID, req.Msg)
|
||||
if err2 == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": proto.SuccessCode, "message": "success", "data": mid})
|
||||
} else {
|
||||
|
|
@ -372,7 +430,7 @@ func ServerSsendMsgV2(c *gin.Context) {
|
|||
msg_id := worker.PopRedisListLeft(key)
|
||||
if msg_id != "" {
|
||||
msg_id_num, _ := strconv.ParseInt(msg_id, 10, 64)
|
||||
msgs := dao.FindMessageByID(uint(msg_id_num))
|
||||
msgs := dao.FindMessageByID2(uint(msg_id_num))
|
||||
if len(msgs) > 0 {
|
||||
msg := msgs[0]
|
||||
//发送消息
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/golang-jwt/jwt"
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
"videoplayer/dao"
|
||||
"videoplayer/proto"
|
||||
"videoplayer/service"
|
||||
"videoplayer/worker"
|
||||
|
|
@ -25,6 +26,7 @@ func SetUpUserGroup(router *gin.Engine) {
|
|||
userGroup.POST("/sqr", SetQRStatus)
|
||||
userGroup.POST("/confirm", ConfirmQRLogin)
|
||||
userGroup.POST("/search", SearchHandler)
|
||||
userGroup.POST("/info", GetUserInfo)
|
||||
}
|
||||
|
||||
type RLReq struct {
|
||||
|
|
@ -46,6 +48,14 @@ type SearchReq struct {
|
|||
ID int `json:"id" form:"id"`
|
||||
}
|
||||
|
||||
func GetUserInfo(c *gin.Context) {
|
||||
id, _ := c.Get("id")
|
||||
user_id := int(id.(float64))
|
||||
user := dao.FindUserByID2(user_id)
|
||||
user.Password = "" //不返回密码
|
||||
c.JSON(200, gin.H{"code": proto.SuccessCode, "message": "success", "data": user})
|
||||
}
|
||||
|
||||
func GetScanUUID(c *gin.Context) {
|
||||
var ReqData QRReq
|
||||
if err := c.ShouldBind(&ReqData); err != nil {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const (
|
|||
MSG_TYPE_SYSTEM = 3 // 系统消息
|
||||
MSG_TYPE_FRIEND = 4 // 好友请求
|
||||
MSG_TYPE_GROUP_ADD = 5 // 加入群聊请求
|
||||
MSG_TYPE_GROUP_INVI = 6 // 邀请加入群聊
|
||||
|
||||
// 以下是消息状态
|
||||
MSG_STATUS_READ = 1 // 已读
|
||||
|
|
|
|||
|
|
@ -5,19 +5,20 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
"videoplayer/dao"
|
||||
"videoplayer/proto"
|
||||
"videoplayer/worker"
|
||||
)
|
||||
|
||||
func CreateGeneralMessageService(from_id, to_id, msg_type int, content string) (error, uint) {
|
||||
func CreateGeneralMessageService(from_id, to_id, msg_type, group_id int, content string) (error, uint) {
|
||||
// 业务逻辑
|
||||
var err error
|
||||
var id uint
|
||||
switch msg_type {
|
||||
case 1:
|
||||
case proto.MSG_TYPE_SIMPLE:
|
||||
//判断是否是好友
|
||||
friend := dao.FindFriend(from_id, to_id)
|
||||
if len(friend) == 0 {
|
||||
return errors.New("not a friend"), 0
|
||||
return errors.New("未添加好友"), 0
|
||||
}
|
||||
err, id = dao.CreateSimpleMessage(from_id, to_id, content)
|
||||
res := worker.GetRedis("user_" + strconv.Itoa(to_id) + "_status_v2")
|
||||
|
|
@ -25,27 +26,60 @@ func CreateGeneralMessageService(from_id, to_id, msg_type int, content string) (
|
|||
//在线,存入redis
|
||||
worker.PushRedisListWithExpire("user_"+strconv.Itoa(to_id)+"_msg_ids", strconv.Itoa(int(id)), time.Second*300)
|
||||
}
|
||||
case 2:
|
||||
err, id = dao.CreateGeneralMessage(from_id, to_id, msg_type, 0, 0, content)
|
||||
case proto.MSG_TYPE_GROUP:
|
||||
if from_id == 0 || group_id == 0 || content == "" {
|
||||
return errors.New("参数错误"), 0
|
||||
}
|
||||
//判断该用户是否在群里
|
||||
groupUser := dao.FindGroupUser(from_id, group_id)
|
||||
if len(groupUser) == 0 {
|
||||
return errors.New("用户不在群里"), 0
|
||||
}
|
||||
err, id = dao.CreateGeneralMessage(from_id, to_id, msg_type, 0, group_id, content)
|
||||
//获取群里的用户
|
||||
users := dao.FindGroupUsers(group_id)
|
||||
for _, user := range users {
|
||||
if user.UserID == from_id {
|
||||
continue
|
||||
}
|
||||
res := worker.GetRedis("user_" + strconv.Itoa(user.UserID) + "_status_v2")
|
||||
if res == "1" {
|
||||
//在线,存入redis
|
||||
worker.PushRedisListWithExpire("user_"+strconv.Itoa(user.UserID)+"_msg_ids", strconv.Itoa(int(id)), time.Second*300)
|
||||
}
|
||||
}
|
||||
case 3:
|
||||
//user := dao.FindUserByID(to_id)
|
||||
// 系统消息,需要管理员权限
|
||||
err, id = dao.CreateGeneralMessage(from_id, to_id, msg_type, 0, 0, content)
|
||||
case 4:
|
||||
err, id = dao.CreateGeneralMessage(from_id, to_id, msg_type, 0, group_id, content)
|
||||
case proto.MSG_TYPE_FRIEND:
|
||||
res1 := dao.FindFriend(from_id, to_id)
|
||||
if len(res1) > 0 {
|
||||
// 已经有会话记录
|
||||
return errors.New("已是好友关系"), 0
|
||||
}
|
||||
res, _ := dao.GetMsgUserByIndex(from_id, to_id, 4, 1, 0)
|
||||
if len(res) > 0 {
|
||||
// 已经有会话记录,返回会话id
|
||||
return nil, res[0].ID
|
||||
return errors.New("已有请求"), res[0].ID
|
||||
}
|
||||
err, id = dao.CreateGeneralMessage(from_id, to_id, msg_type, 0, 0, content)
|
||||
case 5:
|
||||
res, _ := dao.GetMsgUserByIndex(from_id, to_id, 5, 1, 0)
|
||||
if len(res) > 0 {
|
||||
// 已经有会话记录
|
||||
return errors.New("already have a conversation"), 0
|
||||
err, id = dao.CreateGeneralMessage(from_id, to_id, msg_type, 0, group_id, content)
|
||||
case proto.MSG_TYPE_GROUP_ADD:
|
||||
//加入群聊请求
|
||||
|
||||
case proto.MSG_TYPE_GROUP_INVI:
|
||||
//邀请加群,直接加入
|
||||
//判断邀请人是否在群里
|
||||
groupUser := dao.FindGroupUser(from_id, group_id)
|
||||
if len(groupUser) == 0 {
|
||||
return errors.New("邀请人不在群里"), 0
|
||||
}
|
||||
//邀请加入群聊请求
|
||||
err, id = dao.CreateGeneralMessage(from_id, to_id, msg_type, 0, from_id, content)
|
||||
//判断被邀请人是否在群里
|
||||
groupUser = dao.FindGroupUser(to_id, group_id)
|
||||
if len(groupUser) > 0 {
|
||||
return errors.New("已在群里"), 0
|
||||
}
|
||||
err, id = dao.JoinGroup(group_id, to_id)
|
||||
default:
|
||||
// 未知消息类型
|
||||
err = errors.New("unknown message type")
|
||||
|
|
@ -53,15 +87,19 @@ func CreateGeneralMessageService(from_id, to_id, msg_type int, content string) (
|
|||
return err, id
|
||||
}
|
||||
|
||||
func GetMsgUserByIndexService(from_id, to_id, index, msq_type, from_user_id int) ([]dao.Message, error) {
|
||||
func GetMsgUserByIndexService(from_id, to_id, index, msq_type, from_user_id, group_id int) ([]dao.Message, error) {
|
||||
// 业务逻辑
|
||||
var msgs []dao.Message
|
||||
var err error
|
||||
if index <= 0 || index > 100 {
|
||||
return nil, errors.New("index out of range")
|
||||
}
|
||||
if msq_type == 4 {
|
||||
if msq_type == proto.MSG_TYPE_FRIEND {
|
||||
from_id = from_user_id
|
||||
}
|
||||
msgs, err := dao.GetMsgUserByIndex(from_id, to_id, msq_type, index, 0)
|
||||
msgs, err = dao.GetMsgUserByIndex(from_id, to_id, msq_type, index, 0)
|
||||
//群聊消息
|
||||
|
||||
return msgs, err
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +124,7 @@ func AddFriendService(id, from_user_id, to_user_id int) error {
|
|||
if len(groupUser) > 0 {
|
||||
return errors.New("already in the group")
|
||||
}
|
||||
err := dao.JoinGroup(groupUser[0].GroupID, to_user_id)
|
||||
err, _ := dao.JoinGroup(groupUser[0].GroupID, to_user_id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -129,3 +167,21 @@ func GetFriendRequest(user_id int) []dao.FriendRequest {
|
|||
users := dao.GetFriendRequest(user_id)
|
||||
return users
|
||||
}
|
||||
|
||||
func DelFriendService(user_id, friend_id int) error {
|
||||
//删除好友
|
||||
err := dao.DeleteFriend(user_id, friend_id)
|
||||
return err
|
||||
}
|
||||
|
||||
func QuitGroupService(user_id, group_id int) error {
|
||||
//退出群聊
|
||||
err := dao.QuitGroup(group_id, user_id)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetGroups(user_id int) []dao.Group {
|
||||
//获取群聊
|
||||
groups := dao.GetGroups(user_id)
|
||||
return groups
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ func InitRedis() error {
|
|||
// 连接redis
|
||||
redisClient = redis.NewClient(&redis.Options{
|
||||
Addr: proto.REDIS_ADDR, // Redis 服务器地址
|
||||
Password: proto.REDIS_PASSWORD, // 如果 Redis 设置了密码
|
||||
//Password: proto.REDIS_PASSWORD, // 如果 Redis 设置了密码
|
||||
DB: proto.REIDS_DB, // 使用的数据库编号
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue