Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Script that sets blockedReason and blocks reply rquests #267

Merged
merged 8 commits into from Nov 24, 2021
9 changes: 9 additions & 0 deletions README.md
Expand Up @@ -173,6 +173,15 @@ $ node build/scripts/removeArticleReply.js --userId=<userId> --articleId=<articl

- For more options, run the above script with `--help` or see the file level comments.

### Block a user
- Please announce that the user will be blocked openly with a URL first.
- To block a user, execute the following:
```
$ node build/scripts/blockUser.js --userId=<userId> --blockedReason=<Announcement URL>
```

- For more options, run the above script with `--help` or see the file level comments.


## One-off migration scripts

Expand Down
41 changes: 41 additions & 0 deletions src/scripts/__fixtures__/blockUser.js
@@ -0,0 +1,41 @@
export default {
'/users/doc/user-to-block': {
name: 'Naughty spammer',
},

'/users/doc/already-blocked': {
name: 'Blocked spammer',
blockedReason: 'Some announcement',
},

'/replyrequests/doc/already-blocked': {
articleId: 'some-article',
userId: 'user-to-block',
status: 'BLOCKED',
createdAt: '2021-11-11T00:00:00.000Z',
},

'/articles/doc/some-article': {
replyRequestCount: 1,
lastRequestedAt: '2021-01-01T00:00.000Z',
},

'/replyrequests/doc/replyrequest-to-block': {
articleId: 'modified-article',
userId: 'user-to-block',
status: 'NORMAL',
createdAt: '2021-11-11T11:00:00.000Z',
},

'/replyrequests/doc/valid-reply-request': {
articleId: 'modified-article',
userId: 'valid-user',
status: 'NORMAL',
createdAt: '2021-10-10T00:00:00.000Z',
},

'/articles/doc/modified-article': {
replyRequestCount: 2,
lastRequestedAt: '2021-01-01T00:00:01.000Z',
},
};
81 changes: 81 additions & 0 deletions src/scripts/__tests__/blockUser.js
@@ -0,0 +1,81 @@
import { loadFixtures, unloadFixtures } from 'util/fixtures';
import client from 'util/client';
import blockUser from '../blockUser';
import fixtures from '../__fixtures__/blockUser';

beforeEach(() => loadFixtures(fixtures));
afterEach(() => unloadFixtures(fixtures));

it('fails if userId is not valid', async () => {
expect(
blockUser({ userId: 'not-exist', blockedReason: 'announcement url' })
).rejects.toMatchInlineSnapshot(
`[Error: User with ID=not-exist does not exist]`
);
});

it('correctly sets the block reason and updates status of their works', async () => {
await blockUser({
userId: 'user-to-block',
blockedReason: 'announcement url',
});

const {
body: { _source: blockedUser },
} = await client.get({
index: 'users',
type: 'doc',
id: 'user-to-block',
});

// Assert that blockedReason is written on the user
expect(blockedUser).toMatchInlineSnapshot(`
Object {
"blockedReason": "announcement url",
"name": "Naughty spammer",
}
`);

//
// Check reply requests
//

// Assert reply requests that is already blocked are not selected to update
//
const {
body: { _source: someArticleWithAlreadyBlockedReplyRequest },
} = await client.get({
index: 'articles',
type: 'doc',
id: 'some-article',
});
expect(someArticleWithAlreadyBlockedReplyRequest).toMatchObject(
fixtures['/articles/doc/some-article']
);

// Assert normal reply requests being blocked and article being updated
//
const {
body: { _source: replyRequestToBlock },
} = await client.get({
index: 'replyrequests',
type: 'doc',
id: 'replyrequest-to-block',
});
expect(replyRequestToBlock.status).toEqual('BLOCKED');
const {
body: { _source: modifiedArticle },
} = await client.get({
index: 'articles',
type: 'doc',
id: 'modified-article',
});
expect(modifiedArticle).toMatchObject({
// Only replyrequests/doc/valid-reply-request
replyRequestCount: 1,
lastRequestedAt:
fixtures['/replyrequests/doc/valid-reply-request'].createdAt,
});
});

// it('still updates statuses of blocked user even if they are blocked previously')
169 changes: 169 additions & 0 deletions src/scripts/blockUser.js
@@ -0,0 +1,169 @@
/**
* Given userId & block reason, blocks the user and marks all their existing ArticleReply,
* ArticleReplyFeedback, ReplyRequest, ArticleCategory, ArticleCategoryFeedback as BLOCKED.
*/

import 'dotenv/config';
import yargs from 'yargs';
import client from 'util/client';
import getAllDocs from 'util/getAllDocs';

/**
* Update user to write blockedReason. Throws if user does not exist.
*
* @param {string} userId
* @param {string} blockedReason
*/
async function writeBlockedReasonToUser(userId, blockedReason) {
try {
const {
body: { result: setBlockedReasonResult },
} = await client.update({
index: 'users',
type: 'doc',
id: userId,
body: {
doc: { blockedReason },
},
});

/* istanbul ignore if */
if (setBlockedReasonResult === 'noop') {
console.log(
`Info: user ID ${userId} already has set the same blocked reason.`
);
}
} catch (e) {
/* istanbul ignore else */
if (e.message === 'document_missing_exception') {
throw new Error(`User with ID=${userId} does not exist`);
}

throw e;
}
}

/**
* Convert all reply requests with NORMAL status by the user to BLOCKED status.
*
* @param {userId} userId
*/
async function processReplyRequests(userId) {
const NORMAL_REPLY_REQUEST_QUERY = {
bool: {
must: [{ term: { status: 'NORMAL' } }, { term: { userId } }],
},
};

const articleIdsWithNormalReplyRequests = [];

for await (const {
_source: { articleId },
} of getAllDocs('replyrequests', NORMAL_REPLY_REQUEST_QUERY)) {
articleIdsWithNormalReplyRequests.push(articleId);
}

/* Bulk update reply reqeuests status */
const { body: updateByQueryResult } = await client.updateByQuery({
index: 'replyrequests',
type: 'doc',
body: {
query: NORMAL_REPLY_REQUEST_QUERY,
script: {
lang: 'painless',
source: `ctx._source.status = 'BLOCKED';`,
},
},
refresh: true,
});

console.log('Reply request status update result', updateByQueryResult);

/* Bulk update articles' replyRequestCount & lastRequestedAt */
console.log(
`Updating ${articleIdsWithNormalReplyRequests.length} articles...`
);

for (let i = 0; i < articleIdsWithNormalReplyRequests.length; i += 1) {
const articleId = articleIdsWithNormalReplyRequests[i];

const {
body: {
hits: { total },
aggregations: {
lastRequestedAt: { value_as_string: lastRequestedAt },
},
},
} = await client.search({
index: 'replyrequests',
size: 0,
body: {
query: {
bool: {
must: [{ term: { status: 'NORMAL' } }, { term: { articleId } }],
},
},
aggs: {
lastRequestedAt: { max: { field: 'createdAt' } },
},
},
});

await client.update({
index: 'articles',
type: 'doc',
id: articleId,
body: {
doc: {
lastRequestedAt,
replyRequestCount: total,
},
},
});

console.log(
`[${i + 1}/${
articleIdsWithNormalReplyRequests.length
}] article ${articleId}: changed to ${total} reply requests, last requested at ${lastRequestedAt}`
);
}
}

/**
* Convert all article replies with NORMAL status by the user to BLOCKED status.
*
* @param {userId} userId
*/
// async function processArticleReplies(/* userId */) {}

/**
* @param {object} args
*/
async function main({ userId, blockedReason } = {}) {
await writeBlockedReasonToUser(userId, blockedReason);
await processReplyRequests(userId);
// await processArticleReplies(userId);
}

export default main;

if (require.main === module) {
const argv = yargs
.options({
userId: {
alias: 'u',
description: 'The user ID to block',
type: 'string',
demandOption: true,
},
blockedReason: {
alias: 'r',
description: 'The URL to the annoucement that blocks this user',
type: 'string',
demandOption: true,
},
})
.help('help').argv;

main(argv).catch(console.error);
}