Add various deleted flags to entities | Add active to subscriptions | Add DeleteUser && DeleteChannel endpoints
This commit is contained in:
parent
aac34ef738
commit
67882713f9
@ -20,7 +20,7 @@
|
||||
- [ ] push navigation stack
|
||||
- [ ] read + migrate old SharedPrefs (or not? - who uses SCN even??)
|
||||
- [ ] Account-Page
|
||||
- [ ] Logout
|
||||
- [x] Logout
|
||||
- [x] Send-page
|
||||
|
||||
- [ ] Still @ERROR on scn-init, but no logs? - better persist error (write in SharedPrefs at error_$date=txt ?), also perhaps print first error line in scn-init notification?
|
||||
|
@ -503,7 +503,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
}
|
||||
|
||||
void _deleteAccount() async {
|
||||
Toaster.info("Not Implemented", "Account Upgrading will be implemented in a later version"); // TODO
|
||||
// TODO
|
||||
}
|
||||
|
||||
void _changeUsername() async {
|
||||
|
@ -151,7 +151,7 @@ class _SendRootPageState extends State<SendRootPage> {
|
||||
controller: _senderName,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'SenderName',
|
||||
labelText: 'Sender',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -471,3 +471,76 @@ func (h APIHandler) ListChannelMessages(pctx ginext.PreContext) ginext.HTTPRespo
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteChannel swaggerdoc
|
||||
//
|
||||
// @Summary delete a channel (including all messages, subscriptions, etc)
|
||||
// @ID api-channels-delete
|
||||
// @Tags API-v2
|
||||
//
|
||||
// @Param uid path string true "UserID"
|
||||
// @Param cid path string true "ChannelID"
|
||||
//
|
||||
// @Success 200 {object} models.Channel
|
||||
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
||||
// @Failure 404 {object} ginresp.apiError "channel not found"
|
||||
// @Failure 500 {object} ginresp.apiError "internal server error"
|
||||
//
|
||||
// @Router /api/v2/users/{uid}/channels/{cid} [PATCH]
|
||||
func (h APIHandler) DeleteChannel(pctx ginext.PreContext) ginext.HTTPResponse {
|
||||
type uri struct {
|
||||
UserID models.UserID `uri:"uid" binding:"entityid"`
|
||||
ChannelID models.ChannelID `uri:"cid" binding:"entityid"`
|
||||
}
|
||||
|
||||
var u uri
|
||||
ctx, g, errResp := pctx.URI(&u).Start()
|
||||
if errResp != nil {
|
||||
return *errResp
|
||||
}
|
||||
defer ctx.Cancel()
|
||||
|
||||
return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
|
||||
|
||||
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
|
||||
return *permResp
|
||||
}
|
||||
|
||||
channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteChannel(ctx, u.ChannelID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete channel", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteDeliveriesOfChannel(ctx, u.ChannelID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete deliveries", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteMessagesOfChannel(ctx, u.ChannelID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete messages", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteSubscriptionsByChannel(ctx, u.ChannelID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete subscriptions", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteChannelFromKeyTokens(ctx, u.ChannelID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update keytokens", err)
|
||||
}
|
||||
|
||||
return finishSuccess(ginext.JSON(http.StatusOK, channel))
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ func (h APIHandler) ListMessages(pctx ginext.PreContext) ginext.HTTPResponse {
|
||||
}
|
||||
|
||||
filter := models.MessageFilter{
|
||||
ConfirmedSubscriptionBy: langext.Ptr(userid),
|
||||
ConfirmedAndActiveSubscriptionBy: langext.Ptr(userid),
|
||||
}
|
||||
|
||||
if len(q.Search) != 0 {
|
||||
|
@ -430,6 +430,7 @@ func (h APIHandler) UpdateSubscription(pctx ginext.PreContext) ginext.HTTPRespon
|
||||
}
|
||||
type body struct {
|
||||
Confirmed *bool `form:"confirmed"`
|
||||
Active *bool `form:"active"`
|
||||
}
|
||||
|
||||
var u uri
|
||||
@ -460,6 +461,9 @@ func (h APIHandler) UpdateSubscription(pctx ginext.PreContext) ginext.HTTPRespon
|
||||
}
|
||||
|
||||
if b.Confirmed != nil {
|
||||
|
||||
// only channel-owner can confirm|unconfirm
|
||||
|
||||
if subscription.ChannelOwnerUserID != userid {
|
||||
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
|
||||
}
|
||||
@ -469,6 +473,19 @@ func (h APIHandler) UpdateSubscription(pctx ginext.PreContext) ginext.HTTPRespon
|
||||
}
|
||||
}
|
||||
|
||||
if b.Active != nil {
|
||||
|
||||
// channel-owner AND subscriber can change active
|
||||
|
||||
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != userid {
|
||||
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
|
||||
}
|
||||
err = h.database.UpdateSubscriptionActive(ctx, u.SubscriptionID, *b.Active)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err)
|
||||
}
|
||||
}
|
||||
|
||||
subscription, err = h.database.GetSubscription(ctx, u.SubscriptionID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
|
||||
|
@ -274,3 +274,94 @@ func (h APIHandler) UpdateUser(pctx ginext.PreContext) ginext.HTTPResponse {
|
||||
return finishSuccess(ginext.JSON(http.StatusOK, user.PreMarshal()))
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteUser swaggerdoc
|
||||
//
|
||||
// @Summary (Self-)Deletes a user (including all entities - all messages, channels, clients, .....)
|
||||
//
|
||||
// @ID api-user-delete
|
||||
// @Tags API-v2
|
||||
//
|
||||
// @Param uid path string true "UserID"
|
||||
//
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
||||
// @Failure 404 {object} ginresp.apiError "user not found"
|
||||
// @Failure 500 {object} ginresp.apiError "internal server error"
|
||||
//
|
||||
// @Router /api/v2/users/{uid} [PATCH]
|
||||
func (h APIHandler) DeleteUser(pctx ginext.PreContext) ginext.HTTPResponse {
|
||||
type uri struct {
|
||||
UserID models.UserID `uri:"uid" binding:"entityid"`
|
||||
}
|
||||
|
||||
var u uri
|
||||
ctx, g, errResp := pctx.URI(&u).Start()
|
||||
if errResp != nil {
|
||||
return *errResp
|
||||
}
|
||||
defer ctx.Cancel()
|
||||
|
||||
return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
|
||||
|
||||
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
|
||||
return *permResp
|
||||
}
|
||||
|
||||
user, err := h.database.GetUser(ctx, u.UserID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteUser(ctx, u.UserID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete user", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteChannelsOfUser(ctx, u.UserID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete channels", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteClientsOfUser(ctx, u.UserID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete clients", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteDeliveriesOfUser(ctx, u.UserID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete deliveries", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteKeyTokensOfUser(ctx, u.UserID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete tokens", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteMessagesOfUserBySenderUserID(ctx, u.UserID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete messages", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteMessagesOfUserByChannelOwnerUserID(ctx, u.UserID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete messages", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteSubscriptionsOfUserBySubscriber(ctx, u.UserID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete subscriptions", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.Primary.DeleteSubscriptionsOfUserByChannelOwner(ctx, u.UserID)
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete subscriptions", err)
|
||||
}
|
||||
|
||||
return finishSuccess(ginext.JSON(http.StatusOK, user.PreMarshal()))
|
||||
})
|
||||
}
|
||||
|
@ -125,6 +125,7 @@ func (r *Router) Init(e *ginext.GinWrapper) error {
|
||||
apiv2.POST("/users").Handle(r.apiHandler.CreateUser)
|
||||
apiv2.GET("/users/:uid").Handle(r.apiHandler.GetUser)
|
||||
apiv2.PATCH("/users/:uid").Handle(r.apiHandler.UpdateUser)
|
||||
apiv2.DELETE("/users/:uid").Handle(r.apiHandler.DeleteUser)
|
||||
|
||||
apiv2.GET("/users/:uid/keys").Handle(r.apiHandler.ListUserKeys)
|
||||
apiv2.POST("/users/:uid/keys").Handle(r.apiHandler.CreateUserKey)
|
||||
@ -143,6 +144,7 @@ func (r *Router) Init(e *ginext.GinWrapper) error {
|
||||
apiv2.POST("/users/:uid/channels").Handle(r.apiHandler.CreateChannel)
|
||||
apiv2.GET("/users/:uid/channels/:cid").Handle(r.apiHandler.GetChannel)
|
||||
apiv2.PATCH("/users/:uid/channels/:cid").Handle(r.apiHandler.UpdateChannel)
|
||||
apiv2.DELETE("/users/:uid/channels/:cid").Handle(r.apiHandler.DeleteChannel)
|
||||
apiv2.GET("/users/:uid/channels/:cid/messages").Handle(r.apiHandler.ListChannelMessages)
|
||||
apiv2.GET("/users/:uid/channels/:cid/subscriptions").Handle(r.apiHandler.ListChannelSubscriptions)
|
||||
|
||||
|
@ -182,7 +182,7 @@ func (db *Database) migrateSingle(tctx *simplectx.SimpleContext, tx sq.Tx, schem
|
||||
if schemaFrom == schemaTo-1 {
|
||||
|
||||
migSQL := db.schema[schemaTo].MigScript
|
||||
if migSQL == "" {
|
||||
if len(migSQL) == 0 {
|
||||
return exerr.New(exerr.TypeInternal, fmt.Sprintf("missing %s migration from %d to %d", db.name, schemaFrom, schemaTo)).Build()
|
||||
}
|
||||
|
||||
@ -192,7 +192,7 @@ func (db *Database) migrateSingle(tctx *simplectx.SimpleContext, tx sq.Tx, schem
|
||||
return exerr.New(exerr.TypeInternal, fmt.Sprintf("missing %s migration from %d to %d", db.name, schemaFrom, schemaTo)).Build()
|
||||
}
|
||||
|
||||
func (db *Database) migrateBySQL(tctx *simplectx.SimpleContext, tx sq.Tx, stmts string, currSchemaVers int, resultSchemVers int, resultHash string, post func(tctx *simplectx.SimpleContext, tx sq.Tx) error) error {
|
||||
func (db *Database) migrateBySQL(tctx *simplectx.SimpleContext, tx sq.Tx, stmts []string, currSchemaVers int, resultSchemVers int, resultHash string, post func(tctx *simplectx.SimpleContext, tx sq.Tx) error) error {
|
||||
|
||||
schemaHashMeta, err := db.ReadMetaString(tctx, "schema_hash")
|
||||
if err != nil {
|
||||
@ -215,9 +215,13 @@ func (db *Database) migrateBySQL(tctx *simplectx.SimpleContext, tx sq.Tx, stmts
|
||||
|
||||
log.Info().Msgf("Upgrade schema from %d -> %d", currSchemaVers, resultSchemVers)
|
||||
|
||||
_, err = tx.Exec(tctx, stmts, sq.PP{})
|
||||
if err != nil {
|
||||
return err
|
||||
for i, stmt := range stmts {
|
||||
log.Info().Msgf("SQL-Migration of [%s]: %d/%d", db.name, i+1, len(stmts))
|
||||
|
||||
_, err := tx.Exec(tctx, stmt, sq.PP{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
schemHashDBAfter, err := sq.HashSqliteDatabase(tctx, tx)
|
||||
|
@ -13,7 +13,7 @@ func (db *Database) GetChannelByName(ctx db.TxContext, userid models.UserID, cha
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sq.QuerySingleOpt[models.Channel](ctx, tx, "SELECT * FROM channels WHERE owner_user_id = :uid AND internal_name = :nam LIMIT 1", sq.PP{"uid": userid, "nam": chanName}, sq.SModeExtended, sq.Safe)
|
||||
return sq.QuerySingleOpt[models.Channel](ctx, tx, "SELECT * FROM channels WHERE owner_user_id = :uid AND internal_name = :nam AND deleted=0 LIMIT 1", sq.PP{"uid": userid, "nam": chanName}, sq.SModeExtended, sq.Safe)
|
||||
}
|
||||
|
||||
func (db *Database) GetChannelByID(ctx db.TxContext, chanid models.ChannelID) (*models.Channel, error) {
|
||||
@ -22,7 +22,7 @@ func (db *Database) GetChannelByID(ctx db.TxContext, chanid models.ChannelID) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sq.QuerySingleOpt[models.Channel](ctx, tx, "SELECT * FROM channels WHERE channel_id = :cid LIMIT 1", sq.PP{"cid": chanid}, sq.SModeExtended, sq.Safe)
|
||||
return sq.QuerySingleOpt[models.Channel](ctx, tx, "SELECT * FROM channels WHERE channel_id = :cid AND deleted=0 LIMIT 1", sq.PP{"cid": chanid}, sq.SModeExtended, sq.Safe)
|
||||
}
|
||||
|
||||
type CreateChanel struct {
|
||||
@ -49,6 +49,7 @@ func (db *Database) CreateChannel(ctx db.TxContext, userid models.UserID, dispNa
|
||||
TimestampCreated: models.NowSCNTime(),
|
||||
TimestampLastSent: nil,
|
||||
MessagesSent: 0,
|
||||
Deleted: false,
|
||||
}
|
||||
|
||||
_, err = sq.InsertSingle(ctx, tx, "channels", entity)
|
||||
@ -67,7 +68,7 @@ func (db *Database) ListChannelsByOwner(ctx db.TxContext, userid models.UserID,
|
||||
|
||||
order := " ORDER BY channels.timestamp_created ASC, channels.channel_id ASC "
|
||||
|
||||
sql := "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub ON channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE owner_user_id = :ouid" + order
|
||||
sql := "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub ON channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE channels.deleted=0 AND owner_user_id = :ouid" + order
|
||||
|
||||
pp := sq.PP{
|
||||
"ouid": userid,
|
||||
@ -90,9 +91,11 @@ func (db *Database) ListChannelsBySubscriber(ctx db.TxContext, userid models.Use
|
||||
confCond = " AND sub.confirmed = 0"
|
||||
}
|
||||
|
||||
cond := "channels.deleted=0 AND sub.subscription_id IS NOT NULL " + confCond
|
||||
|
||||
order := " ORDER BY channels.timestamp_created ASC, channels.channel_id ASC "
|
||||
|
||||
sql := "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE sub.subscription_id IS NOT NULL " + confCond + order
|
||||
sql := "SELECT channels.*, sub.* " + "FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid AND sub.deleted=0 WHERE " + cond + order
|
||||
|
||||
pp := sq.PP{
|
||||
"subuid": userid,
|
||||
@ -114,9 +117,11 @@ func (db *Database) ListChannelsByAccess(ctx db.TxContext, userid models.UserID,
|
||||
confCond = "OR (sub.subscription_id IS NOT NULL AND sub.confirmed = 0)"
|
||||
}
|
||||
|
||||
cond := "channels.deleted=0 AND (owner_user_id = :ouid " + confCond + ")"
|
||||
|
||||
order := " ORDER BY channels.timestamp_created ASC, channels.channel_id ASC "
|
||||
|
||||
sql := "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE owner_user_id = :ouid " + confCond + order
|
||||
sql := "SELECT channels.*, sub.* " + "FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid AND sub.deleted=0 WHERE " + cond + order
|
||||
|
||||
pp := sq.PP{
|
||||
"ouid": userid,
|
||||
@ -139,13 +144,14 @@ func (db *Database) GetChannel(ctx db.TxContext, userid models.UserID, channelid
|
||||
|
||||
selectors := "channels.*, sub.*"
|
||||
|
||||
join := "LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid"
|
||||
join := "LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid AND sub.deleted=0"
|
||||
|
||||
cond := "channels.channel_id = :cid"
|
||||
if enforceOwner {
|
||||
cond = "owner_user_id = :ouid AND channels.channel_id = :cid"
|
||||
params["ouid"] = userid
|
||||
}
|
||||
cond += " AND channels.deleted=0"
|
||||
|
||||
sql := "SELECT " + selectors + " FROM channels " + join + " WHERE " + cond + " LIMIT 1"
|
||||
|
||||
@ -160,7 +166,7 @@ func (db *Database) IncChannelMessageCounter(ctx db.TxContext, channel *models.C
|
||||
|
||||
now := time.Now()
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE channels SET messages_sent = messages_sent+1, timestamp_lastsent = :ts WHERE channel_id = :cid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE channels SET messages_sent = messages_sent+1, timestamp_lastsent = :ts WHERE channel_id = :cid AND deleted=0", sq.PP{
|
||||
"ts": time2DB(now),
|
||||
"cid": channel.ChannelID,
|
||||
})
|
||||
@ -180,7 +186,7 @@ func (db *Database) UpdateChannelSubscribeKey(ctx db.TxContext, channelid models
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE channels SET subscribe_key = :key WHERE channel_id = :cid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE channels SET subscribe_key = :key WHERE channel_id = :cid AND deleted=0", sq.PP{
|
||||
"key": newkey,
|
||||
"cid": channelid,
|
||||
})
|
||||
@ -197,7 +203,7 @@ func (db *Database) UpdateChannelDisplayName(ctx db.TxContext, channelid models.
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE channels SET display_name = :nam WHERE channel_id = :cid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE channels SET display_name = :nam WHERE channel_id = :cid AND deleted=0", sq.PP{
|
||||
"nam": dispname,
|
||||
"cid": channelid,
|
||||
})
|
||||
@ -214,7 +220,7 @@ func (db *Database) UpdateChannelDescriptionName(ctx db.TxContext, channelid mod
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE channels SET description_name = :nam WHERE channel_id = :cid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE channels SET description_name = :nam WHERE channel_id = :cid AND deleted=0", sq.PP{
|
||||
"nam": descname,
|
||||
"cid": channelid,
|
||||
})
|
||||
@ -224,3 +230,31 @@ func (db *Database) UpdateChannelDescriptionName(ctx db.TxContext, channelid mod
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteChannelsOfUser(ctx db.TxContext, userid models.UserID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE channels SET deleted=1 WHERE owner_user_id = :uid AND deleted=0", sq.PP{"uid": userid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteChannel(ctx db.TxContext, channelid models.ChannelID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE channels SET deleted=1 WHERE channel_id = :cid AND deleted=0", sq.PP{"cid": channelid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ func (db *Database) CreateClient(ctx db.TxContext, userid models.UserID, ctype m
|
||||
AgentModel: agentModel,
|
||||
AgentVersion: agentVersion,
|
||||
Name: name,
|
||||
Deleted: false,
|
||||
}
|
||||
|
||||
_, err = sq.InsertSingle(ctx, tx, "clients", entity)
|
||||
@ -159,3 +160,17 @@ func (db *Database) UpdateClientDescriptionName(ctx db.TxContext, clientid model
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteClientsOfUser(ctx db.TxContext, userid models.UserID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE clients SET deleted=1 WHERE user_id = :uid AND deleted=0", sq.PP{"uid": userid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -182,7 +182,7 @@ func (db *Database) migrateSingle(tctx *simplectx.SimpleContext, tx sq.Tx, schem
|
||||
if schemaFrom == schemaTo-1 {
|
||||
|
||||
migSQL := db.schema[schemaTo].MigScript
|
||||
if migSQL == "" {
|
||||
if len(migSQL) == 0 {
|
||||
return exerr.New(exerr.TypeInternal, fmt.Sprintf("missing %s migration from %d to %d", db.name, schemaFrom, schemaTo)).Build()
|
||||
}
|
||||
|
||||
@ -192,7 +192,7 @@ func (db *Database) migrateSingle(tctx *simplectx.SimpleContext, tx sq.Tx, schem
|
||||
return exerr.New(exerr.TypeInternal, fmt.Sprintf("missing %s migration from %d to %d", db.name, schemaFrom, schemaTo)).Build()
|
||||
}
|
||||
|
||||
func (db *Database) migrateBySQL(tctx *simplectx.SimpleContext, tx sq.Tx, stmts string, currSchemaVers int, resultSchemVers int, resultHash string, post func(tctx *simplectx.SimpleContext, tx sq.Tx) error) error {
|
||||
func (db *Database) migrateBySQL(tctx *simplectx.SimpleContext, tx sq.Tx, stmts []string, currSchemaVers int, resultSchemVers int, resultHash string, post func(tctx *simplectx.SimpleContext, tx sq.Tx) error) error {
|
||||
|
||||
schemaHashMeta, err := db.ReadMetaString(tctx, "schema_hash")
|
||||
if err != nil {
|
||||
@ -215,9 +215,13 @@ func (db *Database) migrateBySQL(tctx *simplectx.SimpleContext, tx sq.Tx, stmts
|
||||
|
||||
log.Info().Msgf("Upgrade schema from %d -> %d", currSchemaVers, resultSchemVers)
|
||||
|
||||
_, err = tx.Exec(tctx, stmts, sq.PP{})
|
||||
if err != nil {
|
||||
return err
|
||||
for i, stmt := range stmts {
|
||||
log.Info().Msgf("SQL-Migration of [%s]: %d/%d", db.name, i+1, len(stmts))
|
||||
|
||||
_, err := tx.Exec(tctx, stmt, sq.PP{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
schemHashDBAfter, err := sq.HashSqliteDatabase(tctx, tx)
|
||||
|
@ -29,6 +29,7 @@ func (db *Database) CreateRetryDelivery(ctx db.TxContext, client models.Client,
|
||||
RetryCount: 0,
|
||||
NextDelivery: models.NewSCNTimePtr(&next),
|
||||
FCMMessageID: nil,
|
||||
Deleted: false,
|
||||
}
|
||||
|
||||
_, err = sq.InsertSingle(ctx, tx, "deliveries", entity)
|
||||
@ -58,6 +59,7 @@ func (db *Database) CreateSuccessDelivery(ctx db.TxContext, client models.Client
|
||||
RetryCount: 0,
|
||||
NextDelivery: nil,
|
||||
FCMMessageID: langext.Ptr(fcmDelivID),
|
||||
Deleted: false,
|
||||
}
|
||||
|
||||
_, err = sq.InsertSingle(ctx, tx, "deliveries", entity)
|
||||
@ -74,7 +76,7 @@ func (db *Database) ListRetrieableDeliveries(ctx db.TxContext, pageSize int) ([]
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sq.QueryAll[models.Delivery](ctx, tx, "SELECT * FROM deliveries WHERE status = 'RETRY' AND next_delivery < :next ORDER BY next_delivery ASC LIMIT :lim", sq.PP{
|
||||
return sq.QueryAll[models.Delivery](ctx, tx, "SELECT * FROM deliveries WHERE status = 'RETRY' AND next_delivery < :next AND deleted=0 ORDER BY next_delivery ASC LIMIT :lim", sq.PP{
|
||||
"next": time2DB(time.Now()),
|
||||
"lim": pageSize,
|
||||
}, sq.SModeExtended, sq.Safe)
|
||||
@ -86,7 +88,7 @@ func (db *Database) SetDeliverySuccess(ctx db.TxContext, delivery models.Deliver
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE deliveries SET status = 'SUCCESS', next_delivery = NULL, retry_count = :rc, timestamp_finalized = :ts, fcm_message_id = :fcm WHERE delivery_id = :did", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE deliveries SET status = 'SUCCESS', next_delivery = NULL, retry_count = :rc, timestamp_finalized = :ts, fcm_message_id = :fcm WHERE delivery_id = :did AND deleted=0", sq.PP{
|
||||
"rc": delivery.RetryCount + 1,
|
||||
"ts": time2DB(time.Now()),
|
||||
"fcm": fcmDelivID,
|
||||
@ -105,7 +107,7 @@ func (db *Database) SetDeliveryFailed(ctx db.TxContext, delivery models.Delivery
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE deliveries SET status = 'FAILED', next_delivery = NULL, retry_count = :rc, timestamp_finalized = :ts WHERE delivery_id = :did",
|
||||
_, err = tx.Exec(ctx, "UPDATE deliveries SET status = 'FAILED', next_delivery = NULL, retry_count = :rc, timestamp_finalized = :ts WHERE delivery_id = :did AND deleted=0",
|
||||
sq.PP{
|
||||
"rc": delivery.RetryCount + 1,
|
||||
"ts": time2DB(time.Now()),
|
||||
@ -124,7 +126,7 @@ func (db *Database) SetDeliveryRetry(ctx db.TxContext, delivery models.Delivery)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE deliveries SET status = 'RETRY', next_delivery = :next, retry_count = :rc WHERE delivery_id = :did", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE deliveries SET status = 'RETRY', next_delivery = :next, retry_count = :rc WHERE delivery_id = :did AND deleted=0", sq.PP{
|
||||
"next": time2DB(scn.NextDeliveryTimestamp(time.Now())),
|
||||
"rc": delivery.RetryCount + 1,
|
||||
"did": delivery.DeliveryID,
|
||||
@ -142,7 +144,7 @@ func (db *Database) CancelPendingDeliveries(ctx db.TxContext, messageID models.M
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE deliveries SET status = 'FAILED', next_delivery = NULL, timestamp_finalized = :ts WHERE message_id = :mid AND status = 'RETRY'", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE deliveries SET status = 'FAILED', next_delivery = NULL, timestamp_finalized = :ts WHERE message_id = :mid AND status = 'RETRY' AND deleted=0", sq.PP{
|
||||
"ts": time.Now(),
|
||||
"mid": messageID,
|
||||
})
|
||||
@ -152,3 +154,31 @@ func (db *Database) CancelPendingDeliveries(ctx db.TxContext, messageID models.M
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteDeliveriesOfUser(ctx db.TxContext, userid models.UserID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE deliveries SET deleted=1 WHERE receiver_user_id = :uid AND deleted=0", sq.PP{"uid": userid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteDeliveriesOfChannel(ctx db.TxContext, channelid models.ChannelID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE deliveries SET deleted=1 WHERE (SELECT channel_id FROM messages WHERE messages.message_id = deliveries.message_id) = :cid AND deleted=0", sq.PP{"cid": channelid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ func (db *Database) CreateKeyToken(ctx db.TxContext, name string, owner models.U
|
||||
Token: token,
|
||||
Permissions: permissions,
|
||||
MessagesSent: 0,
|
||||
Deleted: false,
|
||||
}
|
||||
|
||||
_, err = sq.InsertSingle(ctx, tx, "keytokens", entity)
|
||||
@ -42,7 +43,7 @@ func (db *Database) ListKeyTokens(ctx db.TxContext, ownerID models.UserID) ([]mo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sq.QueryAll[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE owner_user_id = :uid ORDER BY keytokens.timestamp_created DESC, keytokens.keytoken_id ASC", sq.PP{"uid": ownerID}, sq.SModeExtended, sq.Safe)
|
||||
return sq.QueryAll[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE owner_user_id = :uid AND deleted=0 ORDER BY keytokens.timestamp_created DESC, keytokens.keytoken_id ASC", sq.PP{"uid": ownerID}, sq.SModeExtended, sq.Safe)
|
||||
}
|
||||
|
||||
func (db *Database) GetKeyToken(ctx db.TxContext, userid models.UserID, keyTokenid models.KeyTokenID) (models.KeyToken, error) {
|
||||
@ -51,7 +52,7 @@ func (db *Database) GetKeyToken(ctx db.TxContext, userid models.UserID, keyToken
|
||||
return models.KeyToken{}, err
|
||||
}
|
||||
|
||||
return sq.QuerySingle[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE owner_user_id = :uid AND keytoken_id = :cid LIMIT 1", sq.PP{
|
||||
return sq.QuerySingle[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE owner_user_id = :uid AND keytoken_id = :cid AND deleted=0 LIMIT 1", sq.PP{
|
||||
"uid": userid,
|
||||
"cid": keyTokenid,
|
||||
}, sq.SModeExtended, sq.Safe)
|
||||
@ -63,7 +64,7 @@ func (db *Database) GetKeyTokenByID(ctx db.TxContext, keyTokenid models.KeyToken
|
||||
return models.KeyToken{}, err
|
||||
}
|
||||
|
||||
return sq.QuerySingle[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE keytoken_id = :cid LIMIT 1", sq.PP{"cid": keyTokenid}, sq.SModeExtended, sq.Safe)
|
||||
return sq.QuerySingle[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE keytoken_id = :cid AND deleted=0 LIMIT 1", sq.PP{"cid": keyTokenid}, sq.SModeExtended, sq.Safe)
|
||||
}
|
||||
|
||||
func (db *Database) GetKeyTokenByToken(ctx db.TxContext, key string) (*models.KeyToken, error) {
|
||||
@ -72,7 +73,7 @@ func (db *Database) GetKeyTokenByToken(ctx db.TxContext, key string) (*models.Ke
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sq.QuerySingleOpt[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE token = :key LIMIT 1", sq.PP{"key": key}, sq.SModeExtended, sq.Safe)
|
||||
return sq.QuerySingleOpt[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE token = :key AND deleted=0 LIMIT 1", sq.PP{"key": key}, sq.SModeExtended, sq.Safe)
|
||||
}
|
||||
|
||||
func (db *Database) DeleteKeyToken(ctx db.TxContext, keyTokenid models.KeyTokenID) error {
|
||||
@ -81,7 +82,7 @@ func (db *Database) DeleteKeyToken(ctx db.TxContext, keyTokenid models.KeyTokenI
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "DELETE FROM keytokens WHERE keytoken_id = :tid", sq.PP{"tid": keyTokenid})
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET deleted=1 WHERE keytoken_id = :tid AND deleted=0", sq.PP{"tid": keyTokenid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -95,7 +96,7 @@ func (db *Database) UpdateKeyTokenName(ctx db.TxContext, keyTokenid models.KeyTo
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET name = :nam WHERE keytoken_id = :tid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET name = :nam WHERE keytoken_id = :tid AND deleted=0", sq.PP{
|
||||
"nam": name,
|
||||
"tid": keyTokenid,
|
||||
})
|
||||
@ -112,7 +113,7 @@ func (db *Database) UpdateKeyTokenPermissions(ctx db.TxContext, keyTokenid model
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET permissions = :prm WHERE keytoken_id = :tid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET permissions = :prm WHERE keytoken_id = :tid AND deleted=0", sq.PP{
|
||||
"tid": keyTokenid,
|
||||
"prm": perm.String(),
|
||||
})
|
||||
@ -129,7 +130,7 @@ func (db *Database) UpdateKeyTokenAllChannels(ctx db.TxContext, keyTokenid model
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET all_channels = :all WHERE keytoken_id = :tid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET all_channels = :all WHERE keytoken_id = :tid AND deleted=0", sq.PP{
|
||||
"tid": keyTokenid,
|
||||
"all": bool2DB(allChannels),
|
||||
})
|
||||
@ -146,7 +147,7 @@ func (db *Database) UpdateKeyTokenChannels(ctx db.TxContext, keyTokenid models.K
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET channels = :cha WHERE keytoken_id = :tid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET channels = :cha WHERE keytoken_id = :tid AND deleted=0", sq.PP{
|
||||
"tid": keyTokenid,
|
||||
"cha": strings.Join(langext.ArrMap(channels, func(v models.ChannelID) string { return v.String() }), ";"),
|
||||
})
|
||||
@ -165,7 +166,7 @@ func (db *Database) IncKeyTokenMessageCounter(ctx db.TxContext, keyToken *models
|
||||
|
||||
now := time.Now()
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET messages_sent = messages_sent+1, timestamp_lastused = :ts WHERE keytoken_id = :tid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET messages_sent = messages_sent+1, timestamp_lastused = :ts WHERE keytoken_id = :tid AND deleted=0", sq.PP{
|
||||
"ts": time2DB(now),
|
||||
"tid": keyToken.KeyTokenID,
|
||||
})
|
||||
@ -185,7 +186,7 @@ func (db *Database) UpdateKeyTokenLastUsed(ctx db.TxContext, keyTokenid models.K
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET timestamp_lastused = :ts WHERE keytoken_id = :tid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET timestamp_lastused = :ts WHERE keytoken_id = :tid AND deleted=0", sq.PP{
|
||||
"ts": time2DB(time.Now()),
|
||||
"tid": keyTokenid,
|
||||
})
|
||||
@ -195,3 +196,46 @@ func (db *Database) UpdateKeyTokenLastUsed(ctx db.TxContext, keyTokenid models.K
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteKeyTokensOfUser(ctx db.TxContext, userid models.UserID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET deleted=1 WHERE owner_user_id = :uid AND deleted=0", sq.PP{"uid": userid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteChannelFromKeyTokens(ctx db.TxContext, channelid models.ChannelID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tok, err := sq.QueryAll[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE instr(channels, :cid) AND deleted=0", sq.PP{"cid": channelid}, sq.SModeExtended, sq.Safe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range tok {
|
||||
|
||||
newChanList := models.ChannelIDArr(langext.ArrRemove(t.Channels, channelid))
|
||||
|
||||
chanListDB, err := newChanList.MarshalToDB(newChanList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE keytokens SET channels=:clist WHERE owner_user_id = :uid AND deleted=0", sq.PP{"clist": chanListDB})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ func (db *Database) GetMessageByUserMessageID(ctx db.TxContext, usrMsgId string)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sq.QuerySingleOpt[models.Message](ctx, tx, "SELECT * FROM messages WHERE usr_message_id = :umid LIMIT 1", sq.PP{"umid": usrMsgId}, sq.SModeExtended, sq.Safe)
|
||||
return sq.QuerySingleOpt[models.Message](ctx, tx, "SELECT * FROM messages WHERE usr_message_id = :umid LIMIT 1", sq.PP{"umid": usrMsgId}, sq.SModeExtended, sq.Safe) // no deleted=0 check!
|
||||
}
|
||||
|
||||
func (db *Database) GetMessage(ctx db.TxContext, scnMessageID models.MessageID, allowDeleted bool) (models.Message, error) {
|
||||
@ -45,6 +45,7 @@ func (db *Database) CreateMessage(ctx db.TxContext, senderUserID models.UserID,
|
||||
SenderUserID: senderUserID,
|
||||
ChannelInternalName: channel.InternalName,
|
||||
ChannelID: channel.ChannelID,
|
||||
ChannelOwnerUserID: channel.OwnerUserID,
|
||||
SenderIP: senderIP,
|
||||
SenderName: senderName,
|
||||
TimestampReal: models.NowSCNTime(),
|
||||
@ -164,3 +165,45 @@ func (db *Database) CountMessages(ctx db.TxContext, filter models.MessageFilter)
|
||||
|
||||
return countRes, nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteMessagesOfUserBySenderUserID(ctx db.TxContext, userid models.UserID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE messages SET deleted=1 WHERE sender_user_id = :uid AND deleted=0", sq.PP{"uid": userid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteMessagesOfUserByChannelOwnerUserID(ctx db.TxContext, userid models.UserID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE messages SET deleted=1 WHERE channel_owner_user_id = :uid AND deleted=0", sq.PP{"uid": userid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteMessagesOfChannel(ctx db.TxContext, channelid models.ChannelID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE messages SET deleted=1 WHERE channel_id = :cid AND deleted=0", sq.PP{"cid": channelid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ func (db *Database) ListSenderNames(ctx db.TxContext, userid models.UserID, incl
|
||||
prepParams := sq.PP{"uid": userid}
|
||||
|
||||
if includeForeignSubscribed {
|
||||
sqlStr = "SELECT sender_name AS name, MAX(timestamp_real) AS ts_last, MIN(timestamp_real) AS ts_first, COUNT(*) AS count FROM messages LEFT JOIN subscriptions AS subs on messages.channel_id = subs.channel_id WHERE (subs.subscriber_user_id = :uid AND subs.confirmed = 1) AND sender_NAME NOT NULL GROUP BY sender_name ORDER BY ts_last DESC"
|
||||
sqlStr = "SELECT sender_name AS name, MAX(timestamp_real) AS ts_last, MIN(timestamp_real) AS ts_first, COUNT(*) AS count FROM messages LEFT JOIN subscriptions AS subs ON messages.channel_id = subs.channel_id AND subs.deleted=0 WHERE (subs.subscriber_user_id = :uid AND subs.confirmed = 1) AND sender_NAME NOT NULL GROUP BY sender_name ORDER BY ts_last DESC"
|
||||
} else {
|
||||
sqlStr = "SELECT sender_name AS name, MAX(timestamp_real) AS ts_last, MIN(timestamp_real) AS ts_first, COUNT(*) AS count FROM messages WHERE sender_user_id = :uid AND sender_NAME NOT NULL GROUP BY sender_name ORDER BY ts_last DESC"
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ func (db *Database) CreateSubscription(ctx db.TxContext, subscriberUID models.Us
|
||||
ChannelInternalName: channel.InternalName,
|
||||
TimestampCreated: models.NowSCNTime(),
|
||||
Confirmed: confirmed,
|
||||
Active: true,
|
||||
Deleted: false,
|
||||
}
|
||||
|
||||
_, err = sq.InsertSingle(ctx, tx, "subscriptions", entity)
|
||||
@ -40,7 +42,7 @@ func (db *Database) ListSubscriptions(ctx db.TxContext, filter models.Subscripti
|
||||
|
||||
orderClause := " ORDER BY subscriptions.timestamp_created DESC, subscriptions.subscription_id DESC "
|
||||
|
||||
sqlQuery := "SELECT " + "subscriptions.*" + " FROM subscriptions " + filterJoin + " WHERE ( " + filterCond + " ) " + orderClause
|
||||
sqlQuery := "SELECT " + "subscriptions.*" + " FROM subscriptions " + filterJoin + " WHERE ( " + filterCond + " ) AND subscriptions.deleted=0 " + orderClause
|
||||
|
||||
return sq.QueryAll[models.Subscription](ctx, tx, sqlQuery, prepParams, sq.SModeExtended, sq.Safe)
|
||||
}
|
||||
@ -51,7 +53,7 @@ func (db *Database) GetSubscription(ctx db.TxContext, subid models.SubscriptionI
|
||||
return models.Subscription{}, err
|
||||
}
|
||||
|
||||
return sq.QuerySingle[models.Subscription](ctx, tx, "SELECT * FROM subscriptions WHERE subscription_id = :sid LIMIT 1", sq.PP{"sid": subid}, sq.SModeExtended, sq.Safe)
|
||||
return sq.QuerySingle[models.Subscription](ctx, tx, "SELECT * FROM subscriptions WHERE subscription_id = :sid AND deleted=0 LIMIT 1", sq.PP{"sid": subid}, sq.SModeExtended, sq.Safe)
|
||||
}
|
||||
|
||||
func (db *Database) GetSubscriptionBySubscriber(ctx db.TxContext, subscriberId models.UserID, channelId models.ChannelID) (*models.Subscription, error) {
|
||||
@ -60,7 +62,7 @@ func (db *Database) GetSubscriptionBySubscriber(ctx db.TxContext, subscriberId m
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sq.QuerySingleOpt[models.Subscription](ctx, tx, "SELECT * FROM subscriptions WHERE subscriber_user_id = :suid AND channel_id = :cid LIMIT 1", sq.PP{
|
||||
return sq.QuerySingleOpt[models.Subscription](ctx, tx, "SELECT * FROM subscriptions WHERE subscriber_user_id = :suid AND channel_id = :cid AND deleted=0 LIMIT 1", sq.PP{
|
||||
"suid": subscriberId,
|
||||
"cid": channelId,
|
||||
}, sq.SModeExtended, sq.Safe)
|
||||
@ -72,7 +74,7 @@ func (db *Database) DeleteSubscription(ctx db.TxContext, subid models.Subscripti
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "DELETE FROM subscriptions WHERE subscription_id = :sid", sq.PP{"sid": subid})
|
||||
_, err = tx.Exec(ctx, "UPDATE subscriptions SET deleted=1 WHERE subscription_id = :sid AND deleted=0", sq.PP{"sid": subid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -86,7 +88,7 @@ func (db *Database) UpdateSubscriptionConfirmed(ctx db.TxContext, subscriptionID
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE subscriptions SET confirmed = :conf WHERE subscription_id = :sid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE subscriptions SET confirmed = :conf WHERE subscription_id = :sid AND deleted=0", sq.PP{
|
||||
"conf": confirmed,
|
||||
"sid": subscriptionID,
|
||||
})
|
||||
@ -96,3 +98,62 @@ func (db *Database) UpdateSubscriptionConfirmed(ctx db.TxContext, subscriptionID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) UpdateSubscriptionActive(ctx db.TxContext, subscriptionID models.SubscriptionID, active bool) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE subscriptions SET active = :act WHERE subscription_id = :sid AND deleted=0", sq.PP{
|
||||
"act": active,
|
||||
"sid": subscriptionID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteSubscriptionsOfUserByChannelOwner(ctx db.TxContext, userid models.UserID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE subscriptions SET deleted=1 WHERE channel_owner_user_id = :uid AND deleted=0", sq.PP{"uid": userid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteSubscriptionsOfUserBySubscriber(ctx db.TxContext, userid models.UserID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE subscriptions SET deleted=1 WHERE subscriber_user_id = :uid AND deleted=0", sq.PP{"uid": userid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteSubscriptionsByChannel(ctx db.TxContext, channelid models.ChannelID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE subscriptions SET deleted=1 WHERE channel_id = :cid AND deleted=0", sq.PP{"cid": channelid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ func (db *Database) CreateUser(ctx db.TxContext, protoken *string, username *str
|
||||
IsPro: protoken != nil,
|
||||
ProToken: protoken,
|
||||
UserExtra: models.UserExtra{},
|
||||
Deleted: false,
|
||||
}
|
||||
|
||||
entity.PreMarshal()
|
||||
@ -45,7 +46,7 @@ func (db *Database) ClearProTokens(ctx db.TxContext, protoken string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE users SET is_pro=0, pro_token=NULL WHERE pro_token = :tok", sq.PP{"tok": protoken})
|
||||
_, err = tx.Exec(ctx, "UPDATE users SET is_pro=0, pro_token=NULL WHERE pro_token = :tok AND deleted=0", sq.PP{"tok": protoken})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -59,7 +60,7 @@ func (db *Database) GetUser(ctx db.TxContext, userid models.UserID) (models.User
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
return sq.QuerySingle[models.User](ctx, tx, "SELECT * FROM users WHERE user_id = :uid LIMIT 1", sq.PP{"uid": userid}, sq.SModeExtended, sq.Safe)
|
||||
return sq.QuerySingle[models.User](ctx, tx, "SELECT * FROM users WHERE user_id = :uid AND deleted=0 LIMIT 1", sq.PP{"uid": userid}, sq.SModeExtended, sq.Safe)
|
||||
}
|
||||
|
||||
func (db *Database) GetUserOpt(ctx db.TxContext, userid models.UserID) (*models.User, error) {
|
||||
@ -68,7 +69,7 @@ func (db *Database) GetUserOpt(ctx db.TxContext, userid models.UserID) (*models.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sq.QuerySingleOpt[models.User](ctx, tx, "SELECT * FROM users WHERE user_id = :uid LIMIT 1", sq.PP{"uid": userid}, sq.SModeExtended, sq.Safe)
|
||||
return sq.QuerySingleOpt[models.User](ctx, tx, "SELECT * FROM users WHERE user_id = :uid AND deleted=0 LIMIT 1", sq.PP{"uid": userid}, sq.SModeExtended, sq.Safe)
|
||||
}
|
||||
|
||||
func (db *Database) UpdateUserUsername(ctx db.TxContext, userid models.UserID, username *string) error {
|
||||
@ -77,7 +78,7 @@ func (db *Database) UpdateUserUsername(ctx db.TxContext, userid models.UserID, u
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE users SET username = :nam WHERE user_id = :uid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE users SET username = :nam WHERE user_id = :uid AND deleted=0", sq.PP{
|
||||
"nam": username,
|
||||
"uid": userid,
|
||||
})
|
||||
@ -94,7 +95,7 @@ func (db *Database) UpdateUserProToken(ctx db.TxContext, userid models.UserID, p
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE users SET pro_token = :tok, is_pro = :pro WHERE user_id = :uid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE users SET pro_token = :tok, is_pro = :pro WHERE user_id = :uid AND deleted=0", sq.PP{
|
||||
"tok": protoken,
|
||||
"pro": bool2DB(protoken != nil),
|
||||
"uid": userid,
|
||||
@ -119,7 +120,7 @@ func (db *Database) IncUserMessageCounter(ctx db.TxContext, user *models.User) e
|
||||
user.QuotaUsed = quota
|
||||
user.QuotaUsedDay = langext.Ptr(scn.QuotaDayString())
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE users SET timestamp_lastsent = :ts, messages_sent = messages_sent+1, quota_used = :qu, quota_used_day = :qd WHERE user_id = :uid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE users SET timestamp_lastsent = :ts, messages_sent = messages_sent+1, quota_used = :qu, quota_used_day = :qd WHERE user_id = :uid AND deleted=0", sq.PP{
|
||||
"ts": time2DB(now),
|
||||
"qu": user.QuotaUsed,
|
||||
"qd": user.QuotaUsedDay,
|
||||
@ -141,7 +142,7 @@ func (db *Database) UpdateUserLastRead(ctx db.TxContext, userid models.UserID) e
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE users SET timestamp_lastread = :ts WHERE user_id = :uid", sq.PP{
|
||||
_, err = tx.Exec(ctx, "UPDATE users SET timestamp_lastread = :ts WHERE user_id = :uid AND deleted=0", sq.PP{
|
||||
"ts": time2DB(time.Now()),
|
||||
"uid": userid,
|
||||
})
|
||||
@ -151,3 +152,17 @@ func (db *Database) UpdateUserLastRead(ctx db.TxContext, userid models.UserID) e
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) DeleteUser(ctx db.TxContext, userid models.UserID) error {
|
||||
tx, err := ctx.GetOrCreateTransaction(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "UPDATE users SET deleted=1 WHERE user_id = :uid AND deleted=0", sq.PP{"uid": userid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -182,7 +182,7 @@ func (db *Database) migrateSingle(tctx *simplectx.SimpleContext, tx sq.Tx, schem
|
||||
if schemaFrom == schemaTo-1 {
|
||||
|
||||
migSQL := db.schema[schemaTo].MigScript
|
||||
if migSQL == "" {
|
||||
if len(migSQL) == 0 {
|
||||
return exerr.New(exerr.TypeInternal, fmt.Sprintf("missing %s migration from %d to %d", db.name, schemaFrom, schemaTo)).Build()
|
||||
}
|
||||
|
||||
@ -192,7 +192,7 @@ func (db *Database) migrateSingle(tctx *simplectx.SimpleContext, tx sq.Tx, schem
|
||||
return exerr.New(exerr.TypeInternal, fmt.Sprintf("missing %s migration from %d to %d", db.name, schemaFrom, schemaTo)).Build()
|
||||
}
|
||||
|
||||
func (db *Database) migrateBySQL(tctx *simplectx.SimpleContext, tx sq.Tx, stmts string, currSchemaVers int, resultSchemVers int, resultHash string, post func(tctx *simplectx.SimpleContext, tx sq.Tx) error) error {
|
||||
func (db *Database) migrateBySQL(tctx *simplectx.SimpleContext, tx sq.Tx, stmts []string, currSchemaVers int, resultSchemVers int, resultHash string, post func(tctx *simplectx.SimpleContext, tx sq.Tx) error) error {
|
||||
|
||||
schemaHashMeta, err := db.ReadMetaString(tctx, "schema_hash")
|
||||
if err != nil {
|
||||
@ -215,9 +215,13 @@ func (db *Database) migrateBySQL(tctx *simplectx.SimpleContext, tx sq.Tx, stmts
|
||||
|
||||
log.Info().Msgf("Upgrade schema from %d -> %d", currSchemaVers, resultSchemVers)
|
||||
|
||||
_, err = tx.Exec(tctx, stmts, sq.PP{})
|
||||
if err != nil {
|
||||
return err
|
||||
for i, stmt := range stmts {
|
||||
log.Info().Msgf("SQL-Migration of [%s]: %d/%d", db.name, i+1, len(stmts))
|
||||
|
||||
_, err := tx.Exec(tctx, stmt, sq.PP{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
schemHashDBAfter, err := sq.HashSqliteDatabase(tctx, tx)
|
||||
|
@ -3,12 +3,13 @@ package schema
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Def struct {
|
||||
SQL string
|
||||
Hash string
|
||||
MigScript string
|
||||
MigScript []string
|
||||
}
|
||||
|
||||
//go:embed *.ddl
|
||||
@ -16,29 +17,30 @@ type Def struct {
|
||||
var assets embed.FS
|
||||
|
||||
var PrimarySchema = map[int]Def{
|
||||
0: {"", "", ""},
|
||||
1: {readDDL("primary_1.ddl"), "f2b2847f32681a7178e405553beea4a324034915a0c5a5dc70b3c6abbcc852f2", ""},
|
||||
2: {readDDL("primary_2.ddl"), "07ed1449114416ed043084a30e0722a5f97bf172161338d2f7106a8dfd387d0a", ""},
|
||||
3: {readDDL("primary_3.ddl"), "65c2125ad0e12d02490cf2275f0067ef3c62a8522edf9a35ee8aa3f3c09b12e8", ""},
|
||||
0: {"", "", nil},
|
||||
1: {readDDL("primary_1.ddl"), "f2b2847f32681a7178e405553beea4a324034915a0c5a5dc70b3c6abbcc852f2", nil},
|
||||
2: {readDDL("primary_2.ddl"), "07ed1449114416ed043084a30e0722a5f97bf172161338d2f7106a8dfd387d0a", nil},
|
||||
3: {readDDL("primary_3.ddl"), "65c2125ad0e12d02490cf2275f0067ef3c62a8522edf9a35ee8aa3f3c09b12e8", nil},
|
||||
4: {readDDL("primary_4.ddl"), "cb022156ab0e7aea39dd0c985428c43cae7d60e41ca8e9e5a84c774b3019d2ca", readMig("primary_migration_3_4.sql")},
|
||||
5: {readDDL("primary_5.ddl"), "9d6217ba4a3503cfe090f72569367f95a413bb14e9effe49ffeabbf255bce8dd", readMig("primary_migration_4_5.sql")},
|
||||
6: {readDDL("primary_6.ddl"), "8e83d20bcd008082713f248ae8cd558335a37a37ce90bd8c86e782da640ee160", readMig("primary_migration_5_6.sql")},
|
||||
7: {readDDL("primary_7.ddl"), "90d8dbc460afe025f9b74cda5c16bb8e58b178df275223bd2531907a8d8c36c3", readMig("primary_migration_6_7.sql")},
|
||||
8: {readDDL("primary_8.ddl"), "746f6005c7a573b8816e5993ecd1d949fe2552b0134ba63bab8b4d5b2b5058ad", readMig("primary_migration_7_8.sql")},
|
||||
9: {readDDL("primary_9.ddl"), "0fd1eacb03364153b2a2096106b1a11cc48ae764db52c2896dab9725b55ed188", readMig("primary_migration_8_9.sql")},
|
||||
}
|
||||
|
||||
var PrimarySchemaVersion = len(PrimarySchema) - 1
|
||||
|
||||
var RequestsSchema = map[int]Def{
|
||||
0: {"", "", ""},
|
||||
1: {readDDL("requests_1.ddl"), "ebb0a5748b605e8215437413b738279670190ca8159b6227cfc2aa13418b41e9", ""},
|
||||
0: {"", "", nil},
|
||||
1: {readDDL("requests_1.ddl"), "ebb0a5748b605e8215437413b738279670190ca8159b6227cfc2aa13418b41e9", nil},
|
||||
}
|
||||
|
||||
var RequestsSchemaVersion = len(RequestsSchema) - 1
|
||||
|
||||
var LogsSchema = map[int]Def{
|
||||
0: {"", "", ""},
|
||||
1: {readDDL("logs_1.ddl"), "65fba477c04095effc3a8e1bb79fe7547b8e52e983f776f156266eddc4f201d7", ""},
|
||||
0: {"", "", nil},
|
||||
1: {readDDL("logs_1.ddl"), "65fba477c04095effc3a8e1bb79fe7547b8e52e983f776f156266eddc4f201d7", nil},
|
||||
}
|
||||
|
||||
var LogsSchemaVersion = len(LogsSchema) - 1
|
||||
@ -51,10 +53,51 @@ func readDDL(name string) string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func readMig(name string) string {
|
||||
func readMig(name string) []string {
|
||||
data, err := assets.ReadFile(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(data)
|
||||
return splitMigration(string(data))
|
||||
}
|
||||
|
||||
func splitMigration(input string) []string {
|
||||
arr := make([]string, 0)
|
||||
|
||||
acc := ""
|
||||
|
||||
waitForEnd := false
|
||||
|
||||
for _, line := range strings.Split(input, "\n") {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "--") {
|
||||
continue
|
||||
}
|
||||
|
||||
acc += line
|
||||
|
||||
if strings.HasSuffix(strings.TrimSpace(line), "BEGIN") || strings.HasPrefix(strings.TrimSpace(line), "BEGIN") {
|
||||
waitForEnd = true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "END") {
|
||||
waitForEnd = false
|
||||
}
|
||||
|
||||
if strings.HasSuffix(acc, ";") && !waitForEnd {
|
||||
arr = append(arr, acc)
|
||||
acc = ""
|
||||
continue
|
||||
} else {
|
||||
acc += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(acc) != "" {
|
||||
arr = append(arr, acc)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
|
249
scnserver/db/schema/primary_9.ddl
Normal file
249
scnserver/db/schema/primary_9.ddl
Normal file
@ -0,0 +1,249 @@
|
||||
CREATE TABLE users
|
||||
(
|
||||
user_id TEXT NOT NULL,
|
||||
|
||||
username TEXT NULL DEFAULT NULL,
|
||||
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
timestamp_lastread INTEGER NULL DEFAULT NULL,
|
||||
timestamp_lastsent INTEGER NULL DEFAULT NULL,
|
||||
|
||||
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||
|
||||
quota_used INTEGER NOT NULL DEFAULT '0',
|
||||
quota_used_day TEXT NULL DEFAULT NULL,
|
||||
|
||||
is_pro INTEGER CHECK(is_pro IN (0, 1)) NOT NULL DEFAULT 0,
|
||||
pro_token TEXT NULL DEFAULT NULL,
|
||||
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
|
||||
PRIMARY KEY (user_id)
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX "idx_users_protoken" ON users (pro_token) WHERE pro_token IS NOT NULL AND deleted=0;
|
||||
|
||||
|
||||
CREATE TABLE keytokens
|
||||
(
|
||||
keytoken_id TEXT NOT NULL,
|
||||
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
timestamp_lastused INTEGER NULL DEFAULT NULL,
|
||||
|
||||
name TEXT NOT NULL,
|
||||
|
||||
owner_user_id TEXT NOT NULL,
|
||||
|
||||
all_channels INTEGER CHECK(all_channels IN (0, 1)) NOT NULL,
|
||||
channels TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
permissions TEXT NOT NULL,
|
||||
|
||||
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
|
||||
PRIMARY KEY (keytoken_id)
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX "idx_keytokens_token" ON keytokens (token);
|
||||
|
||||
|
||||
CREATE TABLE clients
|
||||
(
|
||||
client_id TEXT NOT NULL,
|
||||
|
||||
user_id TEXT NOT NULL,
|
||||
type TEXT CHECK(type IN ('ANDROID','IOS','LINUX','MACOS','WINDOWS')) NOT NULL,
|
||||
fcm_token TEXT NOT NULL,
|
||||
name TEXT NULL,
|
||||
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
|
||||
agent_model TEXT NOT NULL,
|
||||
agent_version TEXT NOT NULL,
|
||||
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
|
||||
PRIMARY KEY (client_id)
|
||||
) STRICT;
|
||||
CREATE INDEX "idx_clients_userid" ON clients (user_id);
|
||||
CREATE INDEX "idx_clients_deleted" ON clients (deleted);
|
||||
CREATE UNIQUE INDEX "idx_clients_fcmtoken" ON clients (fcm_token) WHERE deleted=0;
|
||||
|
||||
|
||||
CREATE TABLE channels
|
||||
(
|
||||
channel_id TEXT NOT NULL,
|
||||
|
||||
owner_user_id TEXT NOT NULL,
|
||||
|
||||
internal_name TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
description_name TEXT NULL,
|
||||
|
||||
subscribe_key TEXT NOT NULL,
|
||||
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
timestamp_lastsent INTEGER NULL DEFAULT NULL,
|
||||
|
||||
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
|
||||
PRIMARY KEY (channel_id)
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX "idx_channels_identity" ON channels (owner_user_id, internal_name) WHERE deleted=0;
|
||||
|
||||
CREATE TABLE subscriptions
|
||||
(
|
||||
subscription_id TEXT NOT NULL,
|
||||
|
||||
subscriber_user_id TEXT NOT NULL,
|
||||
channel_owner_user_id TEXT NOT NULL,
|
||||
channel_internal_name TEXT NOT NULL,
|
||||
channel_id TEXT NOT NULL,
|
||||
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
|
||||
confirmed INTEGER CHECK(confirmed IN (0, 1)) NOT NULL,
|
||||
active INTEGER CHECK(active IN (0, 1)) NOT NULL,
|
||||
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
|
||||
PRIMARY KEY (subscription_id)
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX "idx_subscriptions_ref" ON subscriptions (subscriber_user_id, channel_owner_user_id, channel_internal_name) WHERE deleted=0;
|
||||
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
|
||||
(
|
||||
message_id TEXT NOT NULL,
|
||||
sender_user_id TEXT NOT NULL,
|
||||
channel_internal_name TEXT NOT NULL,
|
||||
channel_id TEXT NOT NULL,
|
||||
channel_owner_user_id TEXT NOT NULL,
|
||||
sender_ip TEXT NOT NULL,
|
||||
sender_name TEXT 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,
|
||||
|
||||
used_key_id TEXT NOT NULL,
|
||||
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
|
||||
PRIMARY KEY (message_id)
|
||||
) STRICT;
|
||||
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 (sender_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);
|
||||
CREATE INDEX "idx_messages_sendername_nc" ON messages (sender_name COLLATE NOCASE);
|
||||
CREATE INDEX "idx_messages_title" ON messages (title COLLATE BINARY);
|
||||
CREATE INDEX "idx_messages_title_nc" ON messages (title COLLATE NOCASE);
|
||||
CREATE INDEX "idx_messages_usedkey" ON messages (sender_user_id, used_key_id);
|
||||
CREATE INDEX "idx_messages_deleted" ON messages (deleted);
|
||||
|
||||
|
||||
CREATE VIRTUAL TABLE messages_fts USING fts5
|
||||
(
|
||||
channel_internal_name,
|
||||
sender_name,
|
||||
title,
|
||||
content,
|
||||
|
||||
tokenize = unicode61,
|
||||
content = 'messages',
|
||||
content_rowid = 'rowid'
|
||||
);
|
||||
|
||||
CREATE TRIGGER fts_insert AFTER INSERT ON messages BEGIN
|
||||
INSERT INTO messages_fts (rowid, channel_internal_name, sender_name, title, content) VALUES (new.rowid, 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_internal_name, sender_name, title, content) VALUES ('delete', old.rowid, 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.rowid, 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_internal_name, sender_name, title, content) VALUES ('delete', old.rowid, old.channel_internal_name, old.sender_name, old.title, old.content);
|
||||
END;
|
||||
|
||||
|
||||
CREATE TABLE deliveries
|
||||
(
|
||||
delivery_id TEXT NOT NULL,
|
||||
|
||||
message_id TEXT NOT NULL,
|
||||
receiver_user_id TEXT NOT NULL,
|
||||
receiver_client_id TEXT NOT NULL,
|
||||
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
timestamp_finalized INTEGER NULL,
|
||||
|
||||
|
||||
status TEXT CHECK(status IN ('RETRY','SUCCESS','FAILED')) NOT NULL,
|
||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||
next_delivery INTEGER NULL DEFAULT NULL,
|
||||
|
||||
fcm_message_id TEXT NULL,
|
||||
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
|
||||
PRIMARY KEY (delivery_id)
|
||||
) STRICT;
|
||||
CREATE INDEX "idx_deliveries_receiver" ON deliveries (message_id, receiver_client_id);
|
||||
|
||||
|
||||
CREATE TABLE compat_ids
|
||||
(
|
||||
old INTEGER NOT NULL,
|
||||
new TEXT NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX "idx_compatids_new" ON compat_ids (new);
|
||||
CREATE UNIQUE INDEX "idx_compatids_old" ON compat_ids (old, type);
|
||||
|
||||
|
||||
CREATE TABLE compat_acks
|
||||
(
|
||||
user_id TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE INDEX "idx_compatacks_userid" ON compat_acks (user_id);
|
||||
CREATE UNIQUE INDEX "idx_compatacks_messageid" ON compat_acks (message_id);
|
||||
CREATE UNIQUE INDEX "idx_compatacks_userid_messageid" ON compat_acks (user_id, message_id);
|
||||
|
||||
|
||||
CREATE TABLE compat_clients
|
||||
(
|
||||
client_id TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX "idx_compatclient_clientid" ON compat_clients (client_id);
|
||||
|
||||
|
||||
CREATE TABLE `meta`
|
||||
(
|
||||
meta_key TEXT NOT NULL,
|
||||
value_int INTEGER NULL,
|
||||
value_txt TEXT NULL,
|
||||
value_real REAL NULL,
|
||||
value_blob BLOB NULL,
|
||||
|
||||
PRIMARY KEY (meta_key)
|
||||
) STRICT;
|
||||
|
||||
|
||||
INSERT INTO meta (meta_key, value_int) VALUES ('schema', 3)
|
320
scnserver/db/schema/primary_migration_8_9.sql
Normal file
320
scnserver/db/schema/primary_migration_8_9.sql
Normal file
@ -0,0 +1,320 @@
|
||||
|
||||
-- Add deleted to channels ( migrate existing as '0' )
|
||||
-- Add deleted to keytokens ( migrate existing as '0' )
|
||||
-- Add deleted to subscriptions ( migrate existing as '0' )
|
||||
-- Add deleted to users ( migrate existing as '0' )
|
||||
-- Add deleted to deliveries ( migrate existing as '0' )
|
||||
--
|
||||
-- Add active to subcsriptions ( migrate existing as '1' )
|
||||
--
|
||||
-- Add channel_owner_id to messages ( migrate existing by looking up channel )
|
||||
--
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
DROP INDEX "idx_users_protoken";
|
||||
|
||||
CREATE TABLE __new_users
|
||||
(
|
||||
user_id TEXT NOT NULL,
|
||||
username TEXT NULL DEFAULT NULL,
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
timestamp_lastread INTEGER NULL DEFAULT NULL,
|
||||
timestamp_lastsent INTEGER NULL DEFAULT NULL,
|
||||
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||
quota_used INTEGER NOT NULL DEFAULT '0',
|
||||
quota_used_day TEXT NULL DEFAULT NULL,
|
||||
is_pro INTEGER CHECK(is_pro IN (0, 1)) NOT NULL DEFAULT 0,
|
||||
pro_token TEXT NULL DEFAULT NULL,
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (user_id)
|
||||
) STRICT;
|
||||
|
||||
INSERT INTO __new_users
|
||||
SELECT
|
||||
user_id,
|
||||
username,
|
||||
timestamp_created,
|
||||
timestamp_lastread,
|
||||
timestamp_lastsent,
|
||||
messages_sent,
|
||||
quota_used,
|
||||
quota_used_day,
|
||||
is_pro,
|
||||
pro_token,
|
||||
0 AS deleted
|
||||
FROM users;
|
||||
|
||||
DROP TABLE users;
|
||||
|
||||
ALTER TABLE __new_users RENAME TO users;
|
||||
|
||||
CREATE UNIQUE INDEX "idx_users_protoken" ON users (pro_token) WHERE pro_token IS NOT NULL AND deleted=0;
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
DROP INDEX "idx_keytokens_token";
|
||||
|
||||
CREATE TABLE __new_keytokens
|
||||
(
|
||||
keytoken_id TEXT NOT NULL,
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
timestamp_lastused INTEGER NULL DEFAULT NULL,
|
||||
name TEXT NOT NULL,
|
||||
owner_user_id TEXT NOT NULL,
|
||||
all_channels INTEGER CHECK(all_channels IN (0, 1)) NOT NULL,
|
||||
channels TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
permissions TEXT NOT NULL,
|
||||
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (keytoken_id)
|
||||
) STRICT;
|
||||
|
||||
INSERT INTO __new_keytokens
|
||||
SELECT
|
||||
keytoken_id,
|
||||
timestamp_created,
|
||||
timestamp_lastused,
|
||||
name,
|
||||
owner_user_id,
|
||||
all_channels,
|
||||
channels,
|
||||
token,
|
||||
permissions,
|
||||
messages_sent,
|
||||
0 AS deleted
|
||||
FROM keytokens;
|
||||
|
||||
DROP TABLE keytokens;
|
||||
|
||||
ALTER TABLE __new_keytokens RENAME TO keytokens;
|
||||
|
||||
CREATE UNIQUE INDEX "idx_keytokens_token" ON keytokens (token);
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
DROP INDEX "idx_deliveries_receiver";
|
||||
|
||||
CREATE TABLE __new_deliveries
|
||||
(
|
||||
delivery_id TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
receiver_user_id TEXT NOT NULL,
|
||||
receiver_client_id TEXT NOT NULL,
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
timestamp_finalized INTEGER NULL,
|
||||
status TEXT CHECK(status IN ('RETRY','SUCCESS','FAILED')) NOT NULL,
|
||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||
next_delivery INTEGER NULL DEFAULT NULL,
|
||||
fcm_message_id TEXT NULL,
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (delivery_id)
|
||||
) STRICT;
|
||||
|
||||
INSERT INTO __new_deliveries
|
||||
SELECT delivery_id,
|
||||
message_id,
|
||||
receiver_user_id,
|
||||
receiver_client_id,
|
||||
timestamp_created,
|
||||
timestamp_finalized,
|
||||
status,
|
||||
retry_count,
|
||||
next_delivery,
|
||||
fcm_message_id,
|
||||
0 as deleted
|
||||
FROM deliveries;
|
||||
|
||||
DROP TABLE deliveries;
|
||||
|
||||
ALTER TABLE __new_deliveries RENAME TO deliveries;
|
||||
|
||||
CREATE INDEX "idx_deliveries_receiver" ON deliveries (message_id, receiver_client_id);
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
DROP INDEX "idx_messages_channel";
|
||||
DROP INDEX "idx_messages_channel_nc";
|
||||
DROP INDEX "idx_messages_idempotency";
|
||||
DROP INDEX "idx_messages_senderip";
|
||||
DROP INDEX "idx_messages_sendername";
|
||||
DROP INDEX "idx_messages_sendername_nc";
|
||||
DROP INDEX "idx_messages_title";
|
||||
DROP INDEX "idx_messages_title_nc";
|
||||
DROP INDEX "idx_messages_usedkey";
|
||||
DROP INDEX "idx_messages_deleted";
|
||||
|
||||
CREATE TABLE __new_messages
|
||||
(
|
||||
message_id TEXT NOT NULL,
|
||||
sender_user_id TEXT NOT NULL,
|
||||
channel_internal_name TEXT NOT NULL,
|
||||
channel_id TEXT NOT NULL,
|
||||
channel_owner_user_id TEXT NOT NULL,
|
||||
sender_ip TEXT NOT NULL,
|
||||
sender_name TEXT 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,
|
||||
used_key_id TEXT NOT NULL,
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (message_id)
|
||||
) STRICT;
|
||||
|
||||
INSERT INTO __new_messages
|
||||
SELECT
|
||||
m.message_id,
|
||||
m.sender_user_id,
|
||||
m.channel_internal_name,
|
||||
m.channel_id,
|
||||
c.owner_user_id,
|
||||
m.sender_ip,
|
||||
m.sender_name,
|
||||
m.timestamp_real,
|
||||
m.timestamp_client,
|
||||
m.title,
|
||||
m.content,
|
||||
m.priority,
|
||||
m.usr_message_id,
|
||||
m.used_key_id,
|
||||
m.deleted
|
||||
FROM messages m
|
||||
JOIN channels c ON m.channel_id = c.channel_id;
|
||||
|
||||
DROP TABLE messages;
|
||||
|
||||
ALTER TABLE __new_messages RENAME TO messages;
|
||||
|
||||
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 (sender_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);
|
||||
CREATE INDEX "idx_messages_sendername_nc" ON messages (sender_name COLLATE NOCASE);
|
||||
CREATE INDEX "idx_messages_title" ON messages (title COLLATE BINARY);
|
||||
CREATE INDEX "idx_messages_title_nc" ON messages (title COLLATE NOCASE);
|
||||
CREATE INDEX "idx_messages_usedkey" ON messages (sender_user_id, used_key_id);
|
||||
CREATE INDEX "idx_messages_deleted" ON messages (deleted);
|
||||
|
||||
DROP TRIGGER IF EXISTS fts_insert;
|
||||
DROP TRIGGER IF EXISTS fts_update;
|
||||
DROP TRIGGER IF EXISTS fts_delete;
|
||||
DROP TABLE IF EXISTS messages_fts;
|
||||
|
||||
CREATE VIRTUAL TABLE messages_fts USING fts5
|
||||
(
|
||||
channel_internal_name,
|
||||
sender_name,
|
||||
title,
|
||||
content,
|
||||
|
||||
tokenize = unicode61,
|
||||
content = 'messages',
|
||||
content_rowid = 'rowid'
|
||||
);
|
||||
|
||||
CREATE TRIGGER fts_insert AFTER INSERT ON messages BEGIN
|
||||
INSERT INTO messages_fts (rowid, channel_internal_name, sender_name, title, content) VALUES (new.rowid, 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_internal_name, sender_name, title, content) VALUES ('delete', old.rowid, 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.rowid, 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_internal_name, sender_name, title, content) VALUES ('delete', old.rowid, old.channel_internal_name, old.sender_name, old.title, old.content);
|
||||
END;
|
||||
|
||||
INSERT INTO messages_fts (rowid, channel_internal_name, sender_name, title, content)
|
||||
SELECT rowid, channel_internal_name, sender_name, title, content FROM messages;
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
DROP INDEX "idx_channels_identity";
|
||||
|
||||
CREATE TABLE __new_channels
|
||||
(
|
||||
channel_id TEXT NOT NULL,
|
||||
owner_user_id TEXT NOT NULL,
|
||||
internal_name TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
description_name TEXT NULL,
|
||||
subscribe_key TEXT NOT NULL,
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
timestamp_lastsent INTEGER NULL DEFAULT NULL,
|
||||
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (channel_id)
|
||||
) STRICT;
|
||||
|
||||
INSERT INTO __new_channels
|
||||
SELECT
|
||||
channel_id,
|
||||
owner_user_id,
|
||||
internal_name,
|
||||
display_name,
|
||||
description_name,
|
||||
subscribe_key,
|
||||
timestamp_created,
|
||||
timestamp_lastsent,
|
||||
messages_sent,
|
||||
0 AS deleted
|
||||
FROM channels;
|
||||
|
||||
DROP TABLE channels;
|
||||
ALTER TABLE __new_channels RENAME TO channels;
|
||||
|
||||
CREATE UNIQUE INDEX "idx_channels_identity" ON channels (owner_user_id, internal_name) WHERE deleted=0;
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
DROP INDEX "idx_subscriptions_ref";
|
||||
DROP INDEX "idx_subscriptions_chan";
|
||||
DROP INDEX "idx_subscriptions_subuser";
|
||||
DROP INDEX "idx_subscriptions_ownuser";
|
||||
DROP INDEX "idx_subscriptions_tsc";
|
||||
DROP INDEX "idx_subscriptions_conf";
|
||||
|
||||
CREATE TABLE __new_subscriptions
|
||||
(
|
||||
subscription_id TEXT NOT NULL,
|
||||
subscriber_user_id TEXT NOT NULL,
|
||||
channel_owner_user_id TEXT NOT NULL,
|
||||
channel_internal_name TEXT NOT NULL,
|
||||
channel_id TEXT NOT NULL,
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
confirmed INTEGER CHECK(confirmed IN (0, 1)) NOT NULL,
|
||||
active INTEGER CHECK(active IN (0, 1)) NOT NULL,
|
||||
deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (subscription_id)
|
||||
) STRICT;
|
||||
|
||||
INSERT INTO __new_subscriptions
|
||||
SELECT
|
||||
subscription_id,
|
||||
subscriber_user_id,
|
||||
channel_owner_user_id,
|
||||
channel_internal_name,
|
||||
channel_id,
|
||||
timestamp_created,
|
||||
confirmed,
|
||||
1 AS active,
|
||||
0 AS deleted
|
||||
FROM subscriptions;
|
||||
|
||||
DROP TABLE subscriptions;
|
||||
ALTER TABLE __new_subscriptions RENAME TO subscriptions;
|
||||
|
||||
CREATE UNIQUE INDEX "idx_subscriptions_ref" ON subscriptions (subscriber_user_id, channel_owner_user_id, channel_internal_name) WHERE deleted=0;
|
||||
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);
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
@ -155,7 +155,7 @@ func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *mod
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create compat-id", err))
|
||||
}
|
||||
|
||||
subFilter := models.SubscriptionFilter{ChannelID: langext.Ptr([]models.ChannelID{channel.ChannelID}), Confirmed: langext.PTrue}
|
||||
subFilter := models.SubscriptionFilter{ChannelID: langext.Ptr([]models.ChannelID{channel.ChannelID}), Confirmed: langext.PTrue, Active: langext.PTrue}
|
||||
activeSubscriptions, err := app.Database.Primary.ListSubscriptions(ctx, subFilter)
|
||||
if err != nil {
|
||||
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query subscriptions", err))
|
||||
|
@ -10,6 +10,7 @@ type Channel struct {
|
||||
TimestampCreated SCNTime `db:"timestamp_created" json:"timestamp_created"`
|
||||
TimestampLastSent *SCNTime `db:"timestamp_lastsent" json:"timestamp_lastsent"`
|
||||
MessagesSent int `db:"messages_sent" json:"messages_sent"`
|
||||
Deleted bool `db:"deleted" json:"-"`
|
||||
}
|
||||
|
||||
type ChannelWithSubscription struct {
|
||||
|
@ -19,6 +19,7 @@ type Delivery struct {
|
||||
RetryCount int `db:"retry_count" json:"retry_count"`
|
||||
NextDelivery *SCNTime `db:"next_delivery" json:"next_delivery"`
|
||||
FCMMessageID *string `db:"fcm_message_id" json:"fcm_message_id"`
|
||||
Deleted bool `db:"deleted" json:"-"`
|
||||
}
|
||||
|
||||
func (d Delivery) MaxRetryCount() int {
|
||||
|
@ -5,7 +5,7 @@ package models
|
||||
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
import "gogs.mikescher.com/BlackForestBytes/goext/enums"
|
||||
|
||||
const ChecksumEnumGenerator = "a1b9c4807e1cec4ea2a8b19cd447aa4b47c13f8058a12470dff8eeec895ad8f8" // GoExtVersion: 0.0.513
|
||||
const ChecksumEnumGenerator = "7585f93c9270c25db0e367d668827c6ae89b309b6fd2b3d1aaf7f7016b07e0b8" // GoExtVersion: 0.0.513
|
||||
|
||||
// ================================ ClientType ================================
|
||||
//
|
||||
|
@ -15,7 +15,7 @@ import "reflect"
|
||||
import "regexp"
|
||||
import "strings"
|
||||
|
||||
const ChecksumCharsetIDGenerator = "a1b9c4807e1cec4ea2a8b19cd447aa4b47c13f8058a12470dff8eeec895ad8f8" // GoExtVersion: 0.0.513
|
||||
const ChecksumCharsetIDGenerator = "7585f93c9270c25db0e367d668827c6ae89b309b6fd2b3d1aaf7f7016b07e0b8" // GoExtVersion: 0.0.513
|
||||
|
||||
const idlen = 24
|
||||
|
||||
|
@ -80,6 +80,7 @@ type KeyToken struct {
|
||||
Token string `db:"token" json:"token" jsonfilter:"INCLUDE_TOKEN"`
|
||||
Permissions TokenPermissionList `db:"permissions" json:"permissions"`
|
||||
MessagesSent int `db:"messages_sent" json:"messages_sent"`
|
||||
Deleted bool `db:"deleted" json:"-"`
|
||||
}
|
||||
|
||||
type KeyTokenPreview struct {
|
||||
|
@ -13,10 +13,11 @@ const (
|
||||
|
||||
type Message struct {
|
||||
MessageID MessageID `db:"message_id" json:"message_id"`
|
||||
SenderUserID UserID `db:"sender_user_id" json:"sender_user_id"` // user that sent the message (this is also the owner of the channel that contains it)
|
||||
ChannelInternalName string `db:"channel_internal_name" json:"channel_internal_name"`
|
||||
ChannelID ChannelID `db:"channel_id" json:"channel_id"`
|
||||
SenderName *string `db:"sender_name" json:"sender_name"`
|
||||
SenderUserID UserID `db:"sender_user_id" json:"sender_user_id"` // user that sent the message (this is also the owner of the channel that contains it)
|
||||
ChannelInternalName string `db:"channel_internal_name" json:"channel_internal_name"` //
|
||||
ChannelOwnerUserID UserID `db:"channel_owner_user_id" json:"channel_owner_user_id"` // user that owns the channel
|
||||
ChannelID ChannelID `db:"channel_id" json:"channel_id"` //
|
||||
SenderName *string `db:"sender_name" json:"sender_name"` //
|
||||
SenderIP string `db:"sender_ip" json:"sender_ip"`
|
||||
TimestampReal SCNTime `db:"timestamp_real" json:"-"`
|
||||
TimestampClient *SCNTime `db:"timestamp_client" json:"-"`
|
||||
|
@ -14,60 +14,59 @@ import (
|
||||
)
|
||||
|
||||
type MessageFilter struct {
|
||||
ConfirmedSubscriptionBy *UserID
|
||||
SearchStringFTS *[]string
|
||||
SearchStringPlain *[]string
|
||||
Sender *[]UserID
|
||||
ChannelNameCS *[]string // case-sensitive
|
||||
ChannelNameCI *[]string // case-insensitive
|
||||
ChannelID *[]ChannelID
|
||||
SenderNameCS *[]string // case-sensitive
|
||||
SenderNameCI *[]string // case-insensitive
|
||||
HasSenderName *bool
|
||||
SenderIP *[]string
|
||||
TimestampCoalesce *time.Time
|
||||
TimestampCoalesceAfter *time.Time
|
||||
TimestampCoalesceBefore *time.Time
|
||||
TimestampReal *time.Time
|
||||
TimestampRealAfter *time.Time
|
||||
TimestampRealBefore *time.Time
|
||||
TimestampClient *time.Time
|
||||
TimestampClientAfter *time.Time
|
||||
TimestampClientBefore *time.Time
|
||||
TitleCS *string // case-sensitive
|
||||
TitleCI *string // case-insensitive
|
||||
Priority *[]int
|
||||
UserMessageID *[]string
|
||||
OnlyDeleted bool
|
||||
IncludeDeleted bool
|
||||
CompatAcknowledged *bool
|
||||
UsedKeyID *[]KeyTokenID
|
||||
ConfirmedAndActiveSubscriptionBy *UserID
|
||||
SearchStringFTS *[]string
|
||||
SearchStringPlain *[]string
|
||||
Sender *[]UserID
|
||||
ChannelNameCS *[]string // case-sensitive
|
||||
ChannelNameCI *[]string // case-insensitive
|
||||
ChannelID *[]ChannelID
|
||||
SenderNameCS *[]string // case-sensitive
|
||||
SenderNameCI *[]string // case-insensitive
|
||||
HasSenderName *bool
|
||||
SenderIP *[]string
|
||||
TimestampCoalesce *time.Time
|
||||
TimestampCoalesceAfter *time.Time
|
||||
TimestampCoalesceBefore *time.Time
|
||||
TimestampReal *time.Time
|
||||
TimestampRealAfter *time.Time
|
||||
TimestampRealBefore *time.Time
|
||||
TimestampClient *time.Time
|
||||
TimestampClientAfter *time.Time
|
||||
TimestampClientBefore *time.Time
|
||||
TitleCS *string // case-sensitive
|
||||
TitleCI *string // case-insensitive
|
||||
Priority *[]int
|
||||
UserMessageID *[]string
|
||||
OnlyDeleted bool
|
||||
IncludeDeleted bool
|
||||
CompatAcknowledged *bool
|
||||
UsedKeyID *[]KeyTokenID
|
||||
}
|
||||
|
||||
func (f MessageFilter) SQL() (string, string, sq.PP, error) {
|
||||
params := sq.PP{}
|
||||
|
||||
joinClause := ""
|
||||
if f.ConfirmedSubscriptionBy != nil {
|
||||
joinClause += " LEFT JOIN subscriptions AS subs on messages.channel_id = subs.channel_id "
|
||||
if f.ConfirmedAndActiveSubscriptionBy != nil {
|
||||
joinClause += fmt.Sprintf(" LEFT JOIN subscriptions AS subs ON (messages.channel_id = subs.channel_id AND subs.subscriber_user_id = :%s AND subs.confirmed=1 AND subs.active=1 AND subs.deleted=0) ", params.Add(*f.ConfirmedAndActiveSubscriptionBy))
|
||||
}
|
||||
if f.SearchStringFTS != nil {
|
||||
joinClause += " JOIN messages_fts AS mfts on (mfts.rowid = messages.rowid) "
|
||||
joinClause += " JOIN messages_fts AS mfts ON (mfts.rowid = messages.rowid) "
|
||||
}
|
||||
|
||||
sqlClauses := make([]string, 0)
|
||||
|
||||
params := sq.PP{}
|
||||
|
||||
if f.OnlyDeleted {
|
||||
sqlClauses = append(sqlClauses, "(deleted=1)")
|
||||
sqlClauses = append(sqlClauses, "(messages.deleted=1)")
|
||||
} else if f.IncludeDeleted {
|
||||
// nothing, return all
|
||||
} else {
|
||||
sqlClauses = append(sqlClauses, "(deleted=0)") // default
|
||||
sqlClauses = append(sqlClauses, "(messages.deleted=0)") // default
|
||||
}
|
||||
|
||||
if f.ConfirmedSubscriptionBy != nil {
|
||||
sqlClauses = append(sqlClauses, fmt.Sprintf("(subs.subscriber_user_id = :%s AND subs.confirmed = 1)", params.Add(*f.ConfirmedSubscriptionBy)))
|
||||
if f.ConfirmedAndActiveSubscriptionBy != nil {
|
||||
sqlClauses = append(sqlClauses, "(subs.confirmed=1 AND subs.active=1 AND subs.deleted=0)")
|
||||
}
|
||||
|
||||
if f.Sender != nil {
|
||||
|
@ -8,11 +8,13 @@ package models
|
||||
// (use keytokens for write-access)
|
||||
|
||||
type Subscription struct {
|
||||
SubscriptionID SubscriptionID `db:"subscription_id" json:"subscription_id"`
|
||||
SubscriberUserID UserID `db:"subscriber_user_id" json:"subscriber_user_id"`
|
||||
ChannelOwnerUserID UserID `db:"channel_owner_user_id" json:"channel_owner_user_id"`
|
||||
ChannelID ChannelID `db:"channel_id" json:"channel_id"`
|
||||
ChannelInternalName string `db:"channel_internal_name" json:"channel_internal_name"`
|
||||
TimestampCreated SCNTime `db:"timestamp_created" json:"timestamp_created"`
|
||||
Confirmed bool `db:"confirmed" json:"confirmed"`
|
||||
SubscriptionID SubscriptionID `db:"subscription_id" json:"subscription_id"` //
|
||||
SubscriberUserID UserID `db:"subscriber_user_id" json:"subscriber_user_id"` //
|
||||
ChannelOwnerUserID UserID `db:"channel_owner_user_id" json:"channel_owner_user_id"` //
|
||||
ChannelID ChannelID `db:"channel_id" json:"channel_id"` //
|
||||
ChannelInternalName string `db:"channel_internal_name" json:"channel_internal_name"` //
|
||||
TimestampCreated SCNTime `db:"timestamp_created" json:"timestamp_created"` //
|
||||
Confirmed bool `db:"confirmed" json:"confirmed"` // Channel-Owner confirmed subscription
|
||||
Active bool `db:"active" json:"active"` // Subscriber has activated the subscription (default)
|
||||
Deleted bool `db:"deleted" json:"-"` //
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ type SubscriptionFilter struct {
|
||||
ChannelOwnerUserID2 *[]UserID // Used to filter <ChannelOwnerUserID> again
|
||||
ChannelID *[]ChannelID
|
||||
Confirmed *bool
|
||||
Active *bool
|
||||
SubscriberIsChannelOwner *bool
|
||||
Timestamp *time.Time
|
||||
TimestampAfter *time.Time
|
||||
@ -33,6 +34,8 @@ func (f SubscriptionFilter) SQL() (string, string, sq.PP, error) {
|
||||
|
||||
params := sq.PP{}
|
||||
|
||||
sqlClauses = append(sqlClauses, "(deleted=0)")
|
||||
|
||||
if f.AnyUserID != nil {
|
||||
sqlClauses = append(sqlClauses, fmt.Sprintf("(subscriber_user_id = :%s OR channel_owner_user_id = :%s)", params.Add(*f.AnyUserID), params.Add(*f.AnyUserID)))
|
||||
}
|
||||
@ -85,6 +88,14 @@ func (f SubscriptionFilter) SQL() (string, string, sq.PP, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if f.Active != nil {
|
||||
if *f.Active {
|
||||
sqlClauses = append(sqlClauses, "(active=1)")
|
||||
} else {
|
||||
sqlClauses = append(sqlClauses, "(active=0)")
|
||||
}
|
||||
}
|
||||
|
||||
if f.SubscriberIsChannelOwner != nil {
|
||||
if *f.SubscriberIsChannelOwner {
|
||||
sqlClauses = append(sqlClauses, "(subscriber_user_id = channel_owner_user_id)")
|
||||
|
@ -15,6 +15,7 @@ type User struct {
|
||||
QuotaUsedDay *string `db:"quota_used_day" json:"-"`
|
||||
IsPro bool `db:"is_pro" json:"is_pro"`
|
||||
ProToken *string `db:"pro_token" json:"-"`
|
||||
Deleted bool `db:"deleted" json:"-"`
|
||||
|
||||
UserExtra `db:"-"` // fields that are not in DB and are set on PreMarshal
|
||||
}
|
||||
|
@ -39,3 +39,7 @@ func (d *TestSink) SendNotification(ctx context.Context, user models.User, clien
|
||||
|
||||
return key, "", nil
|
||||
}
|
||||
|
||||
func (d *TestSink) Clear() {
|
||||
d.Data = make([]SinkData, 0)
|
||||
}
|
||||
|
@ -881,11 +881,6 @@
|
||||
"name": "channel_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "filter",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "has_sender",
|
||||
@ -910,6 +905,15 @@
|
||||
"name": "priority",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"name": "search",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -919,6 +923,15 @@
|
||||
"name": "sender",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"name": "string_search",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "trimmed",
|
||||
@ -1122,7 +1135,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "TokenKeyID",
|
||||
"description": "TokenKeyID (actual token || token-id)",
|
||||
"name": "kid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -1342,12 +1355,11 @@
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"description": "The body-values are optional, only send the ones you want to update",
|
||||
"tags": [
|
||||
"API-v2"
|
||||
],
|
||||
"summary": "(Partially) update a user",
|
||||
"operationId": "api-user-update",
|
||||
"summary": "(Self-)Deletes a user (including all entities - all messages, channels, clients, .....)",
|
||||
"operationId": "api-user-delete",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@ -1355,22 +1367,6 @@
|
||||
"name": "uid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Change the username (send an empty string to clear it)",
|
||||
"name": "username",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Send a verification of premium purchase",
|
||||
"name": "pro_token",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@ -1581,8 +1577,8 @@
|
||||
"tags": [
|
||||
"API-v2"
|
||||
],
|
||||
"summary": "(Partially) update a channel",
|
||||
"operationId": "api-channels-update",
|
||||
"summary": "delete a channel (including all messages, subscriptions, etc)",
|
||||
"operationId": "api-channels-delete",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@ -1597,37 +1593,13 @@
|
||||
"name": "cid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Send `true` to create a new subscribe_key",
|
||||
"name": "subscribe_key",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Send `true` to create a new send_key",
|
||||
"name": "send_key",
|
||||
"in": "body",
|
||||
"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": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ChannelWithSubscription"
|
||||
"$ref": "#/definitions/models.Channel"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
@ -1666,11 +1638,6 @@
|
||||
"summary": "List messages of a channel",
|
||||
"operationId": "api-channel-messages",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "filter",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "next_page_token",
|
||||
@ -3727,6 +3694,9 @@
|
||||
"handler.UpdateSubscription.body": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"confirmed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
@ -3862,6 +3832,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Channel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"description_name": {
|
||||
"description": "= DescriptionName, (optional), longer description text, initally nil",
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"description": "= DisplayName, used for display purposes, can be changed, initially equals InternalName",
|
||||
"type": "string"
|
||||
},
|
||||
"internal_name": {
|
||||
"description": "= InternalName, used for sending, normalized, cannot be changed",
|
||||
"type": "string"
|
||||
},
|
||||
"messages_sent": {
|
||||
"type": "integer"
|
||||
},
|
||||
"owner_user_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"subscribe_key": {
|
||||
"description": "can be nil, depending on endpoint",
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp_created": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp_lastsent": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ChannelPreview": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -3877,8 +3883,14 @@
|
||||
"internal_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_sent": {
|
||||
"type": "integer"
|
||||
},
|
||||
"owner_user_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"subscription": {
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -4069,6 +4081,10 @@
|
||||
"channel_internal_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"channel_owner_user_id": {
|
||||
"description": "user that owns the channel",
|
||||
"type": "string"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -4125,6 +4141,10 @@
|
||||
"models.Subscription": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"active": {
|
||||
"description": "Subscriber has activated the subscription (default)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"channel_id": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -4135,6 +4155,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"confirmed": {
|
||||
"description": "Channel-Owner confirmed subscription",
|
||||
"type": "boolean"
|
||||
},
|
||||
"subscriber_user_id": {
|
||||
|
@ -421,6 +421,8 @@ definitions:
|
||||
type: object
|
||||
handler.UpdateSubscription.body:
|
||||
properties:
|
||||
active:
|
||||
type: boolean
|
||||
confirmed:
|
||||
type: boolean
|
||||
type: object
|
||||
@ -508,6 +510,33 @@ definitions:
|
||||
uri:
|
||||
type: string
|
||||
type: object
|
||||
models.Channel:
|
||||
properties:
|
||||
channel_id:
|
||||
type: string
|
||||
description_name:
|
||||
description: = DescriptionName, (optional), longer description text, initally
|
||||
nil
|
||||
type: string
|
||||
display_name:
|
||||
description: = DisplayName, used for display purposes, can be changed, initially
|
||||
equals InternalName
|
||||
type: string
|
||||
internal_name:
|
||||
description: = InternalName, used for sending, normalized, cannot be changed
|
||||
type: string
|
||||
messages_sent:
|
||||
type: integer
|
||||
owner_user_id:
|
||||
type: string
|
||||
subscribe_key:
|
||||
description: can be nil, depending on endpoint
|
||||
type: string
|
||||
timestamp_created:
|
||||
type: string
|
||||
timestamp_lastsent:
|
||||
type: string
|
||||
type: object
|
||||
models.ChannelPreview:
|
||||
properties:
|
||||
channel_id:
|
||||
@ -518,8 +547,12 @@ definitions:
|
||||
type: string
|
||||
internal_name:
|
||||
type: string
|
||||
messages_sent:
|
||||
type: integer
|
||||
owner_user_id:
|
||||
type: string
|
||||
subscription:
|
||||
$ref: '#/definitions/models.Subscription'
|
||||
type: object
|
||||
models.ChannelWithSubscription:
|
||||
properties:
|
||||
@ -650,6 +683,9 @@ definitions:
|
||||
type: string
|
||||
channel_internal_name:
|
||||
type: string
|
||||
channel_owner_user_id:
|
||||
description: user that owns the channel
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
message_id:
|
||||
@ -688,6 +724,9 @@ definitions:
|
||||
type: object
|
||||
models.Subscription:
|
||||
properties:
|
||||
active:
|
||||
description: Subscriber has activated the subscription (default)
|
||||
type: boolean
|
||||
channel_id:
|
||||
type: string
|
||||
channel_internal_name:
|
||||
@ -695,6 +734,7 @@ definitions:
|
||||
channel_owner_user_id:
|
||||
type: string
|
||||
confirmed:
|
||||
description: Channel-Owner confirmed subscription
|
||||
type: boolean
|
||||
subscriber_user_id:
|
||||
type: string
|
||||
@ -1418,9 +1458,6 @@ paths:
|
||||
type: string
|
||||
name: channel_id
|
||||
type: array
|
||||
- in: query
|
||||
name: filter
|
||||
type: string
|
||||
- in: query
|
||||
name: has_sender
|
||||
type: boolean
|
||||
@ -1436,12 +1473,24 @@ paths:
|
||||
type: integer
|
||||
name: priority
|
||||
type: array
|
||||
- collectionFormat: csv
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: search
|
||||
type: array
|
||||
- collectionFormat: csv
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: sender
|
||||
type: array
|
||||
- collectionFormat: csv
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: string_search
|
||||
type: array
|
||||
- in: query
|
||||
name: trimmed
|
||||
type: boolean
|
||||
@ -1580,7 +1629,7 @@ paths:
|
||||
get:
|
||||
operationId: api-tokenkeys-get-preview
|
||||
parameters:
|
||||
- description: TokenKeyID
|
||||
- description: TokenKeyID (actual token || token-id)
|
||||
in: path
|
||||
name: kid
|
||||
required: true
|
||||
@ -1731,24 +1780,13 @@ paths:
|
||||
tags:
|
||||
- API-v2
|
||||
patch:
|
||||
description: The body-values are optional, only send the ones you want to update
|
||||
operationId: api-user-update
|
||||
operationId: api-user-delete
|
||||
parameters:
|
||||
- description: UserID
|
||||
in: path
|
||||
name: uid
|
||||
required: true
|
||||
type: string
|
||||
- description: Change the username (send an empty string to clear it)
|
||||
in: body
|
||||
name: username
|
||||
schema:
|
||||
type: string
|
||||
- description: Send a verification of premium purchase
|
||||
in: body
|
||||
name: pro_token
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
@ -1770,7 +1808,8 @@ paths:
|
||||
description: internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
summary: (Partially) update a user
|
||||
summary: (Self-)Deletes a user (including all entities - all messages, channels,
|
||||
clients, .....)
|
||||
tags:
|
||||
- API-v2
|
||||
/api/v2/users/{uid}/channels:
|
||||
@ -1895,7 +1934,7 @@ paths:
|
||||
tags:
|
||||
- API-v2
|
||||
patch:
|
||||
operationId: api-channels-update
|
||||
operationId: api-channels-delete
|
||||
parameters:
|
||||
- description: UserID
|
||||
in: path
|
||||
@ -1907,27 +1946,11 @@ paths:
|
||||
name: cid
|
||||
required: true
|
||||
type: string
|
||||
- description: Send `true` to create a new subscribe_key
|
||||
in: body
|
||||
name: subscribe_key
|
||||
schema:
|
||||
type: string
|
||||
- description: Send `true` to create a new send_key
|
||||
in: body
|
||||
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
|
||||
schema:
|
||||
$ref: '#/definitions/models.ChannelWithSubscription'
|
||||
$ref: '#/definitions/models.Channel'
|
||||
"400":
|
||||
description: supplied values/parameters cannot be parsed / are invalid
|
||||
schema:
|
||||
@ -1944,7 +1967,7 @@ paths:
|
||||
description: internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
summary: (Partially) update a channel
|
||||
summary: delete a channel (including all messages, subscriptions, etc)
|
||||
tags:
|
||||
- API-v2
|
||||
/api/v2/users/{uid}/channels/{cid}/messages:
|
||||
@ -1956,9 +1979,6 @@ paths:
|
||||
By default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size)
|
||||
operationId: api-channel-messages
|
||||
parameters:
|
||||
- in: query
|
||||
name: filter
|
||||
type: string
|
||||
- in: query
|
||||
name: next_page_token
|
||||
type: string
|
||||
|
@ -1,13 +1,14 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
tt "blackforestbytes.com/simplecloudnotifier/test/util"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
tt "blackforestbytes.com/simplecloudnotifier/test/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
)
|
||||
|
||||
func TestCreateChannel(t *testing.T) {
|
||||
@ -1233,3 +1234,94 @@ func TestChannelMessageCounter(t *testing.T) {
|
||||
assertCounter(6, 1, 3)
|
||||
|
||||
}
|
||||
|
||||
func TestDeleteChannel(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
// Initialize default data set
|
||||
data := tt.InitDefaultData(t, ws)
|
||||
|
||||
// User 16 owns channels, User 1 will subscribe
|
||||
user16 := data.User[16]
|
||||
user1 := data.User[1]
|
||||
|
||||
// Find channel "Chan2" belonging to user 16
|
||||
var chan2 tt.ChanData
|
||||
for _, ch := range user16.Channels {
|
||||
if ch.InternalName == "Chan2" {
|
||||
chan2 = ch
|
||||
break
|
||||
}
|
||||
}
|
||||
tt.AssertNotEqual(t, "Channel Chan2 ID", "", chan2.ChannelID) // Ensure channel was found
|
||||
|
||||
// --- Subscribe User 1 to User 16's Chan2 ---
|
||||
chanInfo := tt.RequestAuthGet[gin.H](t, user16.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s", user16.UID, chan2.ChannelID))
|
||||
subKey := chanInfo["subscribe_key"].(string)
|
||||
|
||||
subReq := tt.RequestAuthPost[gin.H](t, user1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?chan_subscribe_key=%s", user1.UID, subKey), gin.H{
|
||||
"channel_id": chan2.ChannelID, // Provide channel ID for subscription
|
||||
})
|
||||
subscriptionID := subReq["subscription_id"].(string)
|
||||
|
||||
// Confirm subscription by owner (user 16)
|
||||
tt.RequestAuthPatch[gin.H](t, user16.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user16.UID, subscriptionID), gin.H{
|
||||
"confirmed": true,
|
||||
})
|
||||
|
||||
// --- Pre-checks ---
|
||||
|
||||
// 1. Check channel exists
|
||||
tt.RequestAuthGet[gin.H](t, user16.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s", user16.UID, chan2.ChannelID))
|
||||
|
||||
// 2. Check channel messages exist (assuming mglist type from previous tests)
|
||||
type msg struct {
|
||||
MessageId string `json:"message_id"`
|
||||
}
|
||||
type mglist struct {
|
||||
Messages []msg `json:"messages"`
|
||||
}
|
||||
msgs := tt.RequestAuthGet[mglist](t, user16.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", user16.UID, chan2.ChannelID))
|
||||
tt.AssertTrue(t, "pre-check messages exist", len(msgs.Messages) > 0)
|
||||
|
||||
// 3. Check subscription exists for User 1 (outgoing)
|
||||
type subobj struct {
|
||||
SubscriptionId string `json:"subscription_id"`
|
||||
}
|
||||
type sublist struct {
|
||||
Subscriptions []subobj `json:"subscriptions"`
|
||||
}
|
||||
subs1 := tt.RequestAuthGet[sublist](t, user1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=outgoing", user1.UID))
|
||||
foundSub1 := langext.ArrAny(subs1.Subscriptions, func(v subobj) bool { return v.SubscriptionId == subscriptionID })
|
||||
tt.AssertTrue(t, "pre-check user1 subs outgoing", foundSub1)
|
||||
|
||||
// 4. Check subscription exists for User 16 (incoming)
|
||||
subs16 := tt.RequestAuthGet[sublist](t, user16.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=incoming", user16.UID))
|
||||
foundSub16 := langext.ArrAny(subs16.Subscriptions, func(v subobj) bool { return v.SubscriptionId == subscriptionID })
|
||||
tt.AssertTrue(t, "pre-check user16 subs incoming", foundSub16)
|
||||
|
||||
// --- Delete Channel ---
|
||||
tt.RequestAuthDelete[tt.Void](t, user16.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s", user16.UID, chan2.ChannelID), nil)
|
||||
|
||||
// --- Post-checks ---
|
||||
|
||||
// 1. Check channel fetch fails
|
||||
tt.RequestAuthGetShouldFail(t, user16.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s", user16.UID, chan2.ChannelID), 404, apierr.CHANNEL_NOT_FOUND)
|
||||
|
||||
// 2. Check channel messages fetch fails
|
||||
tt.RequestAuthGetShouldFail(t, user16.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", user16.UID, chan2.ChannelID), 404, apierr.CHANNEL_NOT_FOUND)
|
||||
|
||||
// Check subscriber cannot fetch messages either
|
||||
tt.RequestAuthGetShouldFail(t, user1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", user16.UID, chan2.ChannelID), 404, apierr.CHANNEL_NOT_FOUND) // Auth fails because subscription is gone
|
||||
|
||||
// 3. Check subscription is gone for User 1
|
||||
subs1After := tt.RequestAuthGet[sublist](t, user1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=outgoing", user1.UID))
|
||||
foundSub1After := langext.ArrAny(subs1After.Subscriptions, func(v subobj) bool { return v.SubscriptionId == subscriptionID })
|
||||
tt.AssertEqual(t, "post-check user1 subs outgoing", false, foundSub1After)
|
||||
|
||||
// 4. Check subscription is gone for User 16
|
||||
subs16After := tt.RequestAuthGet[sublist](t, user16.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=incoming", user16.UID))
|
||||
foundSub16After := langext.ArrAny(subs16After.Subscriptions, func(v subobj) bool { return v.SubscriptionId == subscriptionID })
|
||||
tt.AssertEqual(t, "post-check user16 subs incoming", false, foundSub16After)
|
||||
}
|
||||
|
@ -304,6 +304,8 @@ func TestPrimaryDB_Migrate_from_3_to_latest(t *testing.T) {
|
||||
tt.AssertAny(dbf3)
|
||||
tt.AssertAny(conf)
|
||||
|
||||
schemavers := 3
|
||||
|
||||
{
|
||||
url := fmt.Sprintf("file:%s", dbf1)
|
||||
|
||||
@ -312,8 +314,6 @@ func TestPrimaryDB_Migrate_from_3_to_latest(t *testing.T) {
|
||||
|
||||
qqdb := sq.NewDB(xdb, sq.DBOptions{})
|
||||
|
||||
schemavers := 3
|
||||
|
||||
dbschema := schema.PrimarySchema[schemavers]
|
||||
|
||||
_, err = qqdb.Exec(ctx, dbschema.SQL, sq.PP{})
|
||||
@ -351,7 +351,7 @@ func TestPrimaryDB_Migrate_from_3_to_latest(t *testing.T) {
|
||||
|
||||
schema1, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema1", 3, schema1)
|
||||
tt.AssertEqual(t, "schema1", schemavers, schema1)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
@ -400,6 +400,8 @@ func TestPrimaryDB_Migrate_from_4_to_latest(t *testing.T) {
|
||||
tt.AssertAny(dbf3)
|
||||
tt.AssertAny(conf)
|
||||
|
||||
schemavers := 4
|
||||
|
||||
{
|
||||
url := fmt.Sprintf("file:%s", dbf1)
|
||||
|
||||
@ -408,8 +410,6 @@ func TestPrimaryDB_Migrate_from_4_to_latest(t *testing.T) {
|
||||
|
||||
qqdb := sq.NewDB(xdb, sq.DBOptions{})
|
||||
|
||||
schemavers := 4
|
||||
|
||||
dbschema := schema.PrimarySchema[schemavers]
|
||||
|
||||
_, err = qqdb.Exec(ctx, dbschema.SQL, sq.PP{})
|
||||
@ -447,7 +447,391 @@ func TestPrimaryDB_Migrate_from_4_to_latest(t *testing.T) {
|
||||
|
||||
schema1, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema1", 4, schema1)
|
||||
tt.AssertEqual(t, "schema1", schemavers, schema1)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
//================================================
|
||||
{
|
||||
err = db1.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
//================================================
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema2, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema2", schema.PrimarySchemaVersion, schema2)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, db1.DB())
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schemHashDB", schema.PrimarySchema[schema.PrimarySchemaVersion].Hash, schemHashDB)
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = db1.Stop(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrimaryDB_Migrate_from_5_to_latest(t *testing.T) {
|
||||
dbf1, dbf2, dbf3, conf, stop := tt.StartSimpleTestspace(t)
|
||||
defer stop()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tt.AssertAny(dbf1)
|
||||
tt.AssertAny(dbf2)
|
||||
tt.AssertAny(dbf3)
|
||||
tt.AssertAny(conf)
|
||||
|
||||
schemavers := 5
|
||||
|
||||
{
|
||||
url := fmt.Sprintf("file:%s", dbf1)
|
||||
|
||||
xdb, err := sqlx.Open("sqlite3", url)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
qqdb := sq.NewDB(xdb, sq.DBOptions{})
|
||||
|
||||
dbschema := schema.PrimarySchema[schemavers]
|
||||
|
||||
_, err = qqdb.Exec(ctx, dbschema.SQL, sq.PP{})
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
_, err = qqdb.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
|
||||
"key": "schema",
|
||||
"val": schemavers,
|
||||
})
|
||||
|
||||
_, err = qqdb.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
|
||||
"key": "schema_hash",
|
||||
"val": dbschema.Hash,
|
||||
})
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, qqdb)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schemHashDB", dbschema.Hash, schemHashDB)
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = qqdb.Exit()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
db1, err := primary.NewPrimaryDatabase(conf)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema1, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema1", schemavers, schema1)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
//================================================
|
||||
{
|
||||
err = db1.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
//================================================
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema2, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema2", schema.PrimarySchemaVersion, schema2)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, db1.DB())
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schemHashDB", schema.PrimarySchema[schema.PrimarySchemaVersion].Hash, schemHashDB)
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = db1.Stop(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrimaryDB_Migrate_from_6_to_latest(t *testing.T) {
|
||||
dbf1, dbf2, dbf3, conf, stop := tt.StartSimpleTestspace(t)
|
||||
defer stop()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tt.AssertAny(dbf1)
|
||||
tt.AssertAny(dbf2)
|
||||
tt.AssertAny(dbf3)
|
||||
tt.AssertAny(conf)
|
||||
|
||||
schemavers := 6
|
||||
|
||||
{
|
||||
url := fmt.Sprintf("file:%s", dbf1)
|
||||
|
||||
xdb, err := sqlx.Open("sqlite3", url)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
qqdb := sq.NewDB(xdb, sq.DBOptions{})
|
||||
|
||||
dbschema := schema.PrimarySchema[schemavers]
|
||||
|
||||
_, err = qqdb.Exec(ctx, dbschema.SQL, sq.PP{})
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
_, err = qqdb.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
|
||||
"key": "schema",
|
||||
"val": schemavers,
|
||||
})
|
||||
|
||||
_, err = qqdb.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
|
||||
"key": "schema_hash",
|
||||
"val": dbschema.Hash,
|
||||
})
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, qqdb)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schemHashDB", dbschema.Hash, schemHashDB)
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = qqdb.Exit()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
db1, err := primary.NewPrimaryDatabase(conf)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema1, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema1", schemavers, schema1)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
//================================================
|
||||
{
|
||||
err = db1.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
//================================================
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema2, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema2", schema.PrimarySchemaVersion, schema2)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, db1.DB())
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schemHashDB", schema.PrimarySchema[schema.PrimarySchemaVersion].Hash, schemHashDB)
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = db1.Stop(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrimaryDB_Migrate_from_7_to_latest(t *testing.T) {
|
||||
dbf1, dbf2, dbf3, conf, stop := tt.StartSimpleTestspace(t)
|
||||
defer stop()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tt.AssertAny(dbf1)
|
||||
tt.AssertAny(dbf2)
|
||||
tt.AssertAny(dbf3)
|
||||
tt.AssertAny(conf)
|
||||
|
||||
schemavers := 7
|
||||
|
||||
{
|
||||
url := fmt.Sprintf("file:%s", dbf1)
|
||||
|
||||
xdb, err := sqlx.Open("sqlite3", url)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
qqdb := sq.NewDB(xdb, sq.DBOptions{})
|
||||
|
||||
dbschema := schema.PrimarySchema[schemavers]
|
||||
|
||||
_, err = qqdb.Exec(ctx, dbschema.SQL, sq.PP{})
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
_, err = qqdb.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
|
||||
"key": "schema",
|
||||
"val": schemavers,
|
||||
})
|
||||
|
||||
_, err = qqdb.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
|
||||
"key": "schema_hash",
|
||||
"val": dbschema.Hash,
|
||||
})
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, qqdb)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schemHashDB", dbschema.Hash, schemHashDB)
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = qqdb.Exit()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
db1, err := primary.NewPrimaryDatabase(conf)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema1, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema1", schemavers, schema1)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
//================================================
|
||||
{
|
||||
err = db1.Migrate(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
//================================================
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema2, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema2", schema.PrimarySchemaVersion, schema2)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, db1.DB())
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schemHashDB", schema.PrimarySchema[schema.PrimarySchemaVersion].Hash, schemHashDB)
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = db1.Stop(ctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrimaryDB_Migrate_from_8_to_latest(t *testing.T) {
|
||||
dbf1, dbf2, dbf3, conf, stop := tt.StartSimpleTestspace(t)
|
||||
defer stop()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tt.AssertAny(dbf1)
|
||||
tt.AssertAny(dbf2)
|
||||
tt.AssertAny(dbf3)
|
||||
tt.AssertAny(conf)
|
||||
|
||||
schemavers := 8
|
||||
|
||||
{
|
||||
url := fmt.Sprintf("file:%s", dbf1)
|
||||
|
||||
xdb, err := sqlx.Open("sqlite3", url)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
qqdb := sq.NewDB(xdb, sq.DBOptions{})
|
||||
|
||||
dbschema := schema.PrimarySchema[schemavers]
|
||||
|
||||
_, err = qqdb.Exec(ctx, dbschema.SQL, sq.PP{})
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
_, err = qqdb.Exec(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_int = :val", sq.PP{
|
||||
"key": "schema",
|
||||
"val": schemavers,
|
||||
})
|
||||
|
||||
_, err = qqdb.Exec(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (:key, :val) ON CONFLICT(meta_key) DO UPDATE SET value_txt = :val", sq.PP{
|
||||
"key": "schema_hash",
|
||||
"val": dbschema.Hash,
|
||||
})
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
schemHashDB, err := sq.HashSqliteDatabase(tctx, qqdb)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schemHashDB", dbschema.Hash, schemHashDB)
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
err = qqdb.Exit()
|
||||
tt.TestFailIfErr(t, err)
|
||||
}
|
||||
|
||||
{
|
||||
db1, err := primary.NewPrimaryDatabase(conf)
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
||||
{
|
||||
tctx := simplectx.CreateSimpleContext(ctx, nil)
|
||||
|
||||
schema1, err := db1.ReadSchema(tctx)
|
||||
tt.TestFailIfErr(t, err)
|
||||
tt.AssertEqual(t, "schema1", schemavers, schema1)
|
||||
|
||||
err = tctx.CommitTransaction()
|
||||
tt.TestFailIfErr(t, err)
|
||||
|
@ -1,6 +1,8 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/glebarez/go-sqlite"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"os"
|
||||
@ -12,5 +14,9 @@ func TestMain(m *testing.M) {
|
||||
exerr.Init(exerr.ErrorPackageConfigInit{ZeroLogErrTraces: langext.PFalse, ZeroLogAllTraces: langext.PFalse})
|
||||
}
|
||||
|
||||
if !langext.InArray("sqlite3", sql.Drivers()) {
|
||||
sqlite.RegisterAsSQLITE3()
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
tt "blackforestbytes.com/simplecloudnotifier/test/util"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
tt "blackforestbytes.com/simplecloudnotifier/test/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
)
|
||||
|
||||
func TestSearchMessageFTSSimple(t *testing.T) {
|
||||
@ -901,3 +902,293 @@ func TestListMessagesStringSearch(t *testing.T) {
|
||||
tt.AssertEqual(t, "msgList.filter["+testdata.Name+"].len", testdata.Count, msgList.TotalCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeactivatedSubscriptionListMessages(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
data := tt.InitDefaultData(t, ws)
|
||||
|
||||
type subobj struct {
|
||||
ChannelId string `json:"channel_id"`
|
||||
ChannelInternalName string `json:"channel_internal_name"`
|
||||
ChannelOwnerUserId string `json:"channel_owner_user_id"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Active bool `json:"active"`
|
||||
SubscriberUserId string `json:"subscriber_user_id"`
|
||||
SubscriptionId string `json:"subscription_id"`
|
||||
TimestampCreated string `json:"timestamp_created"`
|
||||
}
|
||||
type msg struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
type mglist struct {
|
||||
Messages []msg `json:"messages"`
|
||||
}
|
||||
|
||||
user14 := data.User[14] // Subscriber
|
||||
user15 := data.User[15] // Owner
|
||||
chanName := "chan_other_accepted"
|
||||
|
||||
subscriptionID, channelID := tt.FindSubscriptionByChanName(t, baseUrl, user14, user15.UID, chanName)
|
||||
|
||||
gsub0 := tt.RequestAuthGet[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID))
|
||||
tt.AssertTrue(t, "gsub1.active", gsub0.Active)
|
||||
tt.AssertTrue(t, "gsub1.confirmed", gsub0.Confirmed)
|
||||
|
||||
tt.RequestAuthPatch[gin.H](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), gin.H{"active": false})
|
||||
|
||||
gsub1 := tt.RequestAuthGet[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID))
|
||||
tt.AssertFalse(t, "gsub1.active", gsub1.Active)
|
||||
tt.AssertTrue(t, "gsub1.confirmed", gsub1.Confirmed)
|
||||
|
||||
gsub2 := tt.RequestAuthGet[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID))
|
||||
tt.AssertFalse(t, "gsub2.active", gsub2.Active)
|
||||
tt.AssertTrue(t, "gsub2.confirmed", gsub2.Confirmed)
|
||||
|
||||
newMessageTitle := langext.RandBase62(48)
|
||||
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
|
||||
"key": user15.AdminKey,
|
||||
"user_id": user15.UID,
|
||||
"channel": chanName,
|
||||
"title": newMessageTitle,
|
||||
})
|
||||
|
||||
// subscription.active == false && subscription.confirmed == true
|
||||
{
|
||||
msgListSub := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, "/api/v2/messages")
|
||||
|
||||
foundActivatedMessageSub := false
|
||||
for _, m := range msgListSub.Messages {
|
||||
if m.Title == newMessageTitle {
|
||||
foundActivatedMessageSub = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tt.AssertFalse(t, "foundActivatedMessageSub", foundActivatedMessageSub)
|
||||
|
||||
msgListDirect := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", user15.UID, channelID))
|
||||
|
||||
foundActivatedMessageDirect := false
|
||||
for _, m := range msgListDirect.Messages {
|
||||
if m.Title == newMessageTitle {
|
||||
foundActivatedMessageDirect = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tt.AssertTrue(t, "foundActivatedMessageDirect", foundActivatedMessageDirect)
|
||||
}
|
||||
|
||||
tt.RequestAuthPatch[gin.H](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), gin.H{"active": true})
|
||||
|
||||
// subscription.active == true && subscription.confirmed == true
|
||||
{
|
||||
msgListSub := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, "/api/v2/messages")
|
||||
|
||||
foundActivatedMessageSub := false
|
||||
for _, m := range msgListSub.Messages {
|
||||
if m.Title == newMessageTitle {
|
||||
foundActivatedMessageSub = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tt.AssertTrue(t, "foundActivatedMessageSub", foundActivatedMessageSub)
|
||||
|
||||
msgListDirect := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", user15.UID, channelID))
|
||||
|
||||
foundActivatedMessageDirect := false
|
||||
for _, m := range msgListDirect.Messages {
|
||||
if m.Title == newMessageTitle {
|
||||
foundActivatedMessageDirect = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tt.AssertTrue(t, "foundActivatedMessageDirect", foundActivatedMessageDirect)
|
||||
}
|
||||
|
||||
tt.RequestAuthPatch[gin.H](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID), gin.H{"confirmed": false})
|
||||
|
||||
// subscription.active == true && subscription.confirmed == false
|
||||
{
|
||||
msgListSub := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, "/api/v2/messages")
|
||||
|
||||
foundActivatedMessageSub := false
|
||||
for _, m := range msgListSub.Messages {
|
||||
if m.Title == newMessageTitle {
|
||||
foundActivatedMessageSub = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tt.AssertFalse(t, "foundActivatedMessageSub", foundActivatedMessageSub)
|
||||
|
||||
tt.RequestAuthGetShouldFail(t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", user15.UID, channelID), 401, apierr.USER_AUTH_FAILED)
|
||||
}
|
||||
|
||||
tt.RequestAuthPatch[gin.H](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), gin.H{"active": false})
|
||||
|
||||
// subscription.active == false && subscription.confirmed == false
|
||||
{
|
||||
msgListSub := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, "/api/v2/messages")
|
||||
|
||||
foundActivatedMessageSub := false
|
||||
for _, m := range msgListSub.Messages {
|
||||
if m.Title == newMessageTitle {
|
||||
foundActivatedMessageSub = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tt.AssertFalse(t, "foundActivatedMessageSub", foundActivatedMessageSub)
|
||||
|
||||
tt.RequestAuthGetShouldFail(t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", user15.UID, channelID), 401, apierr.USER_AUTH_FAILED)
|
||||
}
|
||||
|
||||
tt.RequestAuthPatch[gin.H](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID), gin.H{"confirmed": true})
|
||||
tt.RequestAuthPatch[gin.H](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), gin.H{"active": true})
|
||||
|
||||
// subscription.active == true && subscription.confirmed == true
|
||||
{
|
||||
msgListSub := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, "/api/v2/messages")
|
||||
|
||||
foundActivatedMessageSub := false
|
||||
for _, m := range msgListSub.Messages {
|
||||
if m.Title == newMessageTitle {
|
||||
foundActivatedMessageSub = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tt.AssertTrue(t, "foundActivatedMessageSub", foundActivatedMessageSub)
|
||||
|
||||
msgListDirect := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", user15.UID, channelID))
|
||||
|
||||
foundActivatedMessageDirect := false
|
||||
for _, m := range msgListDirect.Messages {
|
||||
if m.Title == newMessageTitle {
|
||||
foundActivatedMessageDirect = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tt.AssertTrue(t, "foundActivatedMessageDirect", foundActivatedMessageDirect)
|
||||
}
|
||||
|
||||
tt.RequestAuthDelete[gin.H](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), gin.H{})
|
||||
|
||||
// subscription -> deleted
|
||||
{
|
||||
msgListSub := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, "/api/v2/messages")
|
||||
|
||||
foundActivatedMessageSub := false
|
||||
for _, m := range msgListSub.Messages {
|
||||
if m.Title == newMessageTitle {
|
||||
foundActivatedMessageSub = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tt.AssertFalse(t, "foundActivatedMessageSub", foundActivatedMessageSub)
|
||||
|
||||
tt.RequestAuthGetShouldFail(t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", user15.UID, channelID), 401, apierr.USER_AUTH_FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveSubscriptionListMessages(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
data := tt.InitDefaultData(t, ws)
|
||||
|
||||
type subobj struct {
|
||||
ChannelId string `json:"channel_id"`
|
||||
ChannelInternalName string `json:"channel_internal_name"`
|
||||
ChannelOwnerUserId string `json:"channel_owner_user_id"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Active bool `json:"active"`
|
||||
SubscriberUserId string `json:"subscriber_user_id"`
|
||||
SubscriptionId string `json:"subscription_id"`
|
||||
TimestampCreated string `json:"timestamp_created"`
|
||||
}
|
||||
type msg struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
type mglist struct {
|
||||
Messages []msg `json:"messages"`
|
||||
}
|
||||
|
||||
user14 := data.User[14] // Subscriber
|
||||
user15 := data.User[15] // Owner
|
||||
chanName := "chan_other_accepted"
|
||||
|
||||
subscriptionID, _ := tt.FindSubscriptionByChanName(t, baseUrl, user14, user15.UID, chanName)
|
||||
|
||||
gsub1 := tt.RequestAuthGet[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID))
|
||||
tt.AssertTrue(t, "gsub1.active", gsub1.Active)
|
||||
|
||||
newMessageTitle := langext.RandBase62(48)
|
||||
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
|
||||
"key": user15.AdminKey,
|
||||
"user_id": user15.UID,
|
||||
"channel": chanName,
|
||||
"title": newMessageTitle,
|
||||
})
|
||||
|
||||
{
|
||||
msgListSub := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, "/api/v2/messages")
|
||||
|
||||
foundActivatedMessageSub := false
|
||||
for _, m := range msgListSub.Messages {
|
||||
if m.Title == newMessageTitle {
|
||||
foundActivatedMessageSub = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tt.AssertTrue(t, "foundActivatedMessageSub", foundActivatedMessageSub)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnconfirmedSubscriptionListMessages(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
data := tt.InitDefaultData(t, ws)
|
||||
|
||||
type subobj struct {
|
||||
ChannelId string `json:"channel_id"`
|
||||
ChannelInternalName string `json:"channel_internal_name"`
|
||||
ChannelOwnerUserId string `json:"channel_owner_user_id"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Active bool `json:"active"`
|
||||
SubscriberUserId string `json:"subscriber_user_id"`
|
||||
SubscriptionId string `json:"subscription_id"`
|
||||
TimestampCreated string `json:"timestamp_created"`
|
||||
}
|
||||
type msg struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
type mglist struct {
|
||||
Messages []msg `json:"messages"`
|
||||
}
|
||||
|
||||
user14 := data.User[14] // Subscriber
|
||||
user15 := data.User[15] // Owner
|
||||
chanName := "chan_other_request"
|
||||
|
||||
subscriptionID, _ := tt.FindSubscriptionByChanName(t, baseUrl, user14, user15.UID, chanName)
|
||||
|
||||
gsub1 := tt.RequestAuthGet[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID))
|
||||
tt.AssertTrue(t, "gsub1.active", gsub1.Active)
|
||||
tt.AssertFalse(t, "gsub1.confirmed", gsub1.Confirmed)
|
||||
|
||||
newMessageTitle := langext.RandBase62(48)
|
||||
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
|
||||
"key": user15.AdminKey,
|
||||
"user_id": user15.UID,
|
||||
"channel": chanName,
|
||||
"title": newMessageTitle,
|
||||
})
|
||||
|
||||
msgList := tt.RequestAuthGet[mglist](t, user14.AdminKey, baseUrl, "/api/v2/messages")
|
||||
|
||||
foundActivatedMessage := false
|
||||
for _, m := range msgList.Messages {
|
||||
if m.Title == newMessageTitle {
|
||||
foundActivatedMessage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tt.AssertFalse(t, "foundActivatedMessage", foundActivatedMessage)
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ func TestResponseChannel(t *testing.T) {
|
||||
"channel_internal_name": "string",
|
||||
"timestamp_created": "rfc3339",
|
||||
"confirmed": "bool",
|
||||
"active": "bool",
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -179,6 +180,7 @@ func TestResponseMessage(t *testing.T) {
|
||||
"sender_user_id": "id",
|
||||
"channel_internal_name": "string",
|
||||
"channel_id": "id",
|
||||
"channel_owner_user_id": "id",
|
||||
"sender_name": "string",
|
||||
"sender_ip": "string",
|
||||
"timestamp": "rfc3339",
|
||||
@ -207,6 +209,7 @@ func TestResponseSubscription(t *testing.T) {
|
||||
"channel_internal_name": "string",
|
||||
"timestamp_created": "rfc3339",
|
||||
"confirmed": "bool",
|
||||
"active": "bool",
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
tt "blackforestbytes.com/simplecloudnotifier/test/util"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"math/rand/v2"
|
||||
"net/url"
|
||||
"strings"
|
||||
@ -1807,3 +1808,176 @@ func TestSendWithPermissionSendKey(t *testing.T) {
|
||||
func TestSendDeliveryRetry(t *testing.T) {
|
||||
t.SkipNow() //TODO
|
||||
}
|
||||
|
||||
func TestDeactivatedSubscriptionReceiveMessage(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
data := tt.InitDefaultData(t, ws)
|
||||
|
||||
type subobj struct {
|
||||
ChannelId string `json:"channel_id"`
|
||||
ChannelInternalName string `json:"channel_internal_name"`
|
||||
ChannelOwnerUserId string `json:"channel_owner_user_id"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Active bool `json:"active"`
|
||||
SubscriberUserId string `json:"subscriber_user_id"`
|
||||
SubscriptionId string `json:"subscription_id"`
|
||||
TimestampCreated string `json:"timestamp_created"`
|
||||
}
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
pusher.Clear()
|
||||
|
||||
user14 := data.User[14] // Subscriber
|
||||
user15 := data.User[15] // Owner
|
||||
chanName := "chan_other_accepted"
|
||||
|
||||
subscriptionID, _ := tt.FindSubscriptionByChanName(t, baseUrl, user14, user15.UID, chanName)
|
||||
|
||||
gsub0 := tt.RequestAuthGet[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID))
|
||||
tt.AssertTrue(t, "gsub0.active", gsub0.Active)
|
||||
tt.AssertTrue(t, "gsub0.confirmed", gsub0.Confirmed)
|
||||
|
||||
tt.RequestAuthPatch[gin.H](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), gin.H{"active": false})
|
||||
|
||||
gsub1 := tt.RequestAuthGet[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID))
|
||||
tt.AssertFalse(t, "gsub1.active", gsub1.Active)
|
||||
tt.AssertTrue(t, "gsub1.confirmed", gsub1.Confirmed)
|
||||
|
||||
// sub is active=false && confirmed=true
|
||||
{
|
||||
newMessageTitle := langext.RandBase62(48)
|
||||
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
|
||||
"key": user15.AdminKey,
|
||||
"user_id": user15.UID,
|
||||
"channel": chanName,
|
||||
"title": newMessageTitle,
|
||||
})
|
||||
|
||||
pushObj := langext.ArrFirstOrNil(pusher.Data, func(d push.SinkData) bool { return d.Client.UserID.String() == user14.UID })
|
||||
|
||||
tt.AssertNil(t, "pushObj", pushObj)
|
||||
|
||||
pusher.Clear()
|
||||
}
|
||||
|
||||
tt.RequestAuthPatch[gin.H](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), gin.H{"active": true})
|
||||
|
||||
gsub2 := tt.RequestAuthGet[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID))
|
||||
tt.AssertTrue(t, "gsub1.active", gsub2.Active)
|
||||
tt.AssertTrue(t, "gsub1.confirmed", gsub2.Confirmed)
|
||||
|
||||
// sub is active=true && confirmed=true
|
||||
{
|
||||
newMessageTitle := langext.RandBase62(48)
|
||||
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
|
||||
"key": user15.AdminKey,
|
||||
"user_id": user15.UID,
|
||||
"channel": chanName,
|
||||
"title": newMessageTitle,
|
||||
})
|
||||
|
||||
pushObj := langext.ArrFirstOrNil(pusher.Data, func(d push.SinkData) bool { return d.Client.UserID.String() == user14.UID })
|
||||
|
||||
tt.AssertNotNil(t, "pushObj", pushObj)
|
||||
tt.AssertStrRepEqual(t, "msg.title", newMessageTitle, pushObj.Message.Title)
|
||||
tt.AssertStrRepEqual(t, "msg.content", nil, pushObj.Message.Content)
|
||||
tt.AssertStrRepEqual(t, "msg.scn_msg_id", msg1["scn_msg_id"], pushObj.Message.MessageID)
|
||||
tt.AssertStrRepEqual(t, "msg.scn_msg_id", user14.UID, pushObj.Client.UserID)
|
||||
|
||||
pusher.Clear()
|
||||
}
|
||||
|
||||
tt.RequestAuthPatch[gin.H](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID), gin.H{"confirmed": false})
|
||||
|
||||
gsub3 := tt.RequestAuthGet[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID))
|
||||
tt.AssertTrue(t, "gsub1.active", gsub3.Active)
|
||||
tt.AssertFalse(t, "gsub1.confirmed", gsub3.Confirmed)
|
||||
|
||||
// sub is active=true && confirmed=false
|
||||
{
|
||||
newMessageTitle := langext.RandBase62(48)
|
||||
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
|
||||
"key": user15.AdminKey,
|
||||
"user_id": user15.UID,
|
||||
"channel": chanName,
|
||||
"title": newMessageTitle,
|
||||
})
|
||||
|
||||
pushObj := langext.ArrFirstOrNil(pusher.Data, func(d push.SinkData) bool { return d.Client.UserID.String() == user14.UID })
|
||||
|
||||
tt.AssertNil(t, "pushObj", pushObj)
|
||||
|
||||
pusher.Clear()
|
||||
}
|
||||
|
||||
tt.RequestAuthDelete[gin.H](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID), gin.H{})
|
||||
|
||||
tt.RequestAuthGetShouldFail(t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), 404, apierr.SUBSCRIPTION_NOT_FOUND)
|
||||
tt.RequestAuthGetShouldFail(t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID), 404, apierr.SUBSCRIPTION_NOT_FOUND)
|
||||
|
||||
// sub is deleted
|
||||
{
|
||||
newMessageTitle := langext.RandBase62(48)
|
||||
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
|
||||
"key": user15.AdminKey,
|
||||
"user_id": user15.UID,
|
||||
"channel": chanName,
|
||||
"title": newMessageTitle,
|
||||
})
|
||||
|
||||
pushObj := langext.ArrFirstOrNil(pusher.Data, func(d push.SinkData) bool { return d.Client.UserID.String() == user14.UID })
|
||||
|
||||
tt.AssertNil(t, "pushObj", pushObj)
|
||||
|
||||
pusher.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveSubscriptionReceiveMessage(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
data := tt.InitDefaultData(t, ws)
|
||||
|
||||
type subobj struct {
|
||||
ChannelId string `json:"channel_id"`
|
||||
ChannelInternalName string `json:"channel_internal_name"`
|
||||
ChannelOwnerUserId string `json:"channel_owner_user_id"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Active bool `json:"active"`
|
||||
SubscriberUserId string `json:"subscriber_user_id"`
|
||||
SubscriptionId string `json:"subscription_id"`
|
||||
TimestampCreated string `json:"timestamp_created"`
|
||||
}
|
||||
|
||||
pusher := ws.Pusher.(*push.TestSink)
|
||||
pusher.Clear()
|
||||
|
||||
user14 := data.User[14] // Subscriber
|
||||
user15 := data.User[15] // Owner
|
||||
chanName := "chan_other_accepted"
|
||||
|
||||
subscriptionID, _ := tt.FindSubscriptionByChanName(t, baseUrl, user14, user15.UID, chanName)
|
||||
|
||||
gsub1 := tt.RequestAuthGet[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID))
|
||||
tt.AssertTrue(t, "gsub1.active", gsub1.Active)
|
||||
tt.AssertTrue(t, "gsub1.confirmed", gsub1.Confirmed)
|
||||
|
||||
newMessageTitle := langext.RandBase62(48)
|
||||
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
|
||||
"key": user15.AdminKey,
|
||||
"user_id": user15.UID,
|
||||
"channel": chanName,
|
||||
"title": newMessageTitle,
|
||||
})
|
||||
|
||||
pushObj := langext.ArrFirstOrNil(pusher.Data, func(d push.SinkData) bool { return d.Client.UserID.String() == user14.UID })
|
||||
|
||||
tt.AssertNotNil(t, "pushObj", pushObj)
|
||||
tt.AssertStrRepEqual(t, "msg.title", newMessageTitle, pushObj.Message.Title)
|
||||
tt.AssertStrRepEqual(t, "msg.content", nil, pushObj.Message.Content)
|
||||
tt.AssertStrRepEqual(t, "msg.scn_msg_id", msg1["scn_msg_id"], pushObj.Message.MessageID)
|
||||
tt.AssertStrRepEqual(t, "msg.scn_msg_id", user14.UID, pushObj.Client.UserID)
|
||||
|
||||
pusher.Clear()
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
tt "blackforestbytes.com/simplecloudnotifier/test/util"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListSubscriptionsOfUser(t *testing.T) {
|
||||
@ -1240,3 +1241,106 @@ func TestCancelOutgoingSubscription(t *testing.T) {
|
||||
tt.RequestAuthGetShouldFail(t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data1.UID, sub1.SubscriptionId), 404, apierr.SUBSCRIPTION_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionDeactivate(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
data := tt.InitDefaultData(t, ws)
|
||||
|
||||
user14 := data.User[14]
|
||||
user15 := data.User[15]
|
||||
chanName := "chan_other_accepted"
|
||||
|
||||
subscriptionID, _ := tt.FindSubscriptionByChanName(t, baseUrl, user14, user15.UID, chanName)
|
||||
|
||||
type subobj struct {
|
||||
SubscriptionId string `json:"subscription_id"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
initialSub := tt.RequestAuthGet[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID))
|
||||
tt.AssertTrue(t, "initialSub.Active", initialSub.Active)
|
||||
|
||||
// subscriber deactivates
|
||||
{
|
||||
tt.RequestAuthPatch[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), gin.H{
|
||||
"active": false,
|
||||
})
|
||||
|
||||
sub1 := tt.RequestAuthGet[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID))
|
||||
tt.AssertEqual(t, "sub1.Active", false, sub1.Active)
|
||||
|
||||
sub2 := tt.RequestAuthGet[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID))
|
||||
tt.AssertEqual(t, "sub2.Active", false, sub2.Active)
|
||||
}
|
||||
|
||||
// subscriber activates
|
||||
{
|
||||
tt.RequestAuthPatch[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), gin.H{
|
||||
"active": true,
|
||||
})
|
||||
|
||||
sub1 := tt.RequestAuthGet[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID))
|
||||
tt.AssertEqual(t, "sub1.Active", true, sub1.Active)
|
||||
|
||||
sub2 := tt.RequestAuthGet[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID))
|
||||
tt.AssertEqual(t, "sub2.Active", true, sub2.Active)
|
||||
}
|
||||
|
||||
// owner deactivates
|
||||
{
|
||||
tt.RequestAuthPatch[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID), gin.H{
|
||||
"active": false,
|
||||
})
|
||||
|
||||
sub1 := tt.RequestAuthGet[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID))
|
||||
tt.AssertEqual(t, "sub1.Active", false, sub1.Active)
|
||||
|
||||
sub2 := tt.RequestAuthGet[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID))
|
||||
tt.AssertEqual(t, "sub2.Active", false, sub2.Active)
|
||||
}
|
||||
|
||||
// owner activates
|
||||
{
|
||||
tt.RequestAuthPatch[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID), gin.H{
|
||||
"active": true,
|
||||
})
|
||||
|
||||
sub1 := tt.RequestAuthGet[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID))
|
||||
tt.AssertEqual(t, "sub1.Active", true, sub1.Active)
|
||||
|
||||
sub2 := tt.RequestAuthGet[subobj](t, user15.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user15.UID, subscriptionID))
|
||||
tt.AssertEqual(t, "sub2.Active", true, sub2.Active)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionActivate(t *testing.T) {
|
||||
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
data := tt.InitDefaultData(t, ws)
|
||||
|
||||
user14 := data.User[14]
|
||||
user15 := data.User[15]
|
||||
chanName := "chan_other_accepted"
|
||||
|
||||
subscriptionID, _ := tt.FindSubscriptionByChanName(t, baseUrl, user14, user15.UID, chanName)
|
||||
|
||||
type subobj struct {
|
||||
SubscriptionId string `json:"subscription_id"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
tt.RequestAuthPatch[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), gin.H{
|
||||
"active": false,
|
||||
})
|
||||
|
||||
deactivatedSub := tt.RequestAuthGet[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID))
|
||||
tt.AssertEqual(t, "deactivatedSub.Active", false, deactivatedSub.Active)
|
||||
|
||||
tt.RequestAuthPatch[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID), gin.H{
|
||||
"active": true,
|
||||
})
|
||||
|
||||
finalSub := tt.RequestAuthGet[subobj](t, user14.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", user14.UID, subscriptionID))
|
||||
tt.AssertTrue(t, "finalSub.Active", finalSub.Active)
|
||||
}
|
||||
|
@ -185,8 +185,6 @@ func TestFailedUgradeUserToPro(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteUser(t *testing.T) {
|
||||
t.SkipNow() // TODO DeleteUser Not implemented
|
||||
|
||||
_, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||
defer stop()
|
||||
|
||||
@ -199,15 +197,18 @@ func TestDeleteUser(t *testing.T) {
|
||||
|
||||
uid := fmt.Sprintf("%v", r0["user_id"])
|
||||
admintok := r0["admin_key"].(string)
|
||||
readtok := r0["read_key"].(string)
|
||||
sendtok := r0["send_key"].(string)
|
||||
|
||||
tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid)
|
||||
|
||||
tt.RequestAuthDeleteShouldFail(t, admintok, baseUrl, "/api/v2/users/"+uid, nil, 401, apierr.USER_AUTH_FAILED)
|
||||
tt.RequestAuthDeleteShouldFail(t, readtok, baseUrl, "/api/v2/users/"+uid, nil, 401, apierr.USER_AUTH_FAILED)
|
||||
|
||||
tt.RequestAuthDeleteShouldFail(t, sendtok, baseUrl, "/api/v2/users/"+uid, nil, 401, apierr.USER_AUTH_FAILED)
|
||||
|
||||
tt.RequestAuthDelete[tt.Void](t, admintok, baseUrl, "/api/v2/users/"+uid, nil)
|
||||
|
||||
tt.RequestAuthGetShouldFail(t, admintok, baseUrl, "/api/v2/users/"+uid, 404, apierr.USER_NOT_FOUND)
|
||||
|
||||
tt.RequestAuthGetShouldFail(t, admintok, baseUrl, "/api/v2/users/"+uid, 401, apierr.USER_AUTH_FAILED)
|
||||
}
|
||||
|
||||
func TestCreateProUser(t *testing.T) {
|
||||
|
@ -177,6 +177,14 @@ func AssertTrue(t *testing.T, key string, v bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func AssertFalse(t *testing.T, key string, v bool) {
|
||||
if v {
|
||||
t.Errorf("AssertFalse(%s) failed", key)
|
||||
t.Error(string(debug.Stack()))
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func AssertNotDefault[T comparable](t *testing.T, key string, v T) {
|
||||
if v == *new(T) {
|
||||
t.Errorf("AssertNotDefault(%s) failed", key)
|
||||
@ -307,7 +315,7 @@ func AssertAny(v any) {
|
||||
}
|
||||
|
||||
func AssertNil(t *testing.T, key string, v any) {
|
||||
if v != nil {
|
||||
if !langext.IsNil(v) {
|
||||
t.Errorf("AssertNil(%s) failed - actual value:\n%+v", key, v)
|
||||
t.Error(string(debug.Stack()))
|
||||
t.FailNow()
|
||||
@ -315,7 +323,7 @@ func AssertNil(t *testing.T, key string, v any) {
|
||||
}
|
||||
|
||||
func AssertNotNil(t *testing.T, key string, v any) {
|
||||
if v == nil {
|
||||
if langext.IsNil(v) {
|
||||
t.Errorf("AssertNotNil(%s) failed", key)
|
||||
t.Error(string(debug.Stack()))
|
||||
t.FailNow()
|
||||
|
@ -467,8 +467,10 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
|
||||
users[i].Subscriptions = langext.ArrMap(r0.Subs, func(v ssub) string { return v.ID })
|
||||
}
|
||||
|
||||
// Sub/Unsub for Users 12+13
|
||||
|
||||
// Sub/Unsub for Users 14+15
|
||||
// - User 14 is not subscribed to (own) channel "chan_self_unsub"
|
||||
// - User 14 has an unconfirmed request to User15's channel "chan_other_request"
|
||||
// - User 14 has a confirmed+active subscription to User15's channel "chan_other_accepted"
|
||||
{
|
||||
doUnsubscribe(t, baseUrl, users[14], users[14], "chan_self_unsub")
|
||||
doSubscribe(t, baseUrl, users[14], users[15], "chan_other_request")
|
||||
|
34
scnserver/test/util/helper.go
Normal file
34
scnserver/test/util/helper.go
Normal file
@ -0,0 +1,34 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func FindSubscriptionByChanName(t *testing.T, baseUrl string, subscriber Userdat, ownerUID string, chanName string) (subscriptionID string, channelID string) {
|
||||
type subobj struct {
|
||||
SubscriptionId string `json:"subscription_id"`
|
||||
ChannelId string `json:"channel_id"`
|
||||
ChannelInternalName string `json:"channel_internal_name"`
|
||||
ChannelOwnerUserId string `json:"channel_owner_user_id"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
type sublist struct {
|
||||
Subscriptions []subobj `json:"subscriptions"`
|
||||
}
|
||||
|
||||
subs := RequestAuthGet[sublist](t, subscriber.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=outgoing", subscriber.UID))
|
||||
|
||||
for _, sub := range subs.Subscriptions {
|
||||
if sub.ChannelOwnerUserId == ownerUID && sub.ChannelInternalName == chanName {
|
||||
fullSub := RequestAuthGet[subobj](t, subscriber.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", subscriber.UID, sub.SubscriptionId))
|
||||
if fullSub.ChannelOwnerUserId == ownerUID && fullSub.ChannelInternalName == chanName {
|
||||
return fullSub.SubscriptionId, fullSub.ChannelId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("Could not find subscription for user %s to channel %s owned by %s", subscriber.UID, chanName, ownerUID)
|
||||
return "", ""
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user