Save internal_name and display_name in channel

This commit is contained in:
Mike Schwörer 2022-12-22 11:22:36 +01:00
parent f65c231ba0
commit 0cb2a977a0
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
20 changed files with 414 additions and 296 deletions

View File

@ -23,6 +23,8 @@
-------------------------------------------------------------------------------------------------------------------------------
- (?) default-priority for channels
- (?) ack/read deliveries && return ack-count (? or not, how to query?)
- (?) "login" on website and list/search/filter messages

View File

@ -20,13 +20,14 @@ const (
BINDFAIL_URI_PARAM APIError = 1153
INVALID_ENUM_VALUE APIError = 1171
NO_TITLE APIError = 1201
TITLE_TOO_LONG APIError = 1202
CONTENT_TOO_LONG APIError = 1203
USR_MSG_ID_TOO_LONG APIError = 1204
TIMESTAMP_OUT_OF_RANGE APIError = 1205
SENDERNAME_TOO_LONG APIError = 1206
CHANNEL_TOO_LONG APIError = 1207
NO_TITLE APIError = 1201
TITLE_TOO_LONG APIError = 1202
CONTENT_TOO_LONG APIError = 1203
USR_MSG_ID_TOO_LONG APIError = 1204
TIMESTAMP_OUT_OF_RANGE APIError = 1205
SENDERNAME_TOO_LONG APIError = 1206
CHANNEL_TOO_LONG APIError = 1207
CHANNEL_NAME_WOULD_CHANGE APIError = 1207
USER_NOT_FOUND APIError = 1301
CLIENT_NOT_FOUND APIError = 1302

View File

@ -667,9 +667,10 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
return *permResp
}
channelName := h.app.NormalizeChannelName(b.Name)
channelDisplayName := h.app.NormalizeChannelDisplayName(b.Name)
channelInternalName := h.app.NormalizeChannelInternalName(b.Name)
channelExisting, err := h.database.GetChannelByName(ctx, u.UserID, channelName)
channelExisting, err := h.database.GetChannelByName(ctx, u.UserID, channelInternalName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
@ -682,7 +683,10 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err)
}
if len(channelName) > user.MaxChannelNameLength() {
if len(channelDisplayName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if len(channelInternalName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
@ -693,7 +697,7 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
subscribeKey := h.app.GenerateRandomAuthKey()
sendKey := h.app.GenerateRandomAuthKey()
channel, err := h.database.CreateChannel(ctx, u.UserID, channelName, subscribeKey, sendKey)
channel, err := h.database.CreateChannel(ctx, u.UserID, channelDisplayName, channelInternalName, subscribeKey, sendKey)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err)
}
@ -726,6 +730,7 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
//
// @Param subscribe_key body string false "Send `true` to create a new subscribe_key"
// @Param send_key body string false "Send `true` to create a new send_key"
// @Param display_name body string false "Change the cahnnel display-name (only chnages to lowercase/uppercase are allowed - internal_name must stay the same)"
//
// @Success 200 {object} models.ChannelWithSubscriptionJSON
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
@ -740,8 +745,9 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
ChannelID models.ChannelID `uri:"cid"`
}
type body struct {
RefreshSubscribeKey *bool `json:"subscribe_key"`
RefreshSendKey *bool `json:"send_key"`
RefreshSubscribeKey *bool `json:"subscribe_key"`
RefreshSendKey *bool `json:"send_key"`
DisplayName *string `json:"display_name"`
}
var u uri
@ -756,7 +762,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
return *permResp
}
_, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID)
oldChannel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID)
if err == sql.ErrNoRows {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
@ -769,7 +775,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
err := h.database.UpdateChannelSendKey(ctx, u.ChannelID, newkey)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err)
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err)
}
}
@ -778,13 +784,29 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
err := h.database.UpdateChannelSubscribeKey(ctx, u.ChannelID, newkey)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err)
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err)
}
}
if b.DisplayName != nil {
newDisplayName := h.app.NormalizeChannelDisplayName(*b.DisplayName)
newInternalName := h.app.NormalizeChannelInternalName(*b.DisplayName)
if newInternalName != oldChannel.InternalName {
return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_WOULD_CHANGE, "Cannot substantially change the channel name", err)
}
err := h.database.UpdateChannelDisplayName(ctx, u.ChannelID, newDisplayName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err)
}
}
channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err)
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) channel", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON(true)))
@ -1191,7 +1213,9 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
return *permResp
}
channel, err := h.database.GetChannelByName(ctx, b.ChannelOwnerUserID, h.app.NormalizeChannelName(b.Channel))
channelInternalName := h.app.NormalizeChannelInternalName(b.Channel)
channel, err := h.database.GetChannelByName(ctx, b.ChannelOwnerUserID, channelInternalName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}

View File

@ -166,9 +166,11 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err)
}
channelName := user.DefaultChannel()
channelDisplayName := user.DefaultChannel()
channelInternalName := user.DefaultChannel()
if Channel != nil {
channelName = h.app.NormalizeChannelName(*Channel)
channelDisplayName = h.app.NormalizeChannelDisplayName(*Channel)
channelInternalName = h.app.NormalizeChannelInternalName(*Channel)
}
if len(*Title) > user.MaxTitleLength() {
@ -177,7 +179,10 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
if Content != nil && len(*Content) > user.MaxContentLength() {
return ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, hl.CONTENT, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(*Content), user.MaxContentLength()), nil)
}
if len(channelName) > user.MaxChannelNameLength() {
if len(channelDisplayName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if len(channelInternalName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if SenderName != nil && len(*SenderName) > user.MaxSenderName() {
@ -220,7 +225,7 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
if ChanKey != nil {
// foreign channel (+ channel send-key)
foreignChan, err := h.database.GetChannelByNameAndSendKey(ctx, channelName, *ChanKey)
foreignChan, err := h.database.GetChannelByNameAndSendKey(ctx, channelInternalName, *ChanKey)
if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query (foreign) channel", err)
}
@ -231,7 +236,7 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
} else {
// own channel
channel, err = h.app.GetOrCreateChannel(ctx, *UserID, channelName)
channel, err = h.app.GetOrCreateChannel(ctx, *UserID, channelDisplayName, channelInternalName)
if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err)
}

View File

@ -13,7 +13,7 @@ func (db *Database) GetChannelByName(ctx TxContext, userid models.UserID, chanNa
return nil, err
}
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE owner_user_id = :uid AND name = :nam LIMIT 1", sq.PP{
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE owner_user_id = :uid AND internal_name = :nam LIMIT 1", sq.PP{
"uid": userid,
"nam": chanName,
})
@ -38,7 +38,7 @@ func (db *Database) GetChannelByNameAndSendKey(ctx TxContext, chanName string, s
return nil, err
}
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE name = :chan_name OR send_key = :send_key LIMIT 1", sq.PP{
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE internal_name = :chan_name OR send_key = :send_key LIMIT 1", sq.PP{
"chan_name": chanName,
"send_key": sendKey,
})
@ -57,7 +57,7 @@ func (db *Database) GetChannelByNameAndSendKey(ctx TxContext, chanName string, s
return &channel, nil
}
func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, name string, subscribeKey string, sendKey string) (models.Channel, error) {
func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName string, intName string, subscribeKey string, sendKey string) (models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Channel{}, err
@ -65,9 +65,10 @@ func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, name stri
now := time.Now().UTC()
res, err := tx.Exec(ctx, "INSERT INTO channels (owner_user_id, name, subscribe_key, send_key, timestamp_created) VALUES (:ouid, :nam, :subkey, :sendkey, :ts)", sq.PP{
res, err := tx.Exec(ctx, "INSERT INTO channels (owner_user_id, display_name, internal_name, subscribe_key, send_key, timestamp_created) VALUES (:ouid, :dnam, :inam, :subkey, :sendkey, :ts)", sq.PP{
"ouid": userid,
"nam": name,
"dnam": dispName,
"inam": intName,
"subkey": subscribeKey,
"sendkey": sendKey,
"ts": time2DB(now),
@ -84,7 +85,8 @@ func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, name stri
return models.Channel{
ChannelID: models.ChannelID(liid),
OwnerUserID: userid,
Name: name,
DisplayName: dispName,
InternalName: intName,
SubscribeKey: subscribeKey,
SendKey: sendKey,
TimestampCreated: now,
@ -246,3 +248,20 @@ func (db *Database) UpdateChannelSubscribeKey(ctx TxContext, channelid models.Ch
return nil
}
func (db *Database) UpdateChannelDisplayName(ctx TxContext, channelid models.ChannelID, dispname string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE channels SET display_name = :nam WHERE channel_id = :cid", sq.PP{
"nam": dispname,
"cid": channelid,
})
if err != nil {
return err
}
return nil
}

View File

@ -64,10 +64,10 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID models.UserID, cha
now := time.Now().UTC()
res, err := tx.Exec(ctx, "INSERT INTO messages (sender_user_id, owner_user_id, channel_name, channel_id, timestamp_real, timestamp_client, title, content, priority, usr_message_id, sender_ip, sender_name) VALUES (:suid, :ouid, :cnam, :cid, :tsr, :tsc, :tit, :cnt, :prio, :umid, :ip, :snam)", sq.PP{
res, err := tx.Exec(ctx, "INSERT INTO messages (sender_user_id, owner_user_id, channel_internal_name, channel_id, timestamp_real, timestamp_client, title, content, priority, usr_message_id, sender_ip, sender_name) VALUES (:suid, :ouid, :cnam, :cid, :tsr, :tsc, :tit, :cnt, :prio, :umid, :ip, :snam)", sq.PP{
"suid": senderUserID,
"ouid": channel.OwnerUserID,
"cnam": channel.Name,
"cnam": channel.InternalName,
"cid": channel.ChannelID,
"tsr": time2DB(now),
"tsc": time2DBOpt(timestampSend),
@ -88,19 +88,19 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID models.UserID, cha
}
return models.Message{
SCNMessageID: models.SCNMessageID(liid),
SenderUserID: senderUserID,
OwnerUserID: channel.OwnerUserID,
ChannelName: channel.Name,
ChannelID: channel.ChannelID,
SenderIP: senderIP,
SenderName: senderName,
TimestampReal: now,
TimestampClient: timestampSend,
Title: title,
Content: content,
Priority: priority,
UserMessageID: userMsgId,
SCNMessageID: models.SCNMessageID(liid),
SenderUserID: senderUserID,
OwnerUserID: channel.OwnerUserID,
ChannelInternalName: channel.InternalName,
ChannelID: channel.ChannelID,
SenderIP: senderIP,
SenderName: senderName,
TimestampReal: now,
TimestampClient: timestampSend,
Title: title,
Content: content,
Priority: priority,
UserMessageID: userMsgId,
}, nil
}

View File

@ -46,7 +46,8 @@ CREATE TABLE channels
owner_user_id INTEGER NOT NULL,
name TEXT NOT NULL,
internal_name TEXT NOT NULL,
display_name TEXT NOT NULL,
subscribe_key TEXT NOT NULL,
send_key TEXT NOT NULL,
@ -56,7 +57,7 @@ CREATE TABLE channels
messages_sent INTEGER NOT NULL DEFAULT '0'
) STRICT;
CREATE UNIQUE INDEX "idx_channels_identity" ON channels (owner_user_id, name);
CREATE UNIQUE INDEX "idx_channels_identity" ON channels (owner_user_id, internal_name);
CREATE TABLE subscriptions
(
@ -64,40 +65,45 @@ CREATE TABLE subscriptions
subscriber_user_id INTEGER NOT NULL,
channel_owner_user_id INTEGER NOT NULL,
channel_name TEXT NOT NULL,
channel_internal_name TEXT NOT NULL,
channel_id INTEGER NOT NULL,
timestamp_created INTEGER NOT NULL,
confirmed INTEGER CHECK(confirmed IN (0, 1)) NOT NULL
) STRICT;
CREATE UNIQUE INDEX "idx_subscriptions_ref" ON subscriptions (subscriber_user_id, channel_owner_user_id, channel_name);
CREATE UNIQUE INDEX "idx_subscriptions_ref" ON subscriptions (subscriber_user_id, channel_owner_user_id, channel_internal_name);
CREATE INDEX "idx_subscriptions_chan" ON subscriptions (channel_id);
CREATE INDEX "idx_subscriptions_subuser" ON subscriptions (subscriber_user_id);
CREATE INDEX "idx_subscriptions_ownuser" ON subscriptions (channel_owner_user_id);
CREATE INDEX "idx_subscriptions_tsc" ON subscriptions (timestamp_created);
CREATE INDEX "idx_subscriptions_conf" ON subscriptions (confirmed);
CREATE TABLE messages
(
scn_message_id INTEGER PRIMARY KEY AUTOINCREMENT,
sender_user_id INTEGER NOT NULL,
owner_user_id INTEGER NOT NULL,
channel_name TEXT NOT NULL,
channel_id INTEGER NOT NULL,
sender_ip TEXT NOT NULL,
sender_name TEXT NULL,
scn_message_id INTEGER PRIMARY KEY AUTOINCREMENT,
sender_user_id INTEGER NOT NULL,
owner_user_id INTEGER NOT NULL,
channel_internal_name TEXT NOT NULL,
channel_id INTEGER NOT NULL,
sender_ip TEXT NOT NULL,
sender_name TEXT NULL,
timestamp_real INTEGER NOT NULL,
timestamp_client INTEGER NULL,
timestamp_real INTEGER NOT NULL,
timestamp_client INTEGER NULL,
title TEXT NOT NULL,
content TEXT NULL,
priority INTEGER CHECK(priority IN (0, 1, 2)) NOT NULL,
usr_message_id TEXT NULL,
title TEXT NOT NULL,
content TEXT NULL,
priority INTEGER CHECK(priority IN (0, 1, 2)) NOT NULL,
usr_message_id TEXT NULL,
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0'
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0'
) STRICT;
CREATE INDEX "idx_messages_owner_channel" ON messages (owner_user_id, channel_name COLLATE BINARY);
CREATE INDEX "idx_messages_owner_channel_nc" ON messages (owner_user_id, channel_name COLLATE NOCASE);
CREATE INDEX "idx_messages_channel" ON messages (channel_name COLLATE BINARY);
CREATE INDEX "idx_messages_channel_nc" ON messages (channel_name COLLATE NOCASE);
CREATE INDEX "idx_messages_owner_channel" ON messages (owner_user_id, channel_internal_name COLLATE BINARY);
CREATE INDEX "idx_messages_owner_channel_nc" ON messages (owner_user_id, channel_internal_name COLLATE NOCASE);
CREATE INDEX "idx_messages_channel" ON messages (channel_internal_name COLLATE BINARY);
CREATE INDEX "idx_messages_channel_nc" ON messages (channel_internal_name COLLATE NOCASE);
CREATE UNIQUE INDEX "idx_messages_idempotency" ON messages (owner_user_id, usr_message_id COLLATE BINARY);
CREATE INDEX "idx_messages_senderip" ON messages (sender_ip COLLATE BINARY);
CREATE INDEX "idx_messages_sendername" ON messages (sender_name COLLATE BINARY);
@ -109,7 +115,7 @@ CREATE INDEX "idx_messages_deleted" ON messages (deleted);
CREATE VIRTUAL TABLE messages_fts USING fts5
(
channel_name,
channel_internal_name,
sender_name,
title,
content,
@ -120,16 +126,16 @@ CREATE VIRTUAL TABLE messages_fts USING fts5
);
CREATE TRIGGER fts_insert AFTER INSERT ON messages BEGIN
INSERT INTO messages_fts (rowid, channel_name, sender_name, title, content) VALUES (new.scn_message_id, new.channel_name, new.sender_name, new.title, new.content);
INSERT INTO messages_fts (rowid, channel_internal_name, sender_name, title, content) VALUES (new.scn_message_id, new.channel_internal_name, new.sender_name, new.title, new.content);
END;
CREATE TRIGGER fts_update AFTER UPDATE ON messages BEGIN
INSERT INTO messages_fts (messages_fts, rowid, channel_name, sender_name, title, content) VALUES ('delete', old.scn_message_id, old.channel_name, old.sender_name, old.title, old.content);
INSERT INTO messages_fts ( rowid, channel_name, sender_name, title, content) VALUES ( new.scn_message_id, new.channel_name, new.sender_name, new.title, new.content);
INSERT INTO messages_fts (messages_fts, rowid, channel_internal_name, sender_name, title, content) VALUES ('delete', old.scn_message_id, old.channel_internal_name, old.sender_name, old.title, old.content);
INSERT INTO messages_fts ( rowid, channel_internal_name, sender_name, title, content) VALUES ( new.scn_message_id, new.channel_internal_name, new.sender_name, new.title, new.content);
END;
CREATE TRIGGER fts_delete AFTER DELETE ON messages BEGIN
INSERT INTO messages_fts (messages_fts, rowid, channel_name, sender_name, title, content) VALUES ('delete', old.scn_message_id, old.channel_name, old.sender_name, old.title, old.content);
INSERT INTO messages_fts (messages_fts, rowid, channel_internal_name, sender_name, title, content) VALUES ('delete', old.scn_message_id, old.channel_internal_name, old.sender_name, old.title, old.content);
END;

View File

@ -15,10 +15,10 @@ func (db *Database) CreateSubscription(ctx TxContext, subscriberUID models.UserI
now := time.Now().UTC()
res, err := tx.Exec(ctx, "INSERT INTO subscriptions (subscriber_user_id, channel_owner_user_id, channel_name, channel_id, timestamp_created, confirmed) VALUES (:suid, :ouid, :cnam, :cid, :ts, :conf)", sq.PP{
res, err := tx.Exec(ctx, "INSERT INTO subscriptions (subscriber_user_id, channel_owner_user_id, channel_internal_name, channel_id, timestamp_created, confirmed) VALUES (:suid, :ouid, :cnam, :cid, :ts, :conf)", sq.PP{
"suid": subscriberUID,
"ouid": channel.OwnerUserID,
"cnam": channel.Name,
"cnam": channel.InternalName,
"cid": channel.ChannelID,
"ts": time2DB(now),
"conf": confirmed,
@ -33,13 +33,13 @@ func (db *Database) CreateSubscription(ctx TxContext, subscriberUID models.UserI
}
return models.Subscription{
SubscriptionID: models.SubscriptionID(liid),
SubscriberUserID: subscriberUID,
ChannelOwnerUserID: channel.OwnerUserID,
ChannelID: channel.ChannelID,
ChannelName: channel.Name,
TimestampCreated: now,
Confirmed: confirmed,
SubscriptionID: models.SubscriptionID(liid),
SubscriberUserID: subscriberUID,
ChannelOwnerUserID: channel.OwnerUserID,
ChannelID: channel.ChannelID,
ChannelInternalName: channel.InternalName,
TimestampCreated: now,
Confirmed: confirmed,
}, nil
}

View File

@ -282,10 +282,8 @@ func (app *Application) getPermissions(ctx *AppContext, hdr string) (PermissionS
return NewEmptyPermissions(), nil
}
func (app *Application) GetOrCreateChannel(ctx *AppContext, userid models.UserID, chanName string) (models.Channel, error) {
chanName = app.NormalizeChannelName(chanName)
existingChan, err := app.Database.GetChannelByName(ctx, userid, chanName)
func (app *Application) GetOrCreateChannel(ctx *AppContext, userid models.UserID, displayChanName string, intChanName string) (models.Channel, error) {
existingChan, err := app.Database.GetChannelByName(ctx, userid, intChanName)
if err != nil {
return models.Channel{}, err
}
@ -297,7 +295,7 @@ func (app *Application) GetOrCreateChannel(ctx *AppContext, userid models.UserID
subscribeKey := app.GenerateRandomAuthKey()
sendKey := app.GenerateRandomAuthKey()
newChan, err := app.Database.CreateChannel(ctx, userid, chanName, subscribeKey, sendKey)
newChan, err := app.Database.CreateChannel(ctx, userid, displayChanName, intChanName, subscribeKey, sendKey)
if err != nil {
return models.Channel{}, err
}
@ -310,12 +308,22 @@ func (app *Application) GetOrCreateChannel(ctx *AppContext, userid models.UserID
return newChan, nil
}
func (app *Application) NormalizeChannelName(v string) string {
rex := regexp.MustCompile("[^[:alnum:]\\-_]")
var rexWhitespaceStart = regexp.MustCompile("^\\s+")
var rexWhitespaceEnd = regexp.MustCompile("\\s+$")
func (app *Application) NormalizeChannelDisplayName(v string) string {
v = strings.TrimSpace(v)
v = rexWhitespaceStart.ReplaceAllString(v, "")
v = rexWhitespaceEnd.ReplaceAllString(v, "")
return v
}
func (app *Application) NormalizeChannelInternalName(v string) string {
v = strings.TrimSpace(v)
v = strings.ToLower(v)
v = rex.ReplaceAllString(v, "")
v = rexWhitespaceStart.ReplaceAllString(v, "")
v = rexWhitespaceEnd.ReplaceAllString(v, "")
return v
}

View File

@ -10,7 +10,8 @@ import (
type Channel struct {
ChannelID ChannelID
OwnerUserID UserID
Name string
InternalName string
DisplayName string
SubscribeKey string
SendKey string
TimestampCreated time.Time
@ -22,7 +23,8 @@ func (c Channel) JSON(includeKey bool) ChannelJSON {
return ChannelJSON{
ChannelID: c.ChannelID,
OwnerUserID: c.OwnerUserID,
Name: c.Name,
InternalName: c.InternalName,
DisplayName: c.DisplayName,
SubscribeKey: langext.Conditional(includeKey, langext.Ptr(c.SubscribeKey), nil),
SendKey: langext.Conditional(includeKey, langext.Ptr(c.SendKey), nil),
TimestampCreated: c.TimestampCreated.Format(time.RFC3339Nano),
@ -57,7 +59,8 @@ func (c ChannelWithSubscription) JSON(includeChannelKey bool) ChannelWithSubscri
type ChannelJSON struct {
ChannelID ChannelID `json:"channel_id"`
OwnerUserID UserID `json:"owner_user_id"`
Name string `json:"name"`
InternalName string `json:"internal_name"`
DisplayName string `json:"display_name"`
SubscribeKey *string `json:"subscribe_key"` // can be nil, depending on endpoint
SendKey *string `json:"send_key"` // can be nil, depending on endpoint
TimestampCreated string `json:"timestamp_created"`
@ -73,7 +76,8 @@ type ChannelWithSubscriptionJSON struct {
type ChannelDB struct {
ChannelID ChannelID `db:"channel_id"`
OwnerUserID UserID `db:"owner_user_id"`
Name string `db:"name"`
InternalName string `db:"internal_name"`
DisplayName string `db:"display_name"`
SubscribeKey string `db:"subscribe_key"`
SendKey string `db:"send_key"`
TimestampCreated int64 `db:"timestamp_created"`
@ -86,7 +90,8 @@ func (c ChannelDB) Model() Channel {
return Channel{
ChannelID: c.ChannelID,
OwnerUserID: c.OwnerUserID,
Name: c.Name,
InternalName: c.InternalName,
DisplayName: c.DisplayName,
SubscribeKey: c.SubscribeKey,
SendKey: c.SendKey,
TimestampCreated: time.UnixMilli(c.TimestampCreated),

View File

@ -13,55 +13,55 @@ const (
)
type Message struct {
SCNMessageID SCNMessageID
SenderUserID UserID
OwnerUserID UserID
ChannelName string
ChannelID ChannelID
SenderName *string
SenderIP string
TimestampReal time.Time
TimestampClient *time.Time
Title string
Content *string
Priority int
UserMessageID *string
Deleted bool
SCNMessageID SCNMessageID
SenderUserID UserID
OwnerUserID UserID
ChannelInternalName string
ChannelID ChannelID
SenderName *string
SenderIP string
TimestampReal time.Time
TimestampClient *time.Time
Title string
Content *string
Priority int
UserMessageID *string
Deleted bool
}
func (m Message) FullJSON() MessageJSON {
return MessageJSON{
SCNMessageID: m.SCNMessageID,
SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID,
ChannelName: m.ChannelName,
ChannelID: m.ChannelID,
SenderName: m.SenderName,
SenderIP: m.SenderIP,
Timestamp: m.Timestamp().Format(time.RFC3339Nano),
Title: m.Title,
Content: m.Content,
Priority: m.Priority,
UserMessageID: m.UserMessageID,
Trimmed: false,
SCNMessageID: m.SCNMessageID,
SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID,
ChannelInternalName: m.ChannelInternalName,
ChannelID: m.ChannelID,
SenderName: m.SenderName,
SenderIP: m.SenderIP,
Timestamp: m.Timestamp().Format(time.RFC3339Nano),
Title: m.Title,
Content: m.Content,
Priority: m.Priority,
UserMessageID: m.UserMessageID,
Trimmed: false,
}
}
func (m Message) TrimmedJSON() MessageJSON {
return MessageJSON{
SCNMessageID: m.SCNMessageID,
SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID,
ChannelName: m.ChannelName,
ChannelID: m.ChannelID,
SenderName: m.SenderName,
SenderIP: m.SenderIP,
Timestamp: m.Timestamp().Format(time.RFC3339Nano),
Title: m.Title,
Content: m.TrimmedContent(),
Priority: m.Priority,
UserMessageID: m.UserMessageID,
Trimmed: m.NeedsTrim(),
SCNMessageID: m.SCNMessageID,
SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID,
ChannelInternalName: m.ChannelInternalName,
ChannelID: m.ChannelID,
SenderName: m.SenderName,
SenderIP: m.SenderIP,
Timestamp: m.Timestamp().Format(time.RFC3339Nano),
Title: m.Title,
Content: m.TrimmedContent(),
Priority: m.Priority,
UserMessageID: m.UserMessageID,
Trimmed: m.NeedsTrim(),
}
}
@ -94,54 +94,54 @@ func (m Message) ShortContent() string {
}
type MessageJSON struct {
SCNMessageID SCNMessageID `json:"scn_message_id"`
SenderUserID UserID `json:"sender_user_id"`
OwnerUserID UserID `json:"owner_user_id"`
ChannelName string `json:"channel_name"`
ChannelID ChannelID `json:"channel_id"`
SenderName *string `json:"sender_name"`
SenderIP string `json:"sender_ip"`
Timestamp string `json:"timestamp"`
Title string `json:"title"`
Content *string `json:"content"`
Priority int `json:"priority"`
UserMessageID *string `json:"usr_message_id"`
Trimmed bool `json:"trimmed"`
SCNMessageID SCNMessageID `json:"scn_message_id"`
SenderUserID UserID `json:"sender_user_id"`
OwnerUserID UserID `json:"owner_user_id"`
ChannelInternalName string `json:"channel_internal_name"`
ChannelID ChannelID `json:"channel_id"`
SenderName *string `json:"sender_name"`
SenderIP string `json:"sender_ip"`
Timestamp string `json:"timestamp"`
Title string `json:"title"`
Content *string `json:"content"`
Priority int `json:"priority"`
UserMessageID *string `json:"usr_message_id"`
Trimmed bool `json:"trimmed"`
}
type MessageDB struct {
SCNMessageID SCNMessageID `db:"scn_message_id"`
SenderUserID UserID `db:"sender_user_id"`
OwnerUserID UserID `db:"owner_user_id"`
ChannelName string `db:"channel_name"`
ChannelID ChannelID `db:"channel_id"`
SenderName *string `db:"sender_name"`
SenderIP string `db:"sender_ip"`
TimestampReal int64 `db:"timestamp_real"`
TimestampClient *int64 `db:"timestamp_client"`
Title string `db:"title"`
Content *string `db:"content"`
Priority int `db:"priority"`
UserMessageID *string `db:"usr_message_id"`
Deleted int `db:"deleted"`
SCNMessageID SCNMessageID `db:"scn_message_id"`
SenderUserID UserID `db:"sender_user_id"`
OwnerUserID UserID `db:"owner_user_id"`
ChannelInternalName string `db:"channel_internal_name"`
ChannelID ChannelID `db:"channel_id"`
SenderName *string `db:"sender_name"`
SenderIP string `db:"sender_ip"`
TimestampReal int64 `db:"timestamp_real"`
TimestampClient *int64 `db:"timestamp_client"`
Title string `db:"title"`
Content *string `db:"content"`
Priority int `db:"priority"`
UserMessageID *string `db:"usr_message_id"`
Deleted int `db:"deleted"`
}
func (m MessageDB) Model() Message {
return Message{
SCNMessageID: m.SCNMessageID,
SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID,
ChannelName: m.ChannelName,
ChannelID: m.ChannelID,
SenderName: m.SenderName,
SenderIP: m.SenderIP,
TimestampReal: time.UnixMilli(m.TimestampReal),
TimestampClient: timeOptFromMilli(m.TimestampClient),
Title: m.Title,
Content: m.Content,
Priority: m.Priority,
UserMessageID: m.UserMessageID,
Deleted: m.Deleted != 0,
SCNMessageID: m.SCNMessageID,
SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID,
ChannelInternalName: m.ChannelInternalName,
ChannelID: m.ChannelID,
SenderName: m.SenderName,
SenderIP: m.SenderIP,
TimestampReal: time.UnixMilli(m.TimestampReal),
TimestampClient: timeOptFromMilli(m.TimestampClient),
Title: m.Title,
Content: m.Content,
Priority: m.Priority,
UserMessageID: m.UserMessageID,
Deleted: m.Deleted != 0,
}
}

View File

@ -8,56 +8,56 @@ import (
)
type Subscription struct {
SubscriptionID SubscriptionID
SubscriberUserID UserID
ChannelOwnerUserID UserID
ChannelID ChannelID
ChannelName string
TimestampCreated time.Time
Confirmed bool
SubscriptionID SubscriptionID
SubscriberUserID UserID
ChannelOwnerUserID UserID
ChannelID ChannelID
ChannelInternalName string
TimestampCreated time.Time
Confirmed bool
}
func (s Subscription) JSON() SubscriptionJSON {
return SubscriptionJSON{
SubscriptionID: s.SubscriptionID,
SubscriberUserID: s.SubscriberUserID,
ChannelOwnerUserID: s.ChannelOwnerUserID,
ChannelID: s.ChannelID,
ChannelName: s.ChannelName,
TimestampCreated: s.TimestampCreated.Format(time.RFC3339Nano),
Confirmed: s.Confirmed,
SubscriptionID: s.SubscriptionID,
SubscriberUserID: s.SubscriberUserID,
ChannelOwnerUserID: s.ChannelOwnerUserID,
ChannelID: s.ChannelID,
ChannelInternalName: s.ChannelInternalName,
TimestampCreated: s.TimestampCreated.Format(time.RFC3339Nano),
Confirmed: s.Confirmed,
}
}
type SubscriptionJSON struct {
SubscriptionID SubscriptionID `json:"subscription_id"`
SubscriberUserID UserID `json:"subscriber_user_id"`
ChannelOwnerUserID UserID `json:"channel_owner_user_id"`
ChannelID ChannelID `json:"channel_id"`
ChannelName string `json:"channel_name"`
TimestampCreated string `json:"timestamp_created"`
Confirmed bool `json:"confirmed"`
SubscriptionID SubscriptionID `json:"subscription_id"`
SubscriberUserID UserID `json:"subscriber_user_id"`
ChannelOwnerUserID UserID `json:"channel_owner_user_id"`
ChannelID ChannelID `json:"channel_id"`
ChannelInternalName string `json:"channel_internal_name"`
TimestampCreated string `json:"timestamp_created"`
Confirmed bool `json:"confirmed"`
}
type SubscriptionDB struct {
SubscriptionID SubscriptionID `db:"subscription_id"`
SubscriberUserID UserID `db:"subscriber_user_id"`
ChannelOwnerUserID UserID `db:"channel_owner_user_id"`
ChannelID ChannelID `db:"channel_id"`
ChannelName string `db:"channel_name"`
TimestampCreated int64 `db:"timestamp_created"`
Confirmed int `db:"confirmed"`
SubscriptionID SubscriptionID `db:"subscription_id"`
SubscriberUserID UserID `db:"subscriber_user_id"`
ChannelOwnerUserID UserID `db:"channel_owner_user_id"`
ChannelID ChannelID `db:"channel_id"`
ChannelInternalName string `db:"channel_internal_name"`
TimestampCreated int64 `db:"timestamp_created"`
Confirmed int `db:"confirmed"`
}
func (s SubscriptionDB) Model() Subscription {
return Subscription{
SubscriptionID: s.SubscriptionID,
SubscriberUserID: s.SubscriberUserID,
ChannelOwnerUserID: s.ChannelOwnerUserID,
ChannelID: s.ChannelID,
ChannelName: s.ChannelName,
TimestampCreated: time.UnixMilli(s.TimestampCreated),
Confirmed: s.Confirmed != 0,
SubscriptionID: s.SubscriptionID,
SubscriberUserID: s.SubscriberUserID,
ChannelOwnerUserID: s.ChannelOwnerUserID,
ChannelID: s.ChannelID,
ChannelInternalName: s.ChannelInternalName,
TimestampCreated: time.UnixMilli(s.TimestampCreated),
Confirmed: s.Confirmed != 0,
}
}

View File

@ -1356,6 +1356,14 @@
"schema": {
"type": "string"
}
},
{
"description": "Change the cahnnel display-name (only chnages to lowercase/uppercase are allowed - internal_name must stay the same)",
"name": "display_name",
"in": "body",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -2826,12 +2834,15 @@
"channel_id": {
"type": "integer"
},
"display_name": {
"type": "string"
},
"internal_name": {
"type": "string"
},
"messages_sent": {
"type": "integer"
},
"name": {
"type": "string"
},
"owner_user_id": {
"type": "integer"
},
@ -2912,7 +2923,7 @@
"channel_id": {
"type": "integer"
},
"channel_name": {
"channel_internal_name": {
"type": "string"
},
"content": {
@ -2956,7 +2967,7 @@
"channel_id": {
"type": "integer"
},
"channel_name": {
"channel_internal_name": {
"type": "string"
},
"channel_owner_user_id": {

View File

@ -340,10 +340,12 @@ definitions:
properties:
channel_id:
type: integer
display_name:
type: string
internal_name:
type: string
messages_sent:
type: integer
name:
type: string
owner_user_id:
type: integer
send_key:
@ -397,7 +399,7 @@ definitions:
properties:
channel_id:
type: integer
channel_name:
channel_internal_name:
type: string
content:
type: string
@ -426,7 +428,7 @@ definitions:
properties:
channel_id:
type: integer
channel_name:
channel_internal_name:
type: string
channel_owner_user_id:
type: integer
@ -1433,6 +1435,12 @@ paths:
name: send_key
schema:
type: string
- description: Change the cahnnel display-name (only chnages to lowercase/uppercase
are allowed - internal_name must stay the same)
in: body
name: display_name
schema:
type: string
responses:
"200":
description: OK

View File

@ -29,7 +29,8 @@ func TestCreateChannel(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "name")
tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "internal_name")
}
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
@ -39,7 +40,8 @@ func TestCreateChannel(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels))
tt.AssertMappedSet(t, "channels", []string{"test"}, clist.Channels, "name")
tt.AssertMappedSet(t, "channels", []string{"test"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"test"}, clist.Channels, "internal_name")
}
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
@ -48,7 +50,8 @@ func TestCreateChannel(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertMappedSet(t, "channels", []string{"asdf", "test"}, clist.Channels, "name")
tt.AssertMappedSet(t, "channels", []string{"asdf", "test"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"asdf", "test"}, clist.Channels, "internal_name")
}
}
@ -90,8 +93,9 @@ func TestChannelNameNormalization(t *testing.T) {
}
{
chan0 := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 0, len(chan0.Channels))
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "internal_name")
}
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
@ -100,8 +104,8 @@ func TestChannelNameNormalization(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels))
tt.AssertEqual(t, "chan.name", "test", clist.Channels[0]["name"])
tt.AssertMappedSet(t, "channels", []string{"tESt"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"test"}, clist.Channels, "internal_name")
}
tt.RequestAuthPostShouldFail(t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
@ -125,14 +129,25 @@ func TestChannelNameNormalization(t *testing.T) {
}, 409, apierr.CHANNEL_ALREADY_EXISTS)
tt.RequestAuthPostShouldFail(t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
"name": " T e s t ",
"name": "\rTeSt\n",
}, 409, apierr.CHANNEL_ALREADY_EXISTS)
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels))
tt.AssertEqual(t, "chan.name", "test", clist.Channels[0]["name"])
tt.AssertMappedSet(t, "channels", []string{"tESt"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"test"}, clist.Channels, "internal_name")
}
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
"name": " WeiRD_[\uF5FF]\\stUFf\r\n\t ",
})
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertMappedSet(t, "channels", []string{"tESt", "WeiRD_[\uF5FF]\\stUFf"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"test", "weird_[\uF5FF]\\stuff"}, clist.Channels, "internal_name")
}
}
func TestListChannelsOwned(t *testing.T) {
@ -147,24 +162,26 @@ func TestListChannelsOwned(t *testing.T) {
testdata := map[int][]string{
0: {"main", "chattingchamber", "unicdhll", "promotions", "reminders"},
1: {"promotions"},
2: {},
3: {},
4: {},
5: {},
6: {},
7: {},
8: {},
9: {},
10: {},
11: {},
1: {"main", "private"},
2: {"main", "ü", "ö", "ä"},
3: {"main", "innovations", "reminders"},
4: {"main"},
5: {"main", "test1", "test2", "test3", "test4", "test5"},
6: {"main", "security", "lipsum"},
7: {"main"},
8: {"main"},
9: {"main", "manual@chan"},
10: {"main"},
11: {"promotions"},
12: {},
13: {},
14: {"", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"", "chan_other_nosub", "chan_other_request", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
}
for k, v := range testdata {
r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID))
tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "name")
tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name")
}
}

View File

@ -166,7 +166,7 @@ func TestGetMessageFull(t *testing.T) {
tt.AssertEqual(t, "msg.title", "Message_1", msgIn["title"])
tt.AssertEqual(t, "msg.content", content, msgIn["content"])
tt.AssertEqual(t, "msg.channel", "demo-channel-007", msgIn["channel_name"])
tt.AssertEqual(t, "msg.channel", "demo-channel-007", msgIn["channel_internal_name"])
tt.AssertEqual(t, "msg.msg_id", "580b5055-a9b5-4cee-b53c-28cf304d25b0", msgIn["usr_message_id"])
tt.AssertStrRepEqual(t, "msg.priority", 0, msgIn["priority"])
tt.AssertEqual(t, "msg.sender_name", "unit-test-[TestGetMessageFull]", msgIn["sender_name"])

View File

@ -61,7 +61,7 @@ func TestSendSimpleMessageJSON(t *testing.T) {
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_001", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
}
func TestSendSimpleMessageQuery(t *testing.T) {
@ -97,7 +97,7 @@ func TestSendSimpleMessageQuery(t *testing.T) {
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "Hello World 2134", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
}
func TestSendSimpleMessageForm(t *testing.T) {
@ -137,7 +137,7 @@ func TestSendSimpleMessageForm(t *testing.T) {
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "Hello World 9999 [$$$]", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
}
func TestSendSimpleMessageFormAndQuery(t *testing.T) {
@ -238,7 +238,7 @@ func TestSendSimpleMessageAlt1(t *testing.T) {
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_001", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
}
func TestSendContentMessage(t *testing.T) {
@ -278,12 +278,12 @@ func TestSendContentMessage(t *testing.T) {
tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_042", msgList1.Messages[0]["title"])
tt.AssertStrRepEqual(t, "msg.content", "I am Content\nasdf", msgList1.Messages[0]["content"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msgList1.Messages[0]["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msgList1.Messages[0]["channel_internal_name"])
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_042", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.content", "I am Content\nasdf", msg1Get["content"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
}
func TestSendWithSendername(t *testing.T) {
@ -326,13 +326,13 @@ func TestSendWithSendername(t *testing.T) {
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_xyz", msgList1.Messages[0]["title"])
tt.AssertStrRepEqual(t, "msg.content", "Unicode: 日本 - yäy\000\n\t\x00...", msgList1.Messages[0]["content"])
tt.AssertStrRepEqual(t, "msg.sender_name", "localhorst", msgList1.Messages[0]["sender_name"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msgList1.Messages[0]["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msgList1.Messages[0]["channel_internal_name"])
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_xyz", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.content", "Unicode: 日本 - yäy\000\n\t\x00...", msg1Get["content"])
tt.AssertStrRepEqual(t, "msg.sender_name", "localhorst", msg1Get["sender_name"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
}
func TestSendLongContent(t *testing.T) {
@ -377,20 +377,20 @@ func TestSendLongContent(t *testing.T) {
tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_042", msgList1.Messages[0]["title"])
tt.AssertNotStrRepEqual(t, "msg.content", longContent, msgList1.Messages[0]["content"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msgList1.Messages[0]["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msgList1.Messages[0]["channel_internal_name"])
tt.AssertStrRepEqual(t, "msg.trimmmed", true, msgList1.Messages[0]["trimmed"])
msgList2 := tt.RequestAuthGet[mglist](t, admintok, baseUrl, "/api/messages?trimmed=false")
tt.AssertEqual(t, "len(messages)", 1, len(msgList2.Messages))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_042", msgList2.Messages[0]["title"])
tt.AssertStrRepEqual(t, "msg.content", longContent, msgList2.Messages[0]["content"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msgList2.Messages[0]["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msgList2.Messages[0]["channel_internal_name"])
tt.AssertStrRepEqual(t, "msg.trimmmed", false, msgList2.Messages[0]["trimmed"])
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_042", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.titcontentle", longContent, msg1Get["content"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
tt.AssertStrRepEqual(t, "msg.trimmmed", false, msg1Get["trimmed"])
}
@ -859,7 +859,7 @@ func TestSendWithTimestamp(t *testing.T) {
tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages))
tt.AssertStrRepEqual(t, "msg.title", "TTT", msgList1.Messages[0]["title"])
tt.AssertStrRepEqual(t, "msg.content", nil, msgList1.Messages[0]["sender_name"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msgList1.Messages[0]["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msgList1.Messages[0]["channel_internal_name"])
tm1, err := time.Parse(time.RFC3339Nano, msgList1.Messages[0]["timestamp"].(string))
tt.TestFailIfErr(t, err)
@ -868,7 +868,7 @@ func TestSendWithTimestamp(t *testing.T) {
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "TTT", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.content", nil, msg1Get["sender_name"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
tmg1, err := time.Parse(time.RFC3339Nano, msg1Get["timestamp"].(string))
tt.TestFailIfErr(t, err)
@ -1022,7 +1022,7 @@ func TestSendCompat(t *testing.T) {
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_001", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"])
tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
msg2 := tt.RequestPost[gin.H](t, baseUrl, fmt.Sprintf("/send.php?user_key=%s&user_id=%d&title=%s", sendtok, uid, "HelloWorld_002"), nil)
@ -1098,8 +1098,8 @@ func TestSendToNewChannel(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels))
tt.AssertEqual(t, "chan.name", "main", clist.Channels[0]["name"])
tt.AssertMappedSet(t, "channels", []string{"main"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"main"}, clist.Channels, "internal_name")
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1112,8 +1112,8 @@ func TestSendToNewChannel(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels))
tt.AssertEqual(t, "chan.name", "main", clist.Channels[0]["name"])
tt.AssertMappedSet(t, "channels", []string{"main"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"main"}, clist.Channels, "internal_name")
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1126,9 +1126,8 @@ func TestSendToNewChannel(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 2, len(clist.Channels))
tt.AssertArrAny(t, "chan.has('main')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "main" })
tt.AssertArrAny(t, "chan.has('test')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "test" })
tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "internal_name")
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1140,9 +1139,8 @@ func TestSendToNewChannel(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 2, len(clist.Channels))
tt.AssertArrAny(t, "chan.has('main')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "main" })
tt.AssertArrAny(t, "chan.has('test')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "test" })
tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "internal_name")
}
}
@ -1166,8 +1164,9 @@ func TestSendToManualChannel(t *testing.T) {
}
{
chan0 := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 0, len(chan0.Channels))
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "internal_name")
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1178,8 +1177,8 @@ func TestSendToManualChannel(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels))
tt.AssertEqual(t, "chan.name", "main", clist.Channels[0]["name"])
tt.AssertMappedSet(t, "channels", []string{"main"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"main"}, clist.Channels, "internal_name")
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1193,7 +1192,8 @@ func TestSendToManualChannel(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels))
tt.AssertEqual(t, "chan.name", "main", clist.Channels[0]["name"])
tt.AssertEqual(t, "chan.internal_name", "main", clist.Channels[0]["internal_name"])
tt.AssertEqual(t, "chan.display_name", "main", clist.Channels[0]["display_name"])
}
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
@ -1202,9 +1202,8 @@ func TestSendToManualChannel(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 2, len(clist.Channels))
tt.AssertArrAny(t, "chan.has('main')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "main" })
tt.AssertArrAny(t, "chan.has('test')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "test" })
tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "internal_name")
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1217,9 +1216,8 @@ func TestSendToManualChannel(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 2, len(clist.Channels))
tt.AssertArrAny(t, "chan.has('main')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "main" })
tt.AssertArrAny(t, "chan.has('test')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "test" })
tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "internal_name")
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1231,9 +1229,8 @@ func TestSendToManualChannel(t *testing.T) {
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 2, len(clist.Channels))
tt.AssertArrAny(t, "chan.has('main')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "main" })
tt.AssertArrAny(t, "chan.has('test')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "test" })
tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "internal_name")
}
}

View File

@ -203,7 +203,14 @@ func AssertMultiNonEmpty(t *testing.T, key string, args ...any) {
func AssertMappedSet[T langext.OrderedConstraint](t *testing.T, key string, expected []T, values []gin.H, objkey string) {
actual := langext.ArrMap(values, func(v gin.H) T { return v[objkey].(T) })
actual := make([]T, 0)
for idx, vv := range values {
if tv, ok := vv[objkey].(T); ok {
actual = append(actual, tv)
} else {
TestFailFmt(t, "[%s]->[%d] is wrong type (expected: %T, actual: %T)", key, idx, *new(T), vv)
}
}
langext.Sort(actual)
langext.Sort(expected)

View File

@ -86,8 +86,10 @@ var userExamples = []userex{
{9, true, "UniqueUnicorn", "Galaxy Quest", "2023.1", "ANDROID", "FCM_TOK_EX_010", ""},
{10, false, "", "", "", "", "", ""},
{11, false, "", "", "", "", "", "ANDROID|v2|PURCHASED:PRO_TOK_002"},
{12, true, "ChanTester1", "StarfireXX", "1.x", "IOS", "FCM_TOK_EX_012", ""},
{13, true, "ChanTester2", "StarfireXX", "1.x", "IOS", "FCM_TOK_EX_013", ""},
{12, true, "NoMessageUser", "Ocean Explorer", "737edc01", "IOS", "FCM_TOK_EX_014", ""},
{13, false, "EmptyUser", "", "", "", "", ""},
{14, true, "ChanTester1", "StarfireXX", "1.x", "IOS", "FCM_TOK_EX_012", ""},
{15, true, "ChanTester2", "StarfireXX", "1.x", "IOS", "FCM_TOK_EX_013", ""},
}
var clientExamples = []clientex{
@ -267,14 +269,14 @@ var messageExamples = []msgex{
{11, "Promotions", "", PX, AKEY, "Join Our VIP Club and Enjoy Exclusive Benefits", "Sign up for our VIP club and enjoy exclusive benefits like early access to sales, special offers, and personalized service. Don't miss out on this exclusive opportunity.", timeext.FromHours(2.32)},
{11, "Promotions", "", P2, SKEY, "Summer Clearance: Save Up to 75% on Your Favorite Products", "It's time for our annual summer clearance sale! Save up to 75% on your favorite products, from clothing and accessories to home decor and more.", timeext.FromHours(1.87)},
{12, "", "", P0, SKEY, "New Product Launch", "We are excited to announce the launch of our new product, the XYZ widget", 0},
{12, "chan_self_subscribed", "", P0, SKEY, "Important Update", "We have released a critical update", 0},
{12, "chan_self_unsub", "", P0, SKEY, "Reminder: Upcoming Maintenance", "", 0},
{14, "", "", P0, SKEY, "New Product Launch", "We are excited to announce the launch of our new product, the XYZ widget", 0},
{14, "chan_self_subscribed", "", P0, SKEY, "Important Update", "We have released a critical update", 0},
{14, "chan_self_unsub", "", P0, SKEY, "Reminder: Upcoming Maintenance", "", 0},
{13, "", "", P0, SKEY, "New Feature Available", "ability to schedule appointments", 0},
{13, "chan_other_nosub", "", P0, SKEY, "Account Suspended", "Please contact us", 0},
{13, "chan_other_request", "", P0, SKEY, "Invitation to Beta Test", "", 0},
{13, "chan_other_accepted", "", P0, SKEY, "New Blog Post", "Congratulations on your promotion! We are proud", 0},
{15, "", "", P0, SKEY, "New Feature Available", "ability to schedule appointments", 0},
{15, "chan_other_nosub", "", P0, SKEY, "Account Suspended", "Please contact us", 0},
{15, "chan_other_request", "", P0, SKEY, "Invitation to Beta Test", "", 0},
{15, "chan_other_accepted", "", P0, SKEY, "New Blog Post", "Congratulations on your promotion! We are proud", 0},
}
type DefData struct {
@ -373,6 +375,12 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
RequestPost[gin.H](t, baseUrl, "/", body)
}
// create manual channels
{
RequestAuthPost[Void](t, users[9].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", users[9].UID), gin.H{"name": "manual@chan"})
}
// Sub/Unsub for Users 12+13
{

View File

@ -163,10 +163,10 @@ func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL s
return data
}
func RequestAuthAnyShouldFail(t *testing.T, akey string, method string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) {
func RequestAuthAnyShouldFail(t *testing.T, akey string, method string, baseURL string, urlSuffix string, body any, expectedStatusCode int, errcode apierr.APIError) {
client := http.Client{}
TPrintf("[-> REQUEST] (%s) %s%s [%s] (should-fail with %d/%d)\n", method, baseURL, urlSuffix, langext.Conditional(akey == "", "NO AUTH", "AUTH"), statusCode, errcode)
TPrintf("[-> REQUEST] (%s) %s%s [%s] (should-fail with %d/%d)\n", method, baseURL, urlSuffix, langext.Conditional(akey == "", "NO AUTH", "AUTH"), expectedStatusCode, errcode)
bytesbody := make([]byte, 0)
contentType := ""
@ -224,17 +224,17 @@ func RequestAuthAnyShouldFail(t *testing.T, akey string, method string, baseURL
TPrintln("")
TPrintf("---------------- RESPONSE (%d) ----------------\n", resp.StatusCode)
TPrintln(langext.TryPrettyPrintJson(string(respBodyBin)))
if (statusCode != 0 && resp.StatusCode != statusCode) || (statusCode == 0 && resp.StatusCode == 200) {
if (expectedStatusCode != 0 && resp.StatusCode != expectedStatusCode) || (expectedStatusCode == 0 && resp.StatusCode == 200) {
TryPrintTraceObj("---------------- -------- ----------------", respBodyBin, "")
}
TPrintln("---------------- -------- ----------------")
TPrintln("")
if statusCode != 0 && resp.StatusCode != statusCode {
TestFailFmt(t, "Statuscode != %d (expected failure)", statusCode)
if expectedStatusCode != 0 && resp.StatusCode != expectedStatusCode {
TestFailFmt(t, "Statuscode != %d (expected failure, but got %d)", expectedStatusCode, resp.StatusCode)
}
if statusCode == 0 && resp.StatusCode == 200 {
TestFailFmt(t, "Statuscode == %d (expected failure)", resp.StatusCode)
if expectedStatusCode == 0 && resp.StatusCode == 200 {
TestFailFmt(t, "Statuscode == %d (expected any failure, but got %d)", resp.StatusCode, resp.StatusCode)
}
var data gin.H