mirror of
https://github.com/wassname/talk.git
synced 2026-07-15 11:26:58 +08:00
Tag server impl
This commit is contained in:
+24
@@ -307,6 +307,30 @@ module.exports = {
|
||||
}
|
||||
```
|
||||
|
||||
#### Field: `tags`
|
||||
|
||||
The tags hook allows a plugin to define tags that are code controlled (added
|
||||
or enabled by code). Below is an example pulled from the core off topic plugin
|
||||
on how to create a hook for the `OFF_TOPIC` name:
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
name: 'OFF_TOPIC',
|
||||
permissions: {
|
||||
public: true,
|
||||
self: true,
|
||||
roles: []
|
||||
},
|
||||
models: ['COMMENTS'],
|
||||
created_at: new Date()
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
You can refer to `models/schema/tag.js` for the available schema to match when
|
||||
creating models to enable/disable specific features.
|
||||
|
||||
#### Field: `passport`
|
||||
|
||||
```js
|
||||
|
||||
@@ -6,6 +6,7 @@ const Assets = require('./assets');
|
||||
const Comments = require('./comments');
|
||||
const Metrics = require('./metrics');
|
||||
const Settings = require('./settings');
|
||||
const Tags = require('./tags');
|
||||
const Users = require('./users');
|
||||
|
||||
const plugins = require('../../services/plugins');
|
||||
@@ -18,6 +19,7 @@ let loaders = [
|
||||
Comments,
|
||||
Metrics,
|
||||
Settings,
|
||||
Tags,
|
||||
Users,
|
||||
|
||||
// Load the plugin loaders from the manager.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const DataLoader = require('dataloader');
|
||||
const TagsService = require('../../services/tags');
|
||||
|
||||
/**
|
||||
* Get all the tags for the context for the dataloader.
|
||||
*/
|
||||
const genAll = (context, queries) => {
|
||||
return Promise.all(queries.map(({id, item_type, asset_id}) => {
|
||||
return TagsService.getAll({id, item_type, asset_id});
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
* @return {Object} object of loaders
|
||||
*/
|
||||
module.exports = (context) => ({
|
||||
Tags: {
|
||||
getAll: new DataLoader((queries) => genAll(context, queries))
|
||||
}
|
||||
});
|
||||
+55
-19
@@ -2,11 +2,61 @@ const errors = require('../../errors');
|
||||
|
||||
const AssetsService = require('../../services/assets');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const TagsService = require('../../services/tags');
|
||||
const CommentsService = require('../../services/comments');
|
||||
const linkify = require('linkify-it')();
|
||||
|
||||
const Wordlist = require('../../services/wordlist');
|
||||
|
||||
const debug = require('debug')('talk:graph:mutators:tags');
|
||||
const plugins = require('../../services/plugins');
|
||||
|
||||
const pluginTags = plugins.get('server', 'tags').reduce((acc, {plugin, tags}) => {
|
||||
debug(`added plugin '${plugin.name}'`);
|
||||
|
||||
acc = acc.concat(tags);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags = []}) => {
|
||||
const item_type = 'COMMENTS';
|
||||
|
||||
// Handle Tags
|
||||
if (tags.length) {
|
||||
|
||||
// Get the global list of tags from the dataloader.
|
||||
let globalTags = await Tags.getAll.load({
|
||||
item_type,
|
||||
asset_id
|
||||
});
|
||||
if (!Array.isArray(globalTags)) {
|
||||
globalTags = [];
|
||||
}
|
||||
|
||||
globalTags = globalTags.concat(pluginTags);
|
||||
|
||||
// Merge in the tags for the given comment.
|
||||
tags = tags.map((name) => {
|
||||
|
||||
// Resolve the TagLink that we can use for the comment.
|
||||
let {tagLink} = TagsService.resolveLink(user, globalTags, {name, item_type});
|
||||
|
||||
// Return the tagLink for tag insertion.
|
||||
return tagLink;
|
||||
});
|
||||
}
|
||||
|
||||
// Add the staff tag for comments created as a staff member.
|
||||
if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) {
|
||||
tags.push(TagsService.newTagLink(user, {
|
||||
name: 'STAFF',
|
||||
item_type
|
||||
}));
|
||||
}
|
||||
|
||||
return tags;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new comment.
|
||||
* @param {Object} user the user performing the request
|
||||
@@ -16,25 +66,11 @@ const Wordlist = require('../../services/wordlist');
|
||||
* @param {String} [status='NONE'] the status of the new comment
|
||||
* @return {Promise} resolves to the created comment
|
||||
*/
|
||||
const createComment = async ({user, loaders: {Comments}, pubsub}, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => {
|
||||
const createComment = async (context, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => {
|
||||
const {user, loaders: {Comments}, pubsub} = context;
|
||||
|
||||
// Handle Tags
|
||||
if (tags.length) {
|
||||
tags = tags.map((tag) => ({
|
||||
tag: {
|
||||
name: tag
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Add the staff tag for comments created as a staff member.
|
||||
if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) {
|
||||
tags.push({
|
||||
tag: {
|
||||
name: 'STAFF'
|
||||
}
|
||||
});
|
||||
}
|
||||
// Resolve the tags for the comment.
|
||||
tags = await resolveTagsForComment(context, {asset_id, tags});
|
||||
|
||||
let comment = await CommentsService.publicCreate({
|
||||
body,
|
||||
|
||||
+8
-52
@@ -4,62 +4,18 @@ const errors = require('../../errors');
|
||||
/**
|
||||
* Modifies the targeted model with the specified operation to add/remove a tag.
|
||||
*/
|
||||
const modify = async ({user}, operation, {name, id, item_type, asset_id}) => {
|
||||
const modify = async ({user, loaders: {Tags}}, operation, {name, id, item_type, asset_id}) => {
|
||||
|
||||
// Try to find the tag in the global list. This will contain the permission
|
||||
// information if it's found.
|
||||
let tag = await TagsService.get({name, id, item_type, asset_id});
|
||||
// Get the global list of tags from the dataloader.
|
||||
const tags = await Tags.getAll.load({id, item_type, asset_id});
|
||||
|
||||
// Create the new tagLink that will be created to interact to the comment.
|
||||
let tagLink = {
|
||||
tag,
|
||||
assigned_by: user.id,
|
||||
created_at: new Date()
|
||||
};
|
||||
|
||||
// If the tag was found, we need to ensure that the current user can indeed
|
||||
// modify this tag on the comment.
|
||||
if (tag) {
|
||||
|
||||
// If the tag has roles defined, and the current user has at least one of
|
||||
// the required roles, then modify the tag without checking for ownership.
|
||||
if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) {
|
||||
return operation(id, item_type, tagLink, false);
|
||||
}
|
||||
|
||||
// If the permissions allow for self assignment, then ensure that the query
|
||||
// is compose with that in mind.
|
||||
if (tag.permissions && tag.permissions.self) {
|
||||
|
||||
// Otherwise, we assume that we have to check to see that the user indeed
|
||||
// owns the resource before allowing the tag to get modified.
|
||||
return operation(id, item_type, tagLink, true);
|
||||
}
|
||||
|
||||
throw errors.ErrNotAuthorized;
|
||||
}
|
||||
|
||||
// Only admin/moderators can modify unique tags, these are tags that are not
|
||||
// in the global list.
|
||||
if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) {
|
||||
throw errors.ErrNotAuthorized;
|
||||
}
|
||||
|
||||
// Generate the tag in the event now that we have to create the tag for this
|
||||
// specific comment.
|
||||
tagLink.tag = {
|
||||
name,
|
||||
permissions: {
|
||||
public: true,
|
||||
self: false,
|
||||
roles: []
|
||||
},
|
||||
models: [item_type],
|
||||
created_at: new Date()
|
||||
};
|
||||
// Resolve the TagLink that should be used to insert to the user. This will
|
||||
// addtionally return with an ownership property that can be used to determine
|
||||
// that the user who adds this tag must also be the owner of the resource.
|
||||
let {tagLink, ownership} = TagsService.resolveLink(user, tags, {name, item_type});
|
||||
|
||||
// Actually modify the tag on the model.
|
||||
return operation(id, item_type, tagLink, false);
|
||||
return operation(id, item_type, tagLink, ownership);
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"server": [
|
||||
"coral-plugin-respect",
|
||||
"coral-plugin-facebook-auth"
|
||||
"coral-plugin-facebook-auth",
|
||||
"coral-plugin-offtopic"
|
||||
],
|
||||
"client": [
|
||||
"coral-plugin-respect"
|
||||
|
||||
@@ -2,5 +2,16 @@ const {readFileSync} = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8')
|
||||
tags: [
|
||||
{
|
||||
name: 'OFF_TOPIC',
|
||||
permissions: {
|
||||
public: true,
|
||||
self: true,
|
||||
roles: []
|
||||
},
|
||||
models: ['COMMENTS'],
|
||||
created_at: new Date()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
## Extending TAG_TYPE by adding OFF_TOPIC Tag
|
||||
enum TAG_TYPE {
|
||||
OFF_TOPIC
|
||||
}
|
||||
+83
-2
@@ -5,6 +5,8 @@ const UserModel = require('../models/user');
|
||||
const AssetsService = require('./assets');
|
||||
const SettingsService = require('./settings');
|
||||
|
||||
const errors = require('../errors');
|
||||
|
||||
const updateModel = async (item_type, query, update) => {
|
||||
|
||||
// Get the model to update with.
|
||||
@@ -43,7 +45,7 @@ class TagsService {
|
||||
/**
|
||||
* Retrives a global tag from the settings based on the input_type.
|
||||
*/
|
||||
static async get({name, id, item_type, asset_id = null}) {
|
||||
static async getAll({id, item_type, asset_id = null}) {
|
||||
|
||||
// Extract the settings from the database.
|
||||
let settings;
|
||||
@@ -66,7 +68,86 @@ class TagsService {
|
||||
let {tags = []} = settings;
|
||||
|
||||
// Return the first tag that matches the requested form.
|
||||
return tags.find((tag) => tag.name === name && tag.models.include(item_type));
|
||||
return tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the tagLink and ownership verification requirements that should be
|
||||
* used when trying to perform tag adding/removing operations.
|
||||
*/
|
||||
static resolveLink(user, tags, {name, item_type}) {
|
||||
|
||||
// Try to find the tag in the global list. This will contain the permission
|
||||
// information if it's found.
|
||||
let tag = tags.find((tag) => {
|
||||
return tag.name === name && Array.isArray(tag.models) && tag.models.includes(item_type);
|
||||
});
|
||||
|
||||
// Create the new tagLink that will be created to interact to the comment.
|
||||
let tagLink = {
|
||||
tag,
|
||||
assigned_by: user.id,
|
||||
created_at: new Date()
|
||||
};
|
||||
|
||||
// If the tag was found, we need to ensure that the current user can indeed
|
||||
// modify this tag on the comment.
|
||||
if (tag) {
|
||||
|
||||
// If the tag has roles defined, and the current user has at least one of
|
||||
// the required roles, then modify the tag without checking for ownership.
|
||||
if (tag.permissions && tag.permissions.roles && tag.permissions.roles.some((role) => user.roles.include(role))) {
|
||||
return {tagLink, ownership: false};
|
||||
}
|
||||
|
||||
// If the permissions allow for self assignment, then ensure that the query
|
||||
// is compose with that in mind.
|
||||
if (tag.permissions && tag.permissions.self) {
|
||||
|
||||
// Otherwise, we assume that we have to check to see that the user indeed
|
||||
// owns the resource before allowing the tag to get modified.
|
||||
return {tagLink, ownership: true};
|
||||
}
|
||||
|
||||
throw errors.ErrNotAuthorized;
|
||||
}
|
||||
|
||||
// Only admin/moderators can modify unique tags, these are tags that are not
|
||||
// in the global list.
|
||||
if (!(user.hasRoles('ADMIN') || user.hasRoles('MODERATOR'))) {
|
||||
throw errors.ErrNotAuthorized;
|
||||
}
|
||||
|
||||
// Generate the tag in the event now that we have to create the tag for this
|
||||
// specific comment.
|
||||
tagLink = TagsService.newTagLink(user, {name, item_type});
|
||||
|
||||
// Actually modify the tag on the model.
|
||||
return {tagLink, ownership: false};
|
||||
}
|
||||
|
||||
static newTag({name, item_type}) {
|
||||
return {
|
||||
name,
|
||||
permissions: {
|
||||
public: true,
|
||||
self: false,
|
||||
roles: []
|
||||
},
|
||||
models: [item_type],
|
||||
created_at: new Date()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new TagLink based on the input user and the tag data.
|
||||
*/
|
||||
static newTagLink(user, tag) {
|
||||
return {
|
||||
tag: TagsService.newTag(tag),
|
||||
assigned_by: user.id,
|
||||
created_at: new Date()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('graph.mutations.addTag', () => {
|
||||
let comment, asset;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
|
||||
|
||||
asset = new AssetModel({url: 'http://new.test.com/'});
|
||||
await asset.save();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user