From 8aee952aa93137ec3aba4411957df6551aa8a3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Jerman?= Date: Tue, 8 Oct 2019 12:59:20 +0200 Subject: [PATCH] Add settings service to messaging --- api/messaging/spec.json | 86 ++++++ api/messaging/spec/settings.json | 100 +++++++ docs/messaging/README.md | 73 +++++ messaging/db/mysql/static.go | 2 +- .../mysql/20191008125405.settings.up.sql | 10 + messaging/rest/handlers/settings.go | 139 ++++++++++ messaging/rest/request/settings.go | 262 ++++++++++++++++++ messaging/rest/request/util.go | 6 + messaging/rest/router.go | 1 + messaging/rest/settings.go | 61 ++++ messaging/service/access_control.go | 8 + messaging/service/service.go | 9 +- messaging/service/settings.go | 96 +++++++ 13 files changed, 851 insertions(+), 2 deletions(-) create mode 100644 api/messaging/spec/settings.json create mode 100644 messaging/db/schema/mysql/20191008125405.settings.up.sql create mode 100644 messaging/rest/handlers/settings.go create mode 100644 messaging/rest/request/settings.go create mode 100644 messaging/rest/settings.go create mode 100644 messaging/service/settings.go diff --git a/api/messaging/spec.json b/api/messaging/spec.json index f58a5cbb1..102f41275 100644 --- a/api/messaging/spec.json +++ b/api/messaging/spec.json @@ -1245,5 +1245,91 @@ } } ] + }, + { + "title": "Settings", + "path": "/settings", + "entrypoint": "settings", + "authentication": [], + "struct": [{ + "imports": [ + "sqlxTypes github.com/jmoiron/sqlx/types" + ] + }], + "apis": [{ + "name": "list", + "method": "GET", + "title": "List settings", + "path": "/", + "parameters": { + "get": [ + { + "name": "prefix", + "type": "string", + "title": "Key prefix" + } + ] + } + }, + { + "name": "update", + "method": "PATCH", + "title": "Update settings", + "path": "/", + "parameters": { + "post": [{ + "name": "values", + "type": "sqlxTypes.JSONText", + "title": "Array of new settings: `[{ name: ..., value: ... }]`. Omit value to remove setting", + "required": true + }] + } + }, + { + "name": "get", + "method": "GET", + "title": "Get a value for a key", + "path": "/{key}", + "parameters": { + "path": [{ + "name": "key", + "type": "string", + "title": "Setting key", + "required": true + }], + "get": [{ + "name": "ownerID", + "type": "uint64", + "title": "Owner ID" + }] + } + }, + { + "name": "set", + "method": "PUT", + "title": "Set a value for a key", + "path": "/{key}", + "parameters": { + "path": [{ + "name": "key", + "type": "string", + "title": "Setting key", + "required": true + }], + "post": [{ + "name": "ownerID", + "type": "uint64", + "title": "Owner" + }, + { + "name": "value", + "type": "sqlxTypes.JSONText", + "required": true, + "title": "Setting value" + } + ] + } + } + ] } ] diff --git a/api/messaging/spec/settings.json b/api/messaging/spec/settings.json new file mode 100644 index 000000000..12884045d --- /dev/null +++ b/api/messaging/spec/settings.json @@ -0,0 +1,100 @@ +{ + "Title": "Settings", + "Interface": "Settings", + "Struct": [ + { + "imports": [ + "sqlxTypes github.com/jmoiron/sqlx/types" + ] + } + ], + "Parameters": null, + "Protocol": "", + "Authentication": [], + "Path": "/settings", + "APIs": [ + { + "Name": "list", + "Method": "GET", + "Title": "List settings", + "Path": "/", + "Parameters": { + "get": [ + { + "name": "prefix", + "title": "Key prefix", + "type": "string" + } + ] + } + }, + { + "Name": "update", + "Method": "PATCH", + "Title": "Update settings", + "Path": "/", + "Parameters": { + "post": [ + { + "name": "values", + "required": true, + "title": "Array of new settings: `[{ name: ..., value: ... }]`. Omit value to remove setting", + "type": "sqlxTypes.JSONText" + } + ] + } + }, + { + "Name": "get", + "Method": "GET", + "Title": "Get a value for a key", + "Path": "/{key}", + "Parameters": { + "get": [ + { + "name": "ownerID", + "title": "Owner ID", + "type": "uint64" + } + ], + "path": [ + { + "name": "key", + "required": true, + "title": "Setting key", + "type": "string" + } + ] + } + }, + { + "Name": "set", + "Method": "PUT", + "Title": "Set a value for a key", + "Path": "/{key}", + "Parameters": { + "path": [ + { + "name": "key", + "required": true, + "title": "Setting key", + "type": "string" + } + ], + "post": [ + { + "name": "ownerID", + "title": "Owner", + "type": "uint64" + }, + { + "name": "value", + "required": true, + "title": "Setting value", + "type": "sqlxTypes.JSONText" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/docs/messaging/README.md b/docs/messaging/README.md index 812693767..a43adf030 100644 --- a/docs/messaging/README.md +++ b/docs/messaging/README.md @@ -651,6 +651,79 @@ A channel is a representation of a sequence of messages. It has meta data like c +# Settings + +| Method | Endpoint | Purpose | +| ------ | -------- | ------- | +| `GET` | `/settings/` | List settings | +| `PATCH` | `/settings/` | Update settings | +| `GET` | `/settings/{key}` | Get a value for a key | +| `PUT` | `/settings/{key}` | Set a value for a key | + +## List settings + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/settings/` | HTTP/S | GET | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| prefix | string | GET | Key prefix | N/A | NO | + +## Update settings + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/settings/` | HTTP/S | PATCH | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| values | sqlxTypes.JSONText | POST | Array of new settings: `[{ name: ..., value: ... }]`. Omit value to remove setting | N/A | YES | + +## Get a value for a key + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/settings/{key}` | HTTP/S | GET | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| ownerID | uint64 | GET | Owner ID | N/A | NO | +| key | string | PATH | Setting key | N/A | YES | + +## Set a value for a key + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/settings/{key}` | HTTP/S | PUT | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| key | string | PATH | Setting key | N/A | YES | +| ownerID | uint64 | POST | Owner | N/A | NO | +| value | sqlxTypes.JSONText | POST | Setting value | N/A | YES | + +--- + + + + # Status | Method | Endpoint | Purpose | diff --git a/messaging/db/mysql/static.go b/messaging/db/mysql/static.go index de8c8f865..aad750c90 100644 --- a/messaging/db/mysql/static.go +++ b/messaging/db/mysql/static.go @@ -3,4 +3,4 @@ // Package contains static assets. package mysql -var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8-- Keeps all known channels\nCREATE TABLE channels (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the channel\n topic TEXT NOT NULL,\n meta JSON NOT NULL,\n\n type ENUM ('private', 'public', 'group') NOT NULL DEFAULT 'public',\n\n rel_organisation BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_creator BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- channel soft delete\n\n rel_last_message BIGINT UNSIGNED NOT NULL DEFAULT 0,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- handles channel membership\nCREATE TABLE channel_members (\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n type ENUM ('owner', 'member', 'invitee') NOT NULL DEFAULT 'member',\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n\n PRIMARY KEY (rel_channel, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE channel_views (\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n -- timestamp of last view, should be enough to find out which messaghr\n viewed_at DATETIME NOT NULL DEFAULT NOW(),\n\n -- new messages count since last view\n new_since INT UNSIGNED NOT NULL DEFAULT 0,\n\n PRIMARY KEY (rel_user, rel_channel)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE channel_pins (\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n\n PRIMARY KEY (rel_channel, rel_message)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE messages (\n id BIGINT UNSIGNED NOT NULL,\n type TEXT,\n message TEXT NOT NULL,\n meta JSON,\n rel_user BIGINT UNSIGNED NOT NULL,\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n reply_to BIGINT UNSIGNED NULL REFERENCES messages(id),\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE reactions (\n id BIGINT UNSIGNED NOT NULL,\n rel_user BIGINT UNSIGNED NOT NULL,\n rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n reaction TEXT NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE attachments (\n id BIGINT UNSIGNED NOT NULL,\n rel_user BIGINT UNSIGNED NOT NULL,\n\n url VARCHAR(512),\n preview_url VARCHAR(512),\n\n size INT UNSIGNED,\n mimetype VARCHAR(255),\n name TEXT,\n\n meta JSON,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE message_attachment (\n rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),\n rel_attachment BIGINT UNSIGNED NOT NULL REFERENCES attachment(id),\n\n PRIMARY KEY (rel_message)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE event_queue (\n id BIGINT UNSIGNED NOT NULL,\n origin BIGINT UNSIGNED NOT NULL,\n subscriber TEXT,\n payload JSON,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE event_queue_synced (\n origin BIGINT UNSIGNED NOT NULL,\n rel_last BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (origin)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xd5\x9c\xef\x89V\x10\x00\x00V\x10\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00 \x0020181009080000.altering_types.up.sqlUT\x05\x00\x01\x80Cm8update channels set type = 'group' where type = 'direct';\nalter table channels CHANGE type type enum('private', 'public', 'group');\nalter table channel_members CHANGE type type enum('owner', 'member', 'invitee');\nPK\x07\x08E1\xf5\xa4\xd7\x00\x00\x00\xd7\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020181013080000.channel_views.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE channel_views DROP viewed_at;\nALTER TABLE channel_views ADD rel_last_message_id BIGINT UNSIGNED;\nALTER TABLE channel_views CHANGE new_since new_messages_count INT UNSIGNED;\n\n-- Table structure after these changes:\n-- +---------------------+---------------------+------+-----+---------+-------+\n-- | Field | Type | Null | Key | Default | Extra |\n-- +---------------------+---------------------+------+-----+---------+-------+\n-- | rel_channel | bigint(20) unsigned | NO | PRI | NULL | |\n-- | rel_user | bigint(20) unsigned | NO | PRI | NULL | |\n-- | rel_last_message_id | bigint(20) unsigned | YES | | NULL | |\n-- | new_messages_count | int(10) unsigned | NO | | 0 | |\n-- +---------------------+---------------------+------+-----+---------+-------+\n\n-- Prefill with data\nINSERT INTO channel_views (rel_channel, rel_user, rel_last_message_id)\n SELECT cm.rel_channel, cm.rel_user, max(m.ID)\n FROM channel_members AS cm INNER JOIN messages AS m ON (m.rel_channel = cm.rel_channel)\n GROUP BY cm.rel_channel, cm.rel_user;\n\nPK\x07\x08`\xcbP\xf9t\x04\x00\x00t\x04\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x00 \x0020181013080000.replies.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE messages CHANGE reply_to reply_to BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE messages ADD replies INT UNSIGNED NOT NULL DEFAULT 0;\nPK\x07\x08m\xedWA\x94\x00\x00\x00\x94\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00(\x00 \x0020181101080000.pins_and_reactions.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE channel_pins;\nDROP TABLE reactions;\n\nCREATE TABLE message_flags (\n id BIGINT UNSIGNED NOT NULL,\n rel_channel BIGINT UNSIGNED NOT NULL,\n rel_message BIGINT UNSIGNED NOT NULL,\n rel_user BIGINT UNSIGNED NOT NULL,\n flag TEXT,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08eA\x1eo\x90\x01\x00\x00\x90\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00 \x0020181107080000.mentions.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE mentions (\n id BIGINT UNSIGNED NOT NULL,\n rel_channel BIGINT UNSIGNED NOT NULL,\n rel_message BIGINT UNSIGNED NOT NULL,\n rel_user BIGINT UNSIGNED NOT NULL,\n rel_mentioned_by BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX lookup_mentions ON mentions (rel_mentioned_by)\nPK\x07\x08\xfb\xe8\x9b\x98\xac\x01\x00\x00\xac\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x00 \x0020181115080000.unreads.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE channel_views RENAME TO unreads;\n\nALTER TABLE unreads ADD rel_reply_to BIGINT UNSIGNED NOT NULL AFTER rel_channel;\nALTER TABLE unreads CHANGE rel_channel rel_channel BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE unreads CHANGE rel_user rel_user BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE unreads CHANGE rel_last_message_id rel_last_message BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE unreads CHANGE new_messages_count count INT UNSIGNED NOT NULL DEFAULT 0;\n\nPK\x07\x08jf1Q+\x02\x00\x00+\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00*\x00 \x0020181124173028.remove_events_tables.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE event_queue;\nDROP TABLE event_queue_synced;PK\x07\x08\xdd.y06\x00\x00\x006\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020181205153145.messages-to-utf8mb4.up.sqlUT\x05\x00\x01\x80Cm8alter table messages convert to character set utf8mb4 collate utf8mb4_unicode_ci;PK\x07\x08Ig\xbfOQ\x00\x00\x00Q\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00&\x00 \x0020190122191150.membership-flags.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE channel_members ADD flag ENUM ('pinned', 'hidden', 'ignored', '') NOT NULL DEFAULT '' AFTER `type`;\nPK\x07\x084\xfb\xe3\xf4p\x00\x00\x00p\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190206112022.prefix-tables.up.sqlUT\x05\x00\x01\x80Cm8-- misc tables\n\nALTER TABLE attachments RENAME TO messaging_attachment;\nALTER TABLE mentions RENAME TO messaging_mention;\nALTER TABLE unreads RENAME TO messaging_unread;\n\n-- channel tables\n\nALTER TABLE channels RENAME TO messaging_channel;\nALTER TABLE channel_members RENAME TO messaging_channel_member;\n\n-- message tables\n\nALTER TABLE messages RENAME TO messaging_message;\nALTER TABLE message_attachment RENAME TO messaging_message_attachment;\nALTER TABLE message_flags RENAME TO messaging_message_flag;\nPK\x07\x08\x145\xde}Q\x02\x00\x00Q\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190326181923.webhook-table.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `messaging_webhook` (\n `id` bigint(20) unsigned NOT NULL,\n `kind` varchar(8) NOT NULL COMMENT 'Kind: incoming, outgoing',\n `token` varchar(255) NOT NULL COMMENT 'Authentication token',\n `rel_owner` bigint(20) unsigned NOT NULL COMMENT 'Webhook owner User ID',\n `rel_user` bigint(20) unsigned NOT NULL COMMENT 'Webhook message User ID',\n `rel_channel` bigint(20) unsigned NOT NULL COMMENT 'Channel ID',\n `outgoing_trigger` varchar(32) NOT NULL COMMENT 'Outgoing command trigger',\n `outgoing_url` varchar(255) NOT NULL COMMENT 'URL for POST request',\n `created_at` datetime NOT NULL,\n `updated_at` datetime NULL,\n `deleted_at` datetime NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- get webhook by command trigger\nALTER TABLE `messaging_webhook` ADD UNIQUE(`outgoing_trigger`);\n\n-- list webhooks by owner (list your own webhooks)\nALTER TABLE `messaging_webhook` ADD INDEX(`rel_owner`);\n\n-- list webhooks on a channel\nALTER TABLE `messaging_webhook` ADD INDEX(`rel_channel`);\nPK\x07\x08\x16\x95.\xf3\xf7\x03\x00\x00\xf7\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020190526090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS messaging_permission_rules (\n rel_role BIGINT UNSIGNED NOT NULL,\n resource VARCHAR(128) NOT NULL,\n operation VARCHAR(128) NOT NULL,\n access TINYINT(1) NOT NULL,\n\n PRIMARY KEY (rel_role, resource, operation)\n) ENGINE=InnoDB;\nPK\x07\x08\xf0d&V\x14\x01\x00\x00\x14\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x00 \x0020190623080000.unreads.up.sqlUT\x05\x00\x01\x80Cm8UPDATE `messaging_unread` SET rel_reply_to = 0 WHERE rel_reply_to IS NULL;\nALTER TABLE `messaging_unread` CHANGE COLUMN `rel_reply_to` `rel_reply_to` BIGINT UNSIGNED NOT NULL;\nALTER TABLE `messaging_unread` DROP PRIMARY KEY, ADD PRIMARY KEY(`rel_channel`, `rel_reply_to`, `rel_user`);\n\n-- Add entries for all (unexisting) unreads (channels & threads)\nINSERT IGNORE INTO messaging_unread\n (rel_channel, rel_reply_to, rel_user)\nSELECT DISTINCT cm.rel_channel, msg.id, cm.rel_user\n FROM messaging_channel_member AS cm\n INNER JOIN messaging_message AS msg ON (cm.rel_channel = msg.rel_channel AND replies > 0)\n WHERE NOT EXISTS (SELECT 1 FROM messaging_unread AS u WHERE u.rel_reply_to = msg.id AND u.rel_user = cm.rel_user)\n AND msg.rel_user > 0\n\nUNION\n\nSELECT DISTINCT cm.rel_channel, 0, cm.rel_user\n FROM messaging_channel_member AS cm\n WHERE NOT EXISTS (SELECT 1 FROM messaging_unread AS u WHERE u.rel_channel = cm.rel_channel AND u.rel_user = cm.rel_user)\n AND cm.rel_user > 0\n;\n\n\n-- Update counters for channel messages\nINSERT IGNORE INTO messaging_unread\n (rel_channel, rel_reply_to, rel_user, count, rel_last_message)\nSELECT u.rel_channel, 0, u.rel_user, COUNT(m.id), u.rel_last_message\n FROM messaging_unread AS u\n INNER JOIN messaging_message AS m ON (u.rel_channel = m.rel_channel AND m.id > u.rel_last_message)\n WHERE u.rel_reply_to = 0\n AND m.reply_to = 0\n GROUP BY u.rel_channel, u.rel_user;\n\n-- Update counters for thread messages\n\nINSERT IGNORE INTO messaging_unread\n (rel_channel, rel_reply_to, rel_user, count, rel_last_message)\nSELECT u.rel_channel, rpl.reply_to, u.rel_user, COUNT(rpl.id), u.rel_last_message\n FROM messaging_unread AS u\n INNER JOIN messaging_message AS rpl ON (u.rel_channel = rpl.rel_channel AND rpl.reply_to = u.rel_reply_to AND rpl.id > u.rel_last_message)\n WHERE rpl.replies > 0 AND u.rel_reply_to > 0\n GROUP BY u.rel_channel, rpl.reply_to, u.rel_user;\nPK\x07\x08\xa3(M\xda\xa1\x07\x00\x00\xa1\x07\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00/\x00 \x0020190808000000.channel_membership_policy.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `messaging_channel` ADD `membership_policy` ENUM ('featured', 'forced', '') NOT NULL DEFAULT '' AFTER `type`;\nPK\x07\x08E\xa4\xe3\xf0z\x00\x00\x00z\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` TEXT NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sqlPK\x07\x08s\xd4N*.\x00\x00\x00.\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xd5\x9c\xef\x89V\x10\x00\x00V\x10\x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(E1\xf5\xa4\xd7\x00\x00\x00\xd7\x00\x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xa7\x10\x00\x0020181009080000.altering_types.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(`\xcbP\xf9t\x04\x00\x00t\x04\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd9\x11\x00\x0020181013080000.channel_views.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(m\xedWA\x94\x00\x00\x00\x94\x00\x00\x00\x1d\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xa7\x16\x00\x0020181013080000.replies.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(eA\x1eo\x90\x01\x00\x00\x90\x01\x00\x00(\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x8f\x17\x00\x0020181101080000.pins_and_reactions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xfb\xe8\x9b\x98\xac\x01\x00\x00\xac\x01\x00\x00\x1e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81~\x19\x00\x0020181107080000.mentions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(jf1Q+\x02\x00\x00+\x02\x00\x00\x1d\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x7f\x1b\x00\x0020181115080000.unreads.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xdd.y06\x00\x00\x006\x00\x00\x00*\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xfe\x1d\x00\x0020181124173028.remove_events_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(Ig\xbfOQ\x00\x00\x00Q\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x95\x1e\x00\x0020181205153145.messages-to-utf8mb4.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(4\xfb\xe3\xf4p\x00\x00\x00p\x00\x00\x00&\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81F\x1f\x00\x0020190122191150.membership-flags.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x145\xde}Q\x02\x00\x00Q\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x13 \x00\x0020190206112022.prefix-tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x16\x95.\xf3\xf7\x03\x00\x00\xf7\x03\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbe\"\x00\x0020190326181923.webhook-table.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf0d&V\x14\x01\x00\x00\x14\x01\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x0f'\x00\x0020190526090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xa3(M\xda\xa1\x07\x00\x00\xa1\x07\x00\x00\x1d\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81{(\x00\x0020190623080000.unreads.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(E\xa4\xe3\xf0z\x00\x00\x00z\x00\x00\x00/\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81p0\x00\x0020190808000000.channel_membership_policy.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81P1\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81\x0d3\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x11\x00\x11\x00\xc8\x05\x00\x00x3\x00\x00\x00\x00" +var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8-- Keeps all known channels\nCREATE TABLE channels (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the channel\n topic TEXT NOT NULL,\n meta JSON NOT NULL,\n\n type ENUM ('private', 'public', 'group') NOT NULL DEFAULT 'public',\n\n rel_organisation BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_creator BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- channel soft delete\n\n rel_last_message BIGINT UNSIGNED NOT NULL DEFAULT 0,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- handles channel membership\nCREATE TABLE channel_members (\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n type ENUM ('owner', 'member', 'invitee') NOT NULL DEFAULT 'member',\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n\n PRIMARY KEY (rel_channel, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE channel_views (\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n -- timestamp of last view, should be enough to find out which messaghr\n viewed_at DATETIME NOT NULL DEFAULT NOW(),\n\n -- new messages count since last view\n new_since INT UNSIGNED NOT NULL DEFAULT 0,\n\n PRIMARY KEY (rel_user, rel_channel)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE channel_pins (\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n\n PRIMARY KEY (rel_channel, rel_message)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE messages (\n id BIGINT UNSIGNED NOT NULL,\n type TEXT,\n message TEXT NOT NULL,\n meta JSON,\n rel_user BIGINT UNSIGNED NOT NULL,\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n reply_to BIGINT UNSIGNED NULL REFERENCES messages(id),\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE reactions (\n id BIGINT UNSIGNED NOT NULL,\n rel_user BIGINT UNSIGNED NOT NULL,\n rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),\n rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),\n reaction TEXT NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE attachments (\n id BIGINT UNSIGNED NOT NULL,\n rel_user BIGINT UNSIGNED NOT NULL,\n\n url VARCHAR(512),\n preview_url VARCHAR(512),\n\n size INT UNSIGNED,\n mimetype VARCHAR(255),\n name TEXT,\n\n meta JSON,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE message_attachment (\n rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),\n rel_attachment BIGINT UNSIGNED NOT NULL REFERENCES attachment(id),\n\n PRIMARY KEY (rel_message)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE event_queue (\n id BIGINT UNSIGNED NOT NULL,\n origin BIGINT UNSIGNED NOT NULL,\n subscriber TEXT,\n payload JSON,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE event_queue_synced (\n origin BIGINT UNSIGNED NOT NULL,\n rel_last BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (origin)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xd5\x9c\xef\x89V\x10\x00\x00V\x10\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00 \x0020181009080000.altering_types.up.sqlUT\x05\x00\x01\x80Cm8update channels set type = 'group' where type = 'direct';\nalter table channels CHANGE type type enum('private', 'public', 'group');\nalter table channel_members CHANGE type type enum('owner', 'member', 'invitee');\nPK\x07\x08E1\xf5\xa4\xd7\x00\x00\x00\xd7\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020181013080000.channel_views.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE channel_views DROP viewed_at;\nALTER TABLE channel_views ADD rel_last_message_id BIGINT UNSIGNED;\nALTER TABLE channel_views CHANGE new_since new_messages_count INT UNSIGNED;\n\n-- Table structure after these changes:\n-- +---------------------+---------------------+------+-----+---------+-------+\n-- | Field | Type | Null | Key | Default | Extra |\n-- +---------------------+---------------------+------+-----+---------+-------+\n-- | rel_channel | bigint(20) unsigned | NO | PRI | NULL | |\n-- | rel_user | bigint(20) unsigned | NO | PRI | NULL | |\n-- | rel_last_message_id | bigint(20) unsigned | YES | | NULL | |\n-- | new_messages_count | int(10) unsigned | NO | | 0 | |\n-- +---------------------+---------------------+------+-----+---------+-------+\n\n-- Prefill with data\nINSERT INTO channel_views (rel_channel, rel_user, rel_last_message_id)\n SELECT cm.rel_channel, cm.rel_user, max(m.ID)\n FROM channel_members AS cm INNER JOIN messages AS m ON (m.rel_channel = cm.rel_channel)\n GROUP BY cm.rel_channel, cm.rel_user;\n\nPK\x07\x08`\xcbP\xf9t\x04\x00\x00t\x04\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x00 \x0020181013080000.replies.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE messages CHANGE reply_to reply_to BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE messages ADD replies INT UNSIGNED NOT NULL DEFAULT 0;\nPK\x07\x08m\xedWA\x94\x00\x00\x00\x94\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00(\x00 \x0020181101080000.pins_and_reactions.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE channel_pins;\nDROP TABLE reactions;\n\nCREATE TABLE message_flags (\n id BIGINT UNSIGNED NOT NULL,\n rel_channel BIGINT UNSIGNED NOT NULL,\n rel_message BIGINT UNSIGNED NOT NULL,\n rel_user BIGINT UNSIGNED NOT NULL,\n flag TEXT,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08eA\x1eo\x90\x01\x00\x00\x90\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00 \x0020181107080000.mentions.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE mentions (\n id BIGINT UNSIGNED NOT NULL,\n rel_channel BIGINT UNSIGNED NOT NULL,\n rel_message BIGINT UNSIGNED NOT NULL,\n rel_user BIGINT UNSIGNED NOT NULL,\n rel_mentioned_by BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX lookup_mentions ON mentions (rel_mentioned_by)\nPK\x07\x08\xfb\xe8\x9b\x98\xac\x01\x00\x00\xac\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x00 \x0020181115080000.unreads.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE channel_views RENAME TO unreads;\n\nALTER TABLE unreads ADD rel_reply_to BIGINT UNSIGNED NOT NULL AFTER rel_channel;\nALTER TABLE unreads CHANGE rel_channel rel_channel BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE unreads CHANGE rel_user rel_user BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE unreads CHANGE rel_last_message_id rel_last_message BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE unreads CHANGE new_messages_count count INT UNSIGNED NOT NULL DEFAULT 0;\n\nPK\x07\x08jf1Q+\x02\x00\x00+\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00*\x00 \x0020181124173028.remove_events_tables.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE event_queue;\nDROP TABLE event_queue_synced;PK\x07\x08\xdd.y06\x00\x00\x006\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020181205153145.messages-to-utf8mb4.up.sqlUT\x05\x00\x01\x80Cm8alter table messages convert to character set utf8mb4 collate utf8mb4_unicode_ci;PK\x07\x08Ig\xbfOQ\x00\x00\x00Q\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00&\x00 \x0020190122191150.membership-flags.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE channel_members ADD flag ENUM ('pinned', 'hidden', 'ignored', '') NOT NULL DEFAULT '' AFTER `type`;\nPK\x07\x084\xfb\xe3\xf4p\x00\x00\x00p\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190206112022.prefix-tables.up.sqlUT\x05\x00\x01\x80Cm8-- misc tables\n\nALTER TABLE attachments RENAME TO messaging_attachment;\nALTER TABLE mentions RENAME TO messaging_mention;\nALTER TABLE unreads RENAME TO messaging_unread;\n\n-- channel tables\n\nALTER TABLE channels RENAME TO messaging_channel;\nALTER TABLE channel_members RENAME TO messaging_channel_member;\n\n-- message tables\n\nALTER TABLE messages RENAME TO messaging_message;\nALTER TABLE message_attachment RENAME TO messaging_message_attachment;\nALTER TABLE message_flags RENAME TO messaging_message_flag;\nPK\x07\x08\x145\xde}Q\x02\x00\x00Q\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190326181923.webhook-table.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `messaging_webhook` (\n `id` bigint(20) unsigned NOT NULL,\n `kind` varchar(8) NOT NULL COMMENT 'Kind: incoming, outgoing',\n `token` varchar(255) NOT NULL COMMENT 'Authentication token',\n `rel_owner` bigint(20) unsigned NOT NULL COMMENT 'Webhook owner User ID',\n `rel_user` bigint(20) unsigned NOT NULL COMMENT 'Webhook message User ID',\n `rel_channel` bigint(20) unsigned NOT NULL COMMENT 'Channel ID',\n `outgoing_trigger` varchar(32) NOT NULL COMMENT 'Outgoing command trigger',\n `outgoing_url` varchar(255) NOT NULL COMMENT 'URL for POST request',\n `created_at` datetime NOT NULL,\n `updated_at` datetime NULL,\n `deleted_at` datetime NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- get webhook by command trigger\nALTER TABLE `messaging_webhook` ADD UNIQUE(`outgoing_trigger`);\n\n-- list webhooks by owner (list your own webhooks)\nALTER TABLE `messaging_webhook` ADD INDEX(`rel_owner`);\n\n-- list webhooks on a channel\nALTER TABLE `messaging_webhook` ADD INDEX(`rel_channel`);\nPK\x07\x08\x16\x95.\xf3\xf7\x03\x00\x00\xf7\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020190526090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS messaging_permission_rules (\n rel_role BIGINT UNSIGNED NOT NULL,\n resource VARCHAR(128) NOT NULL,\n operation VARCHAR(128) NOT NULL,\n access TINYINT(1) NOT NULL,\n\n PRIMARY KEY (rel_role, resource, operation)\n) ENGINE=InnoDB;\nPK\x07\x08\xf0d&V\x14\x01\x00\x00\x14\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x00 \x0020190623080000.unreads.up.sqlUT\x05\x00\x01\x80Cm8UPDATE `messaging_unread` SET rel_reply_to = 0 WHERE rel_reply_to IS NULL;\nALTER TABLE `messaging_unread` CHANGE COLUMN `rel_reply_to` `rel_reply_to` BIGINT UNSIGNED NOT NULL;\nALTER TABLE `messaging_unread` DROP PRIMARY KEY, ADD PRIMARY KEY(`rel_channel`, `rel_reply_to`, `rel_user`);\n\n-- Add entries for all (unexisting) unreads (channels & threads)\nINSERT IGNORE INTO messaging_unread\n (rel_channel, rel_reply_to, rel_user)\nSELECT DISTINCT cm.rel_channel, msg.id, cm.rel_user\n FROM messaging_channel_member AS cm\n INNER JOIN messaging_message AS msg ON (cm.rel_channel = msg.rel_channel AND replies > 0)\n WHERE NOT EXISTS (SELECT 1 FROM messaging_unread AS u WHERE u.rel_reply_to = msg.id AND u.rel_user = cm.rel_user)\n AND msg.rel_user > 0\n\nUNION\n\nSELECT DISTINCT cm.rel_channel, 0, cm.rel_user\n FROM messaging_channel_member AS cm\n WHERE NOT EXISTS (SELECT 1 FROM messaging_unread AS u WHERE u.rel_channel = cm.rel_channel AND u.rel_user = cm.rel_user)\n AND cm.rel_user > 0\n;\n\n\n-- Update counters for channel messages\nINSERT IGNORE INTO messaging_unread\n (rel_channel, rel_reply_to, rel_user, count, rel_last_message)\nSELECT u.rel_channel, 0, u.rel_user, COUNT(m.id), u.rel_last_message\n FROM messaging_unread AS u\n INNER JOIN messaging_message AS m ON (u.rel_channel = m.rel_channel AND m.id > u.rel_last_message)\n WHERE u.rel_reply_to = 0\n AND m.reply_to = 0\n GROUP BY u.rel_channel, u.rel_user;\n\n-- Update counters for thread messages\n\nINSERT IGNORE INTO messaging_unread\n (rel_channel, rel_reply_to, rel_user, count, rel_last_message)\nSELECT u.rel_channel, rpl.reply_to, u.rel_user, COUNT(rpl.id), u.rel_last_message\n FROM messaging_unread AS u\n INNER JOIN messaging_message AS rpl ON (u.rel_channel = rpl.rel_channel AND rpl.reply_to = u.rel_reply_to AND rpl.id > u.rel_last_message)\n WHERE rpl.replies > 0 AND u.rel_reply_to > 0\n GROUP BY u.rel_channel, rpl.reply_to, u.rel_user;\nPK\x07\x08\xa3(M\xda\xa1\x07\x00\x00\xa1\x07\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00/\x00 \x0020190808000000.channel_membership_policy.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `messaging_channel` ADD `membership_policy` ENUM ('featured', 'forced', '') NOT NULL DEFAULT '' AFTER `type`;\nPK\x07\x08E\xa4\xe3\xf0z\x00\x00\x00z\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00 \x0020191008125405.settings.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `messaging_settings` (\n rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings',\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value JSON COMMENT 'Setting value',\n\n updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated',\n updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value',\n\n PRIMARY KEY (name, rel_owner)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xab\xbe\x82\xefX\x02\x00\x00X\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` TEXT NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sqlPK\x07\x08s\xd4N*.\x00\x00\x00.\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xd5\x9c\xef\x89V\x10\x00\x00V\x10\x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(E1\xf5\xa4\xd7\x00\x00\x00\xd7\x00\x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xa7\x10\x00\x0020181009080000.altering_types.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(`\xcbP\xf9t\x04\x00\x00t\x04\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd9\x11\x00\x0020181013080000.channel_views.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(m\xedWA\x94\x00\x00\x00\x94\x00\x00\x00\x1d\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xa7\x16\x00\x0020181013080000.replies.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(eA\x1eo\x90\x01\x00\x00\x90\x01\x00\x00(\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x8f\x17\x00\x0020181101080000.pins_and_reactions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xfb\xe8\x9b\x98\xac\x01\x00\x00\xac\x01\x00\x00\x1e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81~\x19\x00\x0020181107080000.mentions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(jf1Q+\x02\x00\x00+\x02\x00\x00\x1d\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x7f\x1b\x00\x0020181115080000.unreads.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xdd.y06\x00\x00\x006\x00\x00\x00*\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xfe\x1d\x00\x0020181124173028.remove_events_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(Ig\xbfOQ\x00\x00\x00Q\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x95\x1e\x00\x0020181205153145.messages-to-utf8mb4.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(4\xfb\xe3\xf4p\x00\x00\x00p\x00\x00\x00&\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81F\x1f\x00\x0020190122191150.membership-flags.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x145\xde}Q\x02\x00\x00Q\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x13 \x00\x0020190206112022.prefix-tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x16\x95.\xf3\xf7\x03\x00\x00\xf7\x03\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbe\"\x00\x0020190326181923.webhook-table.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf0d&V\x14\x01\x00\x00\x14\x01\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x0f'\x00\x0020190526090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xa3(M\xda\xa1\x07\x00\x00\xa1\x07\x00\x00\x1d\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81{(\x00\x0020190623080000.unreads.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(E\xa4\xe3\xf0z\x00\x00\x00z\x00\x00\x00/\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81p0\x00\x0020190808000000.channel_membership_policy.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xab\xbe\x82\xefX\x02\x00\x00X\x02\x00\x00\x1e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81P1\x00\x0020191008125405.settings.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xfd3\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81\xba5\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x12\x00\x12\x00\x1d\x06\x00\x00%6\x00\x00\x00\x00" diff --git a/messaging/db/schema/mysql/20191008125405.settings.up.sql b/messaging/db/schema/mysql/20191008125405.settings.up.sql new file mode 100644 index 000000000..7f27ad421 --- /dev/null +++ b/messaging/db/schema/mysql/20191008125405.settings.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS `messaging_settings` ( + rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings', + name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys', + value JSON COMMENT 'Setting value', + + updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated', + updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value', + + PRIMARY KEY (name, rel_owner) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/messaging/rest/handlers/settings.go b/messaging/rest/handlers/settings.go new file mode 100644 index 000000000..4339affaa --- /dev/null +++ b/messaging/rest/handlers/settings.go @@ -0,0 +1,139 @@ +package handlers + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `settings.go`, `settings.util.go` or `settings_test.go` to + implement your API calls, helper functions and tests. The file `settings.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "context" + + "net/http" + + "github.com/go-chi/chi" + "github.com/titpetric/factory/resputil" + + "github.com/cortezaproject/corteza-server/messaging/rest/request" + "github.com/cortezaproject/corteza-server/pkg/logger" +) + +// Internal API interface +type SettingsAPI interface { + List(context.Context, *request.SettingsList) (interface{}, error) + Update(context.Context, *request.SettingsUpdate) (interface{}, error) + Get(context.Context, *request.SettingsGet) (interface{}, error) + Set(context.Context, *request.SettingsSet) (interface{}, error) +} + +// HTTP API interface +type Settings struct { + List func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Get func(http.ResponseWriter, *http.Request) + Set func(http.ResponseWriter, *http.Request) +} + +func NewSettings(h SettingsAPI) *Settings { + return &Settings{ + List: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewSettingsList() + if err := params.Fill(r); err != nil { + logger.LogParamError("Settings.List", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.List(r.Context(), params) + if err != nil { + logger.LogControllerError("Settings.List", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Settings.List", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Update: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewSettingsUpdate() + if err := params.Fill(r); err != nil { + logger.LogParamError("Settings.Update", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Update(r.Context(), params) + if err != nil { + logger.LogControllerError("Settings.Update", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Settings.Update", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Get: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewSettingsGet() + if err := params.Fill(r); err != nil { + logger.LogParamError("Settings.Get", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Get(r.Context(), params) + if err != nil { + logger.LogControllerError("Settings.Get", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Settings.Get", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Set: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewSettingsSet() + if err := params.Fill(r); err != nil { + logger.LogParamError("Settings.Set", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Set(r.Context(), params) + if err != nil { + logger.LogControllerError("Settings.Set", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Settings.Set", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + } +} + +func (h Settings) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Get("/settings/", h.List) + r.Patch("/settings/", h.Update) + r.Get("/settings/{key}", h.Get) + r.Put("/settings/{key}", h.Set) + }) +} diff --git a/messaging/rest/request/settings.go b/messaging/rest/request/settings.go new file mode 100644 index 000000000..368166e14 --- /dev/null +++ b/messaging/rest/request/settings.go @@ -0,0 +1,262 @@ +package request + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `settings.go`, `settings.util.go` or `settings_test.go` to + implement your API calls, helper functions and tests. The file `settings.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "io" + "strings" + + "encoding/json" + "mime/multipart" + "net/http" + + "github.com/go-chi/chi" + "github.com/pkg/errors" + + sqlxTypes "github.com/jmoiron/sqlx/types" +) + +var _ = chi.URLParam +var _ = multipart.FileHeader{} + +// Settings list request parameters +type SettingsList struct { + Prefix string +} + +func NewSettingsList() *SettingsList { + return &SettingsList{} +} + +func (r SettingsList) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["prefix"] = r.Prefix + + return out +} + +func (r *SettingsList) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := get["prefix"]; ok { + r.Prefix = val + } + + return err +} + +var _ RequestFiller = NewSettingsList() + +// Settings update request parameters +type SettingsUpdate struct { + Values sqlxTypes.JSONText +} + +func NewSettingsUpdate() *SettingsUpdate { + return &SettingsUpdate{} +} + +func (r SettingsUpdate) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["values"] = r.Values + + return out +} + +func (r *SettingsUpdate) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := post["values"]; ok { + + if r.Values, err = parseJSONTextWithErr(val); err != nil { + return err + } + } + + return err +} + +var _ RequestFiller = NewSettingsUpdate() + +// Settings get request parameters +type SettingsGet struct { + OwnerID uint64 `json:",string"` + Key string +} + +func NewSettingsGet() *SettingsGet { + return &SettingsGet{} +} + +func (r SettingsGet) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["ownerID"] = r.OwnerID + out["key"] = r.Key + + return out +} + +func (r *SettingsGet) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := get["ownerID"]; ok { + r.OwnerID = parseUInt64(val) + } + r.Key = chi.URLParam(req, "key") + + return err +} + +var _ RequestFiller = NewSettingsGet() + +// Settings set request parameters +type SettingsSet struct { + Key string + OwnerID uint64 `json:",string"` + Value sqlxTypes.JSONText +} + +func NewSettingsSet() *SettingsSet { + return &SettingsSet{} +} + +func (r SettingsSet) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["key"] = r.Key + out["ownerID"] = r.OwnerID + out["value"] = r.Value + + return out +} + +func (r *SettingsSet) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.Key = chi.URLParam(req, "key") + if val, ok := post["ownerID"]; ok { + r.OwnerID = parseUInt64(val) + } + if val, ok := post["value"]; ok { + + if r.Value, err = parseJSONTextWithErr(val); err != nil { + return err + } + } + + return err +} + +var _ RequestFiller = NewSettingsSet() diff --git a/messaging/rest/request/util.go b/messaging/rest/request/util.go index b5dd1757b..f7ca5b403 100644 --- a/messaging/rest/request/util.go +++ b/messaging/rest/request/util.go @@ -13,6 +13,12 @@ import ( var truthy = regexp.MustCompile(`^\s*(t(rue)?|y(es)?|1)\s*$`) +func parseJSONTextWithErr(s string) (types.JSONText, error) { + result := &types.JSONText{} + err := errors.Wrap(result.Scan(s), "error when parsing JSONText") + return *result, err +} + func parseJSONText(s string) (types.JSONText, error) { result := &types.JSONText{} err := errors.Wrap(result.Scan(s), "error when parsing JSONText") diff --git a/messaging/rest/router.go b/messaging/rest/router.go index 83c9a0714..19259996e 100644 --- a/messaging/rest/router.go +++ b/messaging/rest/router.go @@ -27,5 +27,6 @@ func MountRoutes(r chi.Router) { handlers.NewCommands(Commands{}.New()).MountRoutes(r) handlers.NewWebhooks(Webhooks{}.New()).MountRoutes(r) handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) + handlers.NewSettings(Settings{}.New()).MountRoutes(r) }) } diff --git a/messaging/rest/settings.go b/messaging/rest/settings.go new file mode 100644 index 000000000..88e306b8b --- /dev/null +++ b/messaging/rest/settings.go @@ -0,0 +1,61 @@ +package rest + +import ( + "context" + + "github.com/pkg/errors" + "github.com/titpetric/factory/resputil" + + "github.com/cortezaproject/corteza-server/messaging/rest/request" + "github.com/cortezaproject/corteza-server/messaging/service" + "github.com/cortezaproject/corteza-server/pkg/settings" +) + +var _ = errors.Wrap + +type ( + Settings struct { + svc struct { + settings service.SettingsService + } + } +) + +func (Settings) New() *Settings { + ctrl := &Settings{} + ctrl.svc.settings = service.DefaultSettings + + return ctrl +} + +func (ctrl *Settings) List(ctx context.Context, r *request.SettingsList) (interface{}, error) { + if vv, err := ctrl.svc.settings.With(ctx).FindByPrefix(r.Prefix); err != nil { + return nil, err + } else { + return vv, err + } +} + +func (ctrl *Settings) Update(ctx context.Context, r *request.SettingsUpdate) (interface{}, error) { + values := settings.ValueSet{} + + if err := r.Values.Unmarshal(&values); err != nil { + return nil, err + } else if err := ctrl.svc.settings.With(ctx).BulkSet(values); err != nil { + return nil, err + } else { + return true, nil + } +} + +func (ctrl *Settings) Get(ctx context.Context, r *request.SettingsGet) (interface{}, error) { + if v, err := ctrl.svc.settings.With(ctx).Get(r.Key, r.OwnerID); err != nil { + return nil, err + } else { + return v, nil + } +} + +func (ctrl *Settings) Set(ctx context.Context, r *request.SettingsSet) (interface{}, error) { + return resputil.OK(), errors.New("Not implemented: Settings.set") +} diff --git a/messaging/service/access_control.go b/messaging/service/access_control.go index a05901cb1..8f92aa3a8 100644 --- a/messaging/service/access_control.go +++ b/messaging/service/access_control.go @@ -51,6 +51,14 @@ func (svc accessControl) CanGrant(ctx context.Context) bool { return svc.can(ctx, types.MessagingPermissionResource, "grant") } +func (svc accessControl) CanReadSettings(ctx context.Context) bool { + return svc.can(ctx, types.MessagingPermissionResource, "settings.read") +} + +func (svc accessControl) CanManageSettings(ctx context.Context) bool { + return svc.can(ctx, types.MessagingPermissionResource, "settings.manage") +} + func (svc accessControl) CanCreatePublicChannel(ctx context.Context) bool { return svc.can(ctx, types.MessagingPermissionResource, "channel.public.create", permissions.Allowed) } diff --git a/messaging/service/service.go b/messaging/service/service.go index 6d9bfa836..3546eb96b 100644 --- a/messaging/service/service.go +++ b/messaging/service/service.go @@ -10,6 +10,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/cli/options" "github.com/cortezaproject/corteza-server/pkg/http" "github.com/cortezaproject/corteza-server/pkg/permissions" + internalSettings "github.com/cortezaproject/corteza-server/pkg/settings" "github.com/cortezaproject/corteza-server/pkg/store" "github.com/cortezaproject/corteza-server/pkg/store/minio" "github.com/cortezaproject/corteza-server/pkg/store/plain" @@ -36,7 +37,9 @@ var ( DefaultLogger *zap.Logger - DefaultAccessControl *accessControl + DefaultInternalSettings internalSettings.Service + DefaultSettings SettingsService + DefaultAccessControl *accessControl DefaultAttachment AttachmentService DefaultChannel ChannelService @@ -49,6 +52,8 @@ var ( func Init(ctx context.Context, log *zap.Logger, c Config) (err error) { DefaultLogger = log.Named("service") + DefaultInternalSettings = internalSettings.NewService(internalSettings.NewRepository(repository.DB(ctx), "messaging_settings")) + if DefaultStore == nil { if c.Storage.MinioEndpoint != "" { if c.Storage.MinioBucket == "" { @@ -97,6 +102,8 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) { } DefaultAccessControl = AccessControl(DefaultPermissions) + DefaultSettings = Settings(ctx, DefaultInternalSettings) + DefaultEvent = Event(ctx) DefaultChannel = Channel(ctx) DefaultAttachment = Attachment(ctx, DefaultStore) diff --git a/messaging/service/settings.go b/messaging/service/settings.go new file mode 100644 index 000000000..fe16dcc15 --- /dev/null +++ b/messaging/service/settings.go @@ -0,0 +1,96 @@ +package service + +import ( + "context" + + "github.com/pkg/errors" + "github.com/titpetric/factory" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/cortezaproject/corteza-server/messaging/repository" + "github.com/cortezaproject/corteza-server/pkg/logger" + internalSettings "github.com/cortezaproject/corteza-server/pkg/settings" +) + +type ( + // Wrapper service for messaging around internal settings service + settings struct { + ctx context.Context + db *factory.DB + logger *zap.Logger + + ac settingsAccessController + internalSettings internalSettings.Service + } + + settingsAccessController interface { + CanReadSettings(ctx context.Context) bool + CanManageSettings(ctx context.Context) bool + } + + SettingsService interface { + With(ctx context.Context) SettingsService + FindByPrefix(prefix string) (vv internalSettings.ValueSet, err error) + Set(v *internalSettings.Value) (err error) + BulkSet(vv internalSettings.ValueSet) (err error) + Get(name string, ownedBy uint64) (out *internalSettings.Value, err error) + } +) + +func Settings(ctx context.Context, intSet internalSettings.Service) SettingsService { + return (&settings{ + internalSettings: intSet, + ac: DefaultAccessControl, + logger: DefaultLogger.Named("settings"), + }).With(ctx) +} + +func (svc settings) With(ctx context.Context) SettingsService { + db := repository.DB(ctx) + + return &settings{ + ctx: ctx, + db: db, + ac: svc.ac, + logger: svc.logger, + + internalSettings: svc.internalSettings.With(ctx), + } +} + +func (svc settings) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger { + return logger.AddRequestID(ctx, svc.logger).With(fields...) +} + +func (svc settings) FindByPrefix(prefix string) (vv internalSettings.ValueSet, err error) { + if !svc.ac.CanReadSettings(svc.ctx) { + return nil, errors.New("not allowed to read settings") + } + + return svc.internalSettings.FindByPrefix(prefix) +} + +func (svc settings) Set(v *internalSettings.Value) (err error) { + if !svc.ac.CanManageSettings(svc.ctx) { + return errors.New("not allowed to manage settings") + } + + return svc.internalSettings.Set(v) +} + +func (svc settings) BulkSet(vv internalSettings.ValueSet) (err error) { + if !svc.ac.CanManageSettings(svc.ctx) { + return errors.New("not allowed to manage settings") + } + + return svc.internalSettings.BulkSet(vv) +} + +func (svc settings) Get(name string, ownedBy uint64) (out *internalSettings.Value, err error) { + if !svc.ac.CanReadSettings(svc.ctx) { + return nil, errors.New("not allowed to read settings") + } + + return svc.internalSettings.Get(name, ownedBy) +}