Channel join/part/invite through REST

This commit is contained in:
Denis Arh
2018-10-07 20:22:34 +02:00
parent 910b5589dd
commit 0f9f2e08f9
9 changed files with 259 additions and 117 deletions
@@ -61,7 +61,7 @@ CREATE TABLE channel_members (
rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),
rel_user BIGINT UNSIGNED NOT NULL,
type ENUM ('owner', 'member') NOT NULL DEFAULT 'member',
type ENUM ('owner', 'member', 'invitee') NOT NULL DEFAULT 'member',
created_at DATETIME NOT NULL DEFAULT NOW(),
updated_at DATETIME NULL,
+2 -2
View File
@@ -313,7 +313,7 @@ A channel is a representation of a sequence of messages. It has meta data like c
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/members/{userID}/join` | HTTP/S | POST | Client ID, Session ID |
| `/channels/{channelID}/members/{userID}` | HTTP/S | PUT | Client ID, Session ID |
#### Request parameters
@@ -328,7 +328,7 @@ A channel is a representation of a sequence of messages. It has meta data like c
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/members/{userID}/part` | HTTP/S | DELETE | Client ID, Session ID |
| `/channels/{channelID}/members/{userID}` | HTTP/S | DELETE | Client ID, Session ID |
#### Request parameters
+3 -3
View File
@@ -270,8 +270,8 @@
},
{
"name": "join",
"method": "POST",
"path": "/{channelID}/members/{userID}/join",
"method": "PUT",
"path": "/{channelID}/members/{userID}",
"title": "Join channel",
"parameters": {
"path": [
@@ -283,7 +283,7 @@
{
"name": "part",
"method": "DELETE",
"path": "/{channelID}/members/{userID}/part",
"path": "/{channelID}/members/{userID}",
"title": "Remove member from channel",
"parameters": {
"path": [
+3 -3
View File
@@ -142,9 +142,9 @@
},
{
"Name": "join",
"Method": "POST",
"Method": "PUT",
"Title": "Join channel",
"Path": "/{channelID}/members/{userID}/join",
"Path": "/{channelID}/members/{userID}",
"Parameters": {
"path": [
{
@@ -166,7 +166,7 @@
"Name": "part",
"Method": "DELETE",
"Title": "Remove member from channel",
"Path": "/{channelID}/members/{userID}/part",
"Path": "/{channelID}/members/{userID}",
"Parameters": {
"path": [
{
+6 -8
View File
@@ -4,7 +4,6 @@ import (
"context"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/titpetric/factory"
"github.com/crusttech/crust/sam/types"
@@ -18,7 +17,8 @@ type (
Find(filter *types.ChannelMemberFilter) (types.ChannelMemberSet, error)
Create(mod *types.ChannelMember) (*types.ChannelMember, error)
Delete(channelMemberID, userID uint64) error
Update(mod *types.ChannelMember) (*types.ChannelMember, error)
Delete(channelID, userID uint64) error
}
channelMember struct {
@@ -85,8 +85,6 @@ func (r *channelMember) Find(filter *types.ChannelMemberFilter) (types.ChannelMe
}
}
spew.Dump(filter, sql, params)
return mm, r.db().Select(&mm, sql, params...)
}
@@ -102,13 +100,13 @@ func (r *channelMember) Create(mod *types.ChannelMember) (*types.ChannelMember,
func (r *channelMember) Update(mod *types.ChannelMember) (*types.ChannelMember, error) {
mod.UpdatedAt = timeNowPtr()
whitelist := []string{"type", "updated_at"}
whitelist := []string{"type", "updated_at", "rel_channel", "rel_user"}
return mod, r.db().UpdatePartial("channel_members", mod, whitelist, "rel_channel", "rel_user")
}
// Delete removes existing channel membership record
func (r *channelMember) Delete(channelMemberID, userID uint64) error {
sql := `DELETE FROM channel_members WHERE rel_channelMember = ? AND rel_user = ?`
return exec(r.db().Exec(sql, channelMemberID, userID))
func (r *channelMember) Delete(channelID, userID uint64) error {
sql := `DELETE FROM channel_members WHERE rel_channel = ? AND rel_user = ?`
return exec(r.db().Exec(sql, channelID, userID))
}
+14 -6
View File
@@ -66,16 +66,16 @@ func (ctrl *Channel) Members(ctx context.Context, r *request.ChannelMembers) (in
return ctrl.wrapMemberSet(ctrl.svc.ch.With(ctx).FindMembers(r.ChannelID))
}
func (ctrl *Channel) Invite(ctx context.Context, r *request.ChannelInvite) (interface{}, error) {
return ctrl.wrapMemberSet(ctrl.svc.ch.InviteUser(r.ChannelID, r.UserID...))
}
func (ctrl *Channel) Join(ctx context.Context, r *request.ChannelJoin) (interface{}, error) {
return nil, nil
return ctrl.wrapMemberSet(ctrl.svc.ch.AddMember(r.ChannelID, r.UserID))
}
func (ctrl *Channel) Part(ctx context.Context, r *request.ChannelPart) (interface{}, error) {
return nil, nil
}
func (ctrl *Channel) Invite(ctx context.Context, r *request.ChannelInvite) (interface{}, error) {
return nil, nil
return nil, ctrl.svc.ch.DeleteMember(r.ChannelID, r.UserID)
}
func (ctrl *Channel) Attach(ctx context.Context, r *request.ChannelAttach) (interface{}, error) {
@@ -117,6 +117,14 @@ func (ctrl *Channel) wrapSet(cc types.ChannelSet, err error) (*outgoing.ChannelS
}
}
func (ctrl *Channel) wrapMember(m *types.ChannelMember, err error) (*outgoing.ChannelMember, error) {
if err != nil {
return nil, err
} else {
return payload.ChannelMember(m), nil
}
}
func (ctrl *Channel) wrapMemberSet(mm types.ChannelMemberSet, err error) (*outgoing.ChannelMemberSet, error) {
if err != nil {
return nil, err
+2 -2
View File
@@ -138,8 +138,8 @@ func (ch *Channel) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h
r.Get("/{channelID}", ch.Read)
r.Delete("/{channelID}", ch.Delete)
r.Get("/{channelID}/members", ch.Members)
r.Post("/{channelID}/members/{userID}/join", ch.Join)
r.Delete("/{channelID}/members/{userID}/part", ch.Part)
r.Put("/{channelID}/members/{userID}", ch.Join)
r.Delete("/{channelID}/members/{userID}", ch.Part)
r.Post("/{channelID}/invite", ch.Invite)
r.Post("/{channelID}/attach", ch.Attach)
})
+218 -92
View File
@@ -25,6 +25,8 @@ type (
channel repository.ChannelRepository
cmember repository.ChannelMemberRepository
message repository.MessageRepository
sysmsgs types.MessageSet
}
ChannelService interface {
@@ -32,12 +34,17 @@ type (
FindByID(channelID uint64) (*types.Channel, error)
Find(filter *types.ChannelFilter) (types.ChannelSet, error)
FindByMembership() (rval []*types.Channel, err error)
FindMembers(channelID uint64) (types.ChannelMemberSet, error)
Create(channel *types.Channel) (*types.Channel, error)
Update(channel *types.Channel) (*types.Channel, error)
FindByMembership() (rval []*types.Channel, err error)
FindMembers(channelID uint64) (types.ChannelMemberSet, error)
InviteUser(channelID uint64, memberIDs ...uint64) (out types.ChannelMemberSet, err error)
AddMember(channelID uint64, memberIDs ...uint64) (out types.ChannelMemberSet, err error)
DeleteMember(channelID uint64, memberIDs ...uint64) (err error)
Archive(ID uint64) error
Unarchive(ID uint64) error
Delete(ID uint64) error
@@ -67,6 +74,9 @@ func (svc *channel) With(ctx context.Context) ChannelService {
channel: repository.Channel(ctx, db),
cmember: repository.ChannelMember(ctx, db),
message: repository.Message(ctx, db),
// System messages should be flushed at the end of each session
sysmsgs: types.MessageSet{},
}
}
@@ -219,12 +229,12 @@ func (svc *channel) Create(in *types.Channel) (out *types.Channel, err error) {
// Create the first message, doing this directly with repository to circumvent
// message service constraints
msg, err = svc.message.CreateMessage(svc.makeSystemMessage(
svc.scheduleSystemMessage(
out,
"@%d created new %s channel, topic is: %s",
chCreatorID,
"<PRIVATE-OR-PUBLIC>",
"<TOPIC>"))
"<TOPIC>")
_ = msg
if err != nil {
@@ -232,18 +242,16 @@ func (svc *channel) Create(in *types.Channel) (out *types.Channel, err error) {
return
}
svc.sendMessageEvent(msg)
svc.flushSystemMessages()
return svc.evl.Channel(out)
})
}
func (svc *channel) Update(in *types.Channel) (out *types.Channel, err error) {
func (svc *channel) Update(ch *types.Channel) (out *types.Channel, err error) {
return out, svc.db.Transaction(func() (err error) {
var msgs types.MessageSet
// @todo [SECURITY] can user access this channel?
if out, err = svc.channel.FindChannelByID(in.ID); err != nil {
if out, err = svc.channel.FindChannelByID(ch.ID); err != nil {
return
}
@@ -253,19 +261,19 @@ func (svc *channel) Update(in *types.Channel) (out *types.Channel, err error) {
return errors.New("Not allowed to edit deleted channels")
}
if out.Type != in.Type {
if out.Type != ch.Type {
// @todo [SECURITY] check if user can create public channels
if in.Type == types.ChannelTypePublic && false {
if ch.Type == types.ChannelTypePublic && false {
return errors.New("Not allowed to change type of this channel to public")
}
// @todo [SECURITY] check if user can create private channels
if in.Type == types.ChannelTypePrivate && false {
if ch.Type == types.ChannelTypePrivate && false {
return errors.New("Not allowed to change type of this channel to private")
}
// @todo [SECURITY] check if user can create group channels
if in.Type == types.ChannelTypeGroup && false {
if ch.Type == types.ChannelTypeGroup && false {
return errors.New("Not allowed to change type of this channel to group")
}
}
@@ -273,48 +281,33 @@ func (svc *channel) Update(in *types.Channel) (out *types.Channel, err error) {
var chUpdatorId = repository.Identity(svc.ctx)
// Copy values
if out.Name != in.Name {
if out.Name != ch.Name {
// @todo [SECURITY] can we change channel's name?
if false {
return errors.New("Not allowed to rename channel")
} else {
msgs = append(msgs, svc.makeSystemMessage(
out, "@%d renamed channel %s (was: %s)", chUpdatorId, out.Name, in.Name))
svc.scheduleSystemMessage(ch, "@%d renamed channel %s (was: %s)", chUpdatorId, out.Name, ch.Name)
}
out.Name = in.Name
out.Name = ch.Name
}
if out.Topic != in.Topic && true {
if out.Topic != ch.Topic && true {
// @todo [SECURITY] can we change channel's topic?
if false {
return errors.New("Not allowed to change channel topic")
} else {
msgs = append(msgs, svc.makeSystemMessage(
out, "@%d changed channel topic: %s (was: %s)", chUpdatorId, out.Topic, in.Topic))
svc.scheduleSystemMessage(ch, "@%d changed channel topic: %s (was: %s)", chUpdatorId, out.Topic, ch.Topic)
}
out.Topic = in.Topic
out.Topic = ch.Topic
}
// Save the updated channel
if out, err = svc.channel.UpdateChannel(in); err != nil {
if out, err = svc.channel.UpdateChannel(ch); err != nil {
return
}
// Create the first message, doing this directly with repository to circumvent
// message service constraints
for _, msg := range msgs {
if msg, err = svc.message.CreateMessage(msg); err != nil {
return err
}
svc.sendMessageEvent(msg)
}
if err != nil {
// Message creation failed
return
}
svc.flushSystemMessages()
return svc.evl.Channel(out)
})
@@ -336,16 +329,13 @@ func (svc *channel) Delete(id uint64) error {
return errors.New("Channel already deleted")
}
msg, err := svc.message.CreateMessage(svc.makeSystemMessage(ch, "@%d deleted this channel", userID))
if err != nil {
return
}
svc.sendMessageEvent(msg)
svc.scheduleSystemMessage(ch, "@%d deleted this channel", userID)
if err = svc.channel.DeleteChannelByID(id); err != nil {
return
}
svc.flushSystemMessages()
return svc.evl.Channel(ch)
})
}
@@ -366,24 +356,20 @@ func (svc *channel) Recover(id uint64) error {
return errors.New("Channel not deleted")
}
msg, err := svc.message.CreateMessage(svc.makeSystemMessage(ch, "@%d recovered this channel", userID))
if err != nil {
return
}
svc.sendMessageEvent(msg)
svc.scheduleSystemMessage(ch, "@%d recovered this channel", userID)
err = svc.channel.UnarchiveChannelByID(id)
if err != nil {
if err = svc.channel.UnarchiveChannelByID(id); err != nil {
return
}
svc.flushSystemMessages()
return svc.evl.Channel(ch)
})
}
func (svc *channel) Archive(id uint64) error {
return svc.db.Transaction(func() (err error) {
var userID = repository.Identity(svc.ctx)
var ch *types.Channel
@@ -398,17 +384,13 @@ func (svc *channel) Archive(id uint64) error {
return errors.New("Channel already archived")
}
msg, err := svc.message.CreateMessage(svc.makeSystemMessage(ch, "@%d archived this channel", userID))
if err != nil {
return
}
svc.sendMessageEvent(msg)
svc.scheduleSystemMessage(ch, "@%d archived this channel", userID)
err = svc.channel.ArchiveChannelByID(id)
if err != nil {
if err = svc.channel.ArchiveChannelByID(id); err != nil {
return
}
svc.flushSystemMessages()
return svc.evl.Channel(ch)
})
}
@@ -429,64 +411,208 @@ func (svc *channel) Unarchive(id uint64) error {
return errors.New("Channel not archived")
}
msg, err := svc.message.CreateMessage(svc.makeSystemMessage(ch, "@%d unarchived this channel", userID))
if err != nil {
return
}
svc.sendMessageEvent(msg)
err = svc.channel.ArchiveChannelByID(id)
if err != nil {
return
}
svc.scheduleSystemMessage(ch, "@%d unarchived this channel", userID)
svc.flushSystemMessages()
return svc.evl.Channel(ch)
})
}
func (svc *channel) AddMember(m *types.ChannelMember) (out *types.ChannelMember, err error) {
func (svc *channel) InviteUser(channelID uint64, memberIDs ...uint64) (out types.ChannelMemberSet, err error) {
var (
userID = repository.Identity(svc.ctx)
ch *types.Channel
existing types.ChannelMemberSet
)
out = types.ChannelMemberSet{}
// @todo [SECURITY] can user access this channel?
if ch, err = svc.channel.FindChannelByID(channelID); err != nil {
return
}
// @todo [SECURITY] can user add members to this channel?
return out, svc.db.Transaction(func() (err error) {
var userID = repository.Identity(svc.ctx)
var ch *types.Channel
// @todo [SECURITY] can user access this channel?
if ch, err = svc.channel.FindChannelByID(m.ChannelID); err != nil {
if existing, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: channelID}); err != nil {
return
}
// @todo [SECURITY] can user add members to this channel?
msg, err := svc.message.CreateMessage(svc.makeSystemMessage(ch, "@%d added a new member to this channel: @%d", userID, m.UserID))
users, err := svc.usr.Find(nil)
if err != nil {
return
return err
}
svc.sendMessageEvent(msg)
return err
for _, memberID := range memberIDs {
user := users.FindById(memberID)
if user == nil {
return errors.New("Unexisting user")
}
if e := existing.FindByUserId(memberID); e != nil {
// Already a member/invited
e.User = user
out = append(out, e)
continue
}
svc.scheduleSystemMessage(ch, "@%d invited @%d to the channel", userID, memberID)
member := &types.ChannelMember{
ChannelID: channelID,
UserID: memberID,
Type: types.ChannelMembershipTypeInvitee,
}
if member, err = svc.cmember.Create(member); err != nil {
return err
}
out = append(out, member)
}
return svc.flushSystemMessages()
})
}
func (svc *channel) makeSystemMessage(ch *types.Channel, format string, a ...interface{}) *types.Message {
return &types.Message{
func (svc *channel) AddMember(channelID uint64, memberIDs ...uint64) (out types.ChannelMemberSet, err error) {
var (
userID = repository.Identity(svc.ctx)
ch *types.Channel
existing types.ChannelMemberSet
)
out = types.ChannelMemberSet{}
// @todo [SECURITY] can user access this channel?
if ch, err = svc.channel.FindChannelByID(channelID); err != nil {
return
}
// @todo [SECURITY] can user add members to this channel?
return out, svc.db.Transaction(func() (err error) {
if existing, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: channelID}); err != nil {
return
}
users, err := svc.usr.Find(nil)
if err != nil {
return err
}
for _, memberID := range memberIDs {
var exists bool
user := users.FindById(memberID)
if user == nil {
return errors.New("Unexisting user")
}
if e := existing.FindByUserId(memberID); e != nil {
if e.Type != types.ChannelMembershipTypeInvitee {
e.User = user
out = append(out, e)
continue
} else {
exists = true
}
}
if !exists {
if userID == memberID {
svc.scheduleSystemMessage(ch, "@%d joined", memberID)
} else {
svc.scheduleSystemMessage(ch, "@%d added @%d to the channel", userID, memberID)
}
}
member := &types.ChannelMember{
ChannelID: channelID,
UserID: memberID,
Type: types.ChannelMembershipTypeOwner,
}
if exists {
member, err = svc.cmember.Update(member)
} else {
member, err = svc.cmember.Create(member)
}
if err != nil {
return err
}
out = append(out, member)
}
return svc.flushSystemMessages()
})
}
func (svc *channel) DeleteMember(channelID uint64, memberIDs ...uint64) (err error) {
var (
userID = repository.Identity(svc.ctx)
ch *types.Channel
existing types.ChannelMemberSet
)
// @todo [SECURITY] can user access this channel?
if ch, err = svc.channel.FindChannelByID(channelID); err != nil {
return
}
// @todo [SECURITY] can user remove members from this channel?
return svc.db.Transaction(func() (err error) {
if existing, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: channelID}); err != nil {
return
}
for _, memberID := range memberIDs {
if existing.FindByUserId(memberID) == nil {
// Not really a member...
continue
}
if userID == memberID {
svc.scheduleSystemMessage(ch, "@%d parted", memberID)
} else {
svc.scheduleSystemMessage(ch, "@%d kicked @%d out", userID, memberID)
}
if err = svc.cmember.Delete(channelID, memberID); err != nil {
return err
}
}
return svc.flushSystemMessages()
})
}
func (svc *channel) scheduleSystemMessage(ch *types.Channel, format string, a ...interface{}) {
svc.sysmsgs = append(svc.sysmsgs, &types.Message{
ChannelID: ch.ID,
Message: fmt.Sprintf(format, a...),
Type: types.MessageTypeChannelEvent,
}
})
}
// Sends message to event loop
//
// It also preloads user
func (svc *channel) sendMessageEvent(msg *types.Message) (err error) {
if msg.User == nil {
// @todo pull user from cache
if msg.User, err = svc.usr.FindByID(msg.UserID); err != nil {
return
// Flushes sys message stack, stores them into repo & pushes them into event loop
func (svc *channel) flushSystemMessages() (err error) {
defer func() {
svc.sysmsgs = types.MessageSet{}
}()
return svc.sysmsgs.Walk(func(msg *types.Message) error {
if msg, err = svc.message.CreateMessage(msg); err != nil {
return err
} else {
return svc.evl.Message(msg)
}
}
})
return svc.evl.Message(msg)
}
//// @todo temp location, move this somewhere else
+10
View File
@@ -52,6 +52,16 @@ func (mm ChannelMemberSet) MembersOf(channelID uint64) []uint64 {
return mmof
}
func (uu ChannelMemberSet) FindByUserId(userID uint64) *ChannelMember {
for i := range uu {
if uu[i].UserID == userID {
return uu[i]
}
}
return nil
}
const (
ChannelMembershipTypeOwner ChannelMembershipType = "owner"
ChannelMembershipTypeMember = "member"