mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 06:07:21 +01:00
Merge branch 'develop' into release
This commit is contained in:
12840
package-lock.json
generated
12840
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -26,7 +26,7 @@
|
|||||||
"babel-preset-es2015": "^6.6.0",
|
"babel-preset-es2015": "^6.6.0",
|
||||||
"babel-register": "^6.6.0",
|
"babel-register": "^6.6.0",
|
||||||
"babel-runtime": "^6.11.6",
|
"babel-runtime": "^6.11.6",
|
||||||
"bcrypt": "^2.0.0",
|
"bcrypt": "^3.0.0",
|
||||||
"body-parser": "^1.18.3",
|
"body-parser": "^1.18.3",
|
||||||
"bootstrap": "^4.1.1",
|
"bootstrap": "^4.1.1",
|
||||||
"bootstrap-vue": "^2.0.0-rc.9",
|
"bootstrap-vue": "^2.0.0-rc.9",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"coupon-code": "^0.4.5",
|
"coupon-code": "^0.4.5",
|
||||||
"cross-env": "^5.1.5",
|
"cross-env": "^5.1.5",
|
||||||
"css-loader": "^0.28.11",
|
"css-loader": "^0.28.11",
|
||||||
"csv-stringify": "^2.1.0",
|
"csv-stringify": "^3.0.0",
|
||||||
"cwait": "^1.1.1",
|
"cwait": "^1.1.1",
|
||||||
"domain-middleware": "~0.1.0",
|
"domain-middleware": "~0.1.0",
|
||||||
"express": "^4.16.3",
|
"express": "^4.16.3",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"express-validator": "^5.2.0",
|
"express-validator": "^5.2.0",
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
"extract-text-webpack-plugin": "^3.0.2",
|
||||||
"glob": "^7.1.2",
|
"glob": "^7.1.2",
|
||||||
"got": "^8.3.1",
|
"got": "^9.0.0",
|
||||||
"gulp": "^4.0.0",
|
"gulp": "^4.0.0",
|
||||||
"gulp-babel": "^7.0.1",
|
"gulp-babel": "^7.0.1",
|
||||||
"gulp-imagemin": "^4.1.0",
|
"gulp-imagemin": "^4.1.0",
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
"url-loader": "^1.0.0",
|
"url-loader": "^1.0.0",
|
||||||
"useragent": "^2.1.9",
|
"useragent": "^2.1.9",
|
||||||
"uuid": "^3.0.1",
|
"uuid": "^3.0.1",
|
||||||
"validator": "^9.4.1",
|
"validator": "^10.5.0",
|
||||||
"vinyl-buffer": "^1.0.1",
|
"vinyl-buffer": "^1.0.1",
|
||||||
"vue": "^2.5.16",
|
"vue": "^2.5.16",
|
||||||
"vue-loader": "^14.2.2",
|
"vue-loader": "^14.2.2",
|
||||||
@@ -163,19 +163,19 @@
|
|||||||
"expect.js": "^0.3.1",
|
"expect.js": "^0.3.1",
|
||||||
"http-proxy-middleware": "^0.18.0",
|
"http-proxy-middleware": "^0.18.0",
|
||||||
"istanbul": "^1.1.0-alpha.1",
|
"istanbul": "^1.1.0-alpha.1",
|
||||||
"karma": "^2.0.2",
|
"karma": "^3.0.0",
|
||||||
"karma-babel-preprocessor": "^7.0.0",
|
"karma-babel-preprocessor": "^7.0.0",
|
||||||
"karma-chai-plugins": "^0.9.0",
|
"karma-chai-plugins": "^0.9.0",
|
||||||
"karma-chrome-launcher": "^2.2.0",
|
"karma-chrome-launcher": "^2.2.0",
|
||||||
"karma-coverage": "^1.1.2",
|
"karma-coverage": "^1.1.2",
|
||||||
"karma-mocha": "^1.3.0",
|
"karma-mocha": "^1.3.0",
|
||||||
"karma-mocha-reporter": "^2.2.5",
|
"karma-mocha-reporter": "^2.2.5",
|
||||||
"karma-sinon-chai": "^1.3.4",
|
"karma-sinon-chai": "^2.0.0",
|
||||||
"karma-sinon-stub-promise": "^1.0.0",
|
"karma-sinon-stub-promise": "^1.0.0",
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-sourcemap-loader": "^0.3.7",
|
||||||
"karma-spec-reporter": "0.0.32",
|
"karma-spec-reporter": "0.0.32",
|
||||||
"karma-webpack": "^3.0.0",
|
"karma-webpack": "^3.0.0",
|
||||||
"lcov-result-merger": "^2.0.0",
|
"lcov-result-merger": "^3.0.0",
|
||||||
"mocha": "^5.1.1",
|
"mocha": "^5.1.1",
|
||||||
"monk": "^6.0.6",
|
"monk": "^6.0.6",
|
||||||
"nightwatch": "^0.9.21",
|
"nightwatch": "^0.9.21",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { IncomingWebhook } from '@slack/client';
|
||||||
|
import nconf from 'nconf';
|
||||||
import {
|
import {
|
||||||
createAndPopulateGroup,
|
createAndPopulateGroup,
|
||||||
generateUser,
|
generateUser,
|
||||||
@@ -15,8 +17,6 @@ import { getMatchesByWordArray } from '../../../../../website/server/libs/string
|
|||||||
import bannedWords from '../../../../../website/server/libs/bannedWords';
|
import bannedWords from '../../../../../website/server/libs/bannedWords';
|
||||||
import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords';
|
import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords';
|
||||||
import * as email from '../../../../../website/server/libs/email';
|
import * as email from '../../../../../website/server/libs/email';
|
||||||
import { IncomingWebhook } from '@slack/client';
|
|
||||||
import nconf from 'nconf';
|
|
||||||
|
|
||||||
const BASE_URL = nconf.get('BASE_URL');
|
const BASE_URL = nconf.get('BASE_URL');
|
||||||
|
|
||||||
@@ -80,14 +80,16 @@ describe('POST /chat', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('mute user', () => {
|
||||||
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
|
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
|
||||||
let userWithChatRevoked = await member.update({'flags.chatRevoked': true});
|
const userWithChatRevoked = await member.update({'flags.chatRevoked': true});
|
||||||
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
|
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
|
||||||
code: 401,
|
code: 401,
|
||||||
error: 'NotAuthorized',
|
error: 'NotAuthorized',
|
||||||
message: t('chatPrivilegesRevoked'),
|
message: t('chatPrivilegesRevoked'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
context('banned word', () => {
|
context('banned word', () => {
|
||||||
it('returns an error when chat message contains a banned word in tavern', async () => {
|
it('returns an error when chat message contains a banned word in tavern', async () => {
|
||||||
@@ -273,6 +275,7 @@ describe('POST /chat', () => {
|
|||||||
message: t('chatPrivilegesRevoked'),
|
message: t('chatPrivilegesRevoked'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @TODO: The next test should not depend on this. We should reset the user test in a beforeEach
|
||||||
// Restore chat privileges to continue testing
|
// Restore chat privileges to continue testing
|
||||||
user.flags.chatRevoked = false;
|
user.flags.chatRevoked = false;
|
||||||
await user.update({'flags.chatRevoked': false});
|
await user.update({'flags.chatRevoked': false});
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
.checkbox
|
.checkbox
|
||||||
label
|
label
|
||||||
input(type='checkbox', v-if='hero.flags', v-model='hero.flags.chatRevoked')
|
input(type='checkbox', v-if='hero.flags', v-model='hero.flags.chatRevoked')
|
||||||
| Chat Privileges Revoked
|
strong Chat Privileges Revoked
|
||||||
.form-group
|
.form-group
|
||||||
.checkbox
|
.checkbox
|
||||||
label
|
label
|
||||||
@@ -103,7 +103,6 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// import keys from 'lodash/keys';
|
|
||||||
import each from 'lodash/each';
|
import each from 'lodash/each';
|
||||||
|
|
||||||
import markdownDirective from 'client/directives/markdown';
|
import markdownDirective from 'client/directives/markdown';
|
||||||
@@ -174,7 +173,7 @@ export default {
|
|||||||
async loadHero (uuid, heroIndex) {
|
async loadHero (uuid, heroIndex) {
|
||||||
this.currentHeroIndex = heroIndex;
|
this.currentHeroIndex = heroIndex;
|
||||||
let hero = await this.$store.dispatch('hall:getHero', { uuid });
|
let hero = await this.$store.dispatch('hall:getHero', { uuid });
|
||||||
this.hero = Object.assign({}, this.hero, hero);
|
this.hero = Object.assign({}, hero);
|
||||||
if (!this.hero.flags) {
|
if (!this.hero.flags) {
|
||||||
this.hero.flags = {
|
this.hero.flags = {
|
||||||
chatRevoked: false,
|
chatRevoked: false,
|
||||||
@@ -204,9 +203,6 @@ export default {
|
|||||||
startingPage: 'profile',
|
startingPage: 'profile',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
userLevelStyle () {
|
|
||||||
// @TODO: implement
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ import axios from 'axios';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
|
|
||||||
|
import { toNextLevel } from '../../common/script/statHelpers';
|
||||||
import { shouldDo } from '../../common/script/cron';
|
import { shouldDo } from '../../common/script/cron';
|
||||||
import { mapState } from 'client/libs/store';
|
import { mapState } from 'client/libs/store';
|
||||||
import notifications from 'client/mixins/notifications';
|
import notifications from 'client/mixins/notifications';
|
||||||
@@ -222,7 +223,12 @@ export default {
|
|||||||
userExp (after, before) {
|
userExp (after, before) {
|
||||||
if (after === before) return;
|
if (after === before) return;
|
||||||
if (this.user.stats.lvl === 0) return;
|
if (this.user.stats.lvl === 0) return;
|
||||||
this.exp(after - before);
|
|
||||||
|
let exp = after - before;
|
||||||
|
if (exp < -50) { // recalculate exp if user level up
|
||||||
|
exp = toNextLevel(this.user.stats.lvl - 1) - before + after;
|
||||||
|
}
|
||||||
|
this.exp(exp);
|
||||||
},
|
},
|
||||||
userGp (after, before) {
|
userGp (after, before) {
|
||||||
if (after === before) return;
|
if (after === before) return;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ div(v-if='user.stats.lvl > 10')
|
|||||||
.col-4.mana
|
.col-4.mana
|
||||||
.img(:class='`shop_${spell.key} shop-sprite item-img`')
|
.img(:class='`shop_${spell.key} shop-sprite item-img`')
|
||||||
|
|
||||||
|
.drawer-wrapper.d-flex.justify-content-center
|
||||||
drawer(
|
drawer(
|
||||||
:title="$t('skillsTitle')",
|
:title="$t('skillsTitle')",
|
||||||
v-if='user.stats.class && !user.preferences.disableClasses',
|
v-if='user.stats.class && !user.preferences.disableClasses',
|
||||||
@@ -20,7 +21,7 @@ div(v-if='user.stats.lvl > 10')
|
|||||||
div(slot="drawer-slider")
|
div(slot="drawer-slider")
|
||||||
.container.spell-container
|
.container.spell-container
|
||||||
.row
|
.row
|
||||||
.col-3(
|
.col-12.col-md-3(
|
||||||
@click='castStart(skill)',
|
@click='castStart(skill)',
|
||||||
v-for='(skill, key) in spells[user.stats.class]',
|
v-for='(skill, key) in spells[user.stats.class]',
|
||||||
v-if='user.stats.lvl >= skill.lvl',
|
v-if='user.stats.lvl >= skill.lvl',
|
||||||
@@ -37,8 +38,21 @@ div(v-if='user.stats.lvl > 10')
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.drawer-wrapper {
|
||||||
|
width: 100vw;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 19;
|
||||||
|
|
||||||
|
.drawer-container {
|
||||||
|
left: auto !important;
|
||||||
|
right: auto !important;
|
||||||
|
min-width: 60%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.drawer-container {
|
.drawer-container {
|
||||||
left: calc((100% - 978px) / 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-slider {
|
.drawer-slider {
|
||||||
|
|||||||
@@ -52,8 +52,14 @@
|
|||||||
// @TODO: Implement new message header here when we fix the above
|
// @TODO: Implement new message header here when we fix the above
|
||||||
|
|
||||||
.new-message-row(v-if='selectedConversation.key && !user.flags.chatRevoked')
|
.new-message-row(v-if='selectedConversation.key && !user.flags.chatRevoked')
|
||||||
textarea(v-model='newMessage', @keyup.ctrl.enter='sendPrivateMessage()')
|
textarea(
|
||||||
button.btn.btn-secondary(@click='sendPrivateMessage()') Send
|
v-model='newMessage',
|
||||||
|
@keyup.ctrl.enter='sendPrivateMessage()',
|
||||||
|
maxlength='3000'
|
||||||
|
)
|
||||||
|
button.btn.btn-secondary(@click='sendPrivateMessage()') {{$t('send')}}
|
||||||
|
.row
|
||||||
|
span.ml-3 {{ currentLength }} / 3000
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -322,6 +328,9 @@ export default {
|
|||||||
return conversation.name.toLowerCase().indexOf(this.search.toLowerCase()) !== -1;
|
return conversation.name.toLowerCase().indexOf(this.search.toLowerCase()) !== -1;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
currentLength () {
|
||||||
|
return this.newMessage.length;
|
||||||
|
},
|
||||||
placeholderTexts () {
|
placeholderTexts () {
|
||||||
if (this.user.flags.chatRevoked) {
|
if (this.user.flags.chatRevoked) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -47,6 +47,6 @@ export function round (number, nDigits) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getXPMessage (val) {
|
export function getXPMessage (val) {
|
||||||
if (val < -50) return; // don't show when they level up (resetting their exp)
|
if (val < -50) return; // don't show when they multi-level up (resetting their exp)
|
||||||
return `${getSign(val)} ${round(val)}`;
|
return `${getSign(val)} ${round(val)}`;
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
invalidAttribute: '"<%= attr %>" is not a valid Stat.',
|
invalidAttribute: '"<%= attr %>" is not a valid Stat.',
|
||||||
|
|
||||||
statsObjectRequired: '"stats" update is required',
|
statsObjectRequired: '"stats" object is required',
|
||||||
|
|
||||||
missingTypeParam: '"req.params.type" is required.',
|
missingTypeParam: '"req.params.type" is required.',
|
||||||
missingKeyParam: '"req.params.key" is required.',
|
missingKeyParam: '"req.params.key" is required.',
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ api.postChat = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||||
|
|
||||||
if (group.privacy !== 'private' && user.flags.chatRevoked) {
|
if (group.privacy !== 'private' && user.flags.chatRevoked) {
|
||||||
throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
|
throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ api.updateHero = {
|
|||||||
if (updateData.auth && updateData.auth.blocked === false) {
|
if (updateData.auth && updateData.auth.blocked === false) {
|
||||||
hero.auth.blocked = false;
|
hero.auth.blocked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) hero.flags.chatRevoked = updateData.flags.chatRevoked;
|
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) hero.flags.chatRevoked = updateData.flags.chatRevoked;
|
||||||
|
|
||||||
let savedHero = await hero.save();
|
let savedHero = await hero.save();
|
||||||
|
|||||||
@@ -1445,8 +1445,8 @@ api.userSell = {
|
|||||||
* @apiParam (Query) {String} path Full path to unlock. See "content" API call for list of items.
|
* @apiParam (Query) {String} path Full path to unlock. See "content" API call for list of items.
|
||||||
*
|
*
|
||||||
* @apiParamExample {curl}
|
* @apiParamExample {curl}
|
||||||
* curl -x POST http://habitica.com/api/v3/user/unlock?path=background.midnight_clouds
|
* curl -X POST http://habitica.com/api/v3/user/unlock?path=background.midnight_clouds
|
||||||
* curl -x POST http://habitica.com/api/v3/user/unlock?path=hair.color.midnight
|
* curl -X POST http://habitica.com/api/v3/user/unlock?path=hair.color.midnight
|
||||||
*
|
*
|
||||||
* @apiSuccess {Object} data.purchased
|
* @apiSuccess {Object} data.purchased
|
||||||
* @apiSuccess {Object} data.items
|
* @apiSuccess {Object} data.items
|
||||||
|
|||||||
@@ -5,24 +5,24 @@ import { authWithHeaders } from '../../../middlewares/auth';
|
|||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /api/v3/user/allocate Allocate a single attribute point
|
* @api {post} /api/v3/user/allocate Allocate a single Stat Point (previously called Attribute Point)
|
||||||
* @apiName UserAllocate
|
* @apiName UserAllocate
|
||||||
* @apiGroup User
|
* @apiGroup User
|
||||||
*
|
*
|
||||||
* @apiParam (Body) {String="str","con","int","per"} stat Query parameter - Default ='str'
|
* @apiParam (Query) {String="str","con","int","per"} stat The Stat to increase. Default is 'str'
|
||||||
*
|
*
|
||||||
* @apiParamExample {json} Example request
|
* @apiParamExample {curl}
|
||||||
* {"stat":"int"}
|
* curl -X POST -d "" https://habitica.com/api/v3/user/allocate?stat=int
|
||||||
*
|
*
|
||||||
* @apiSuccess {Object} data Returns stats from the user profile
|
* @apiSuccess {Object} data Returns stats and notifications from the user profile
|
||||||
*
|
*
|
||||||
* @apiError {NotAuthorized} NoPoints Not enough attribute points to increment a stat.
|
* @apiError {NotAuthorized} NoPoints You don't have enough Stat Points.
|
||||||
*
|
*
|
||||||
* @apiErrorExample {json}
|
* @apiErrorExample {json}
|
||||||
* {
|
* {
|
||||||
* "success": false,
|
* "success": false,
|
||||||
* "error": "NotAuthorized",
|
* "error": "NotAuthorized",
|
||||||
* "message": "You don't have enough attribute points."
|
* "message": "You don't have enough Stat Points."
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
api.allocate = {
|
api.allocate = {
|
||||||
@@ -40,7 +40,7 @@ api.allocate = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /api/v3/user/allocate-bulk Allocate multiple attribute points
|
* @api {post} /api/v3/user/allocate-bulk Allocate multiple Stat Points
|
||||||
* @apiName UserAllocateBulk
|
* @apiName UserAllocateBulk
|
||||||
* @apiGroup User
|
* @apiGroup User
|
||||||
*
|
*
|
||||||
@@ -49,22 +49,22 @@ api.allocate = {
|
|||||||
* @apiParamExample {json} Example request
|
* @apiParamExample {json} Example request
|
||||||
* {
|
* {
|
||||||
* stats: {
|
* stats: {
|
||||||
* 'int': int,
|
* "int": int,
|
||||||
* 'str': int,
|
* "str": str,
|
||||||
* 'con': int,
|
* "con": con,
|
||||||
* 'per': int,
|
* "per": per
|
||||||
* },
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* @apiSuccess {Object} data Returns stats from the user profile
|
* @apiSuccess {Object} data Returns stats and notifications from the user profile
|
||||||
*
|
*
|
||||||
* @apiError {NotAuthorized} NoPoints Not enough attribute points to increment a stat.
|
* @apiError {NotAuthorized} NoPoints You don't have enough Stat Points.
|
||||||
*
|
*
|
||||||
* @apiErrorExample {json}
|
* @apiErrorExample {json}
|
||||||
* {
|
* {
|
||||||
* "success": false,
|
* "success": false,
|
||||||
* "error": "NotAuthorized",
|
* "error": "NotAuthorized",
|
||||||
* "message": "You don't have enough attribute points."
|
* "message": "You don't have enough Stat Points."
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
api.allocateBulk = {
|
api.allocateBulk = {
|
||||||
@@ -82,7 +82,7 @@ api.allocateBulk = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /api/v3/user/allocate-now Allocate all attribute points
|
* @api {post} /api/v3/user/allocate-now Allocate all Stat Points
|
||||||
* @apiDescription Uses the user's chosen automatic allocation method, or if none, assigns all to STR. Note: will return success, even if there are 0 points to allocate.
|
* @apiDescription Uses the user's chosen automatic allocation method, or if none, assigns all to STR. Note: will return success, even if there are 0 points to allocate.
|
||||||
* @apiName UserAllocateNow
|
* @apiName UserAllocateNow
|
||||||
* @apiGroup User
|
* @apiGroup User
|
||||||
@@ -119,7 +119,8 @@ api.allocateBulk = {
|
|||||||
* "per": 0,
|
* "per": 0,
|
||||||
* "str": 0,
|
* "str": 0,
|
||||||
* "con": 0
|
* "con": 0
|
||||||
* }
|
* },
|
||||||
|
* "notifications": [ .... ],
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user