Compare commits

...

67 Commits

Author SHA1 Message Date
renovate[bot]
0ef5cbbbca fix(deps): update dependency fetch-cookie to v3.2.0 (#13836)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 16:56:30 -05:00
renovate[bot]
7c2e83303c fix(deps): update dependency autoprefixer to v10.4.23 (#13835)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 16:54:56 -05:00
renovate[bot]
ad895efb61 chore(deps): update dependency smtp-server to v3.17.1 (#13829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 10:36:23 -05:00
renovate[bot]
22fe83f005 chore(deps): update dependency @eslint/js to v9.39.2 (#13830)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 10:36:13 -05:00
renovate[bot]
b169621860 chore(deps): update github artifact actions (#13831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 10:36:02 -05:00
renovate[bot]
da7c9b32b8 fix(deps): update dependency terser-webpack-plugin to v5.3.16 (#13827)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 07:56:00 -05:00
renovate[bot]
0fcc8543c6 chore(deps): update actions/cache action to v5 (#13828)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 07:55:41 -05:00
Barış Soner Uşaklı
ed977c48b4 Merge branch 'master' into develop 2025-12-11 21:25:56 -05:00
Julian Lam
20918b5281 fix: wrong increment value 2025-12-11 11:10:29 -05:00
Julian Lam
8abe0dfa9f fix: increment progress on upgrade script 2025-12-11 11:09:13 -05:00
Julian Lam
097d0802b7 feat: stop extraneous vote and tids_read data from being saved for remote users 2025-12-11 10:57:00 -05:00
renovate[bot]
3adcbe0f7d chore(deps): update dependency smtp-server to v3.17.0 (#13824)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 10:34:57 -05:00
renovate[bot]
b992511bb9 chore(deps): update dependency sass-embedded to v1.96.0 (#13821)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 10:13:51 -05:00
renovate[bot]
d4f53a6242 fix(deps): update dependency sass to v1.96.0 (#13822)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 10:13:35 -05:00
Julian Lam
528cd258c4 feat: support remote Dislike activity, federate out a Dislike on downvote, bwahahah 2025-12-10 12:40:46 -05:00
renovate[bot]
a2f2c8c761 chore(deps): update dependency sass-embedded to v1.95.1 (#13817)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 20:04:25 -05:00
renovate[bot]
81c232f181 fix(deps): update dependency winston to v3.19.0 (#13812)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 19:47:59 -05:00
renovate[bot]
f077c4cab8 fix(deps): update dependency cron to v4.4.0 (#13818)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 19:46:55 -05:00
renovate[bot]
adedb7b626 fix(deps): update dependency sass to v1.95.1 (#13816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 19:46:11 -05:00
renovate[bot]
a35c326a6c chore(deps): update dependency jsdom to v27.3.0 (#13814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 19:25:06 -05:00
renovate[bot]
eaa6e71a99 fix(deps): update dependency sass to v1.95.0 (#13815)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 19:24:47 -05:00
Barış Soner Uşaklı
011f8b2465 Merge branch 'master' into develop 2025-12-08 10:19:00 -05:00
Barış Soner Uşaklı
2c57cb13e4 Merge branch 'master' into develop 2025-12-06 20:44:57 -05:00
Barış Soner Uşaklı
f6fbb0226b Merge branch 'master' into develop 2025-12-06 20:40:30 -05:00
Barış Soner Uşaklı
823c6cb340 Merge branch 'master' into develop 2025-12-05 12:28:29 -05:00
renovate[bot]
e50edd52fc chore(deps): update commitlint monorepo to v20.2.0 (#13810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-05 12:24:16 -05:00
renovate[bot]
10d2e929a1 fix(deps): update dependency terser-webpack-plugin to v5.3.15 (#13811)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-05 12:24:04 -05:00
Barış Uşaklı
6956270411 test: add a test for set db.exists (#13809)
* test: add a test for set db.exists

* delete empty sets

* test: psql
2025-12-04 18:33:55 -05:00
Julian Lam
c529244229 test: fix failing test by adjusting the tests 2025-12-04 16:03:28 -05:00
Julian Lam
f1d50c3510 fix: add join-lemmy context for outgoing category group actors context prop 2025-12-04 11:55:00 -05:00
renovate[bot]
6b1dcb4b90 fix(deps): update dependency esbuild to v0.27.1 (#13806)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 11:39:32 -05:00
renovate[bot]
7b734cfdc5 fix(deps): update dependency jsonwebtoken to v9.0.3 (#13807)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 11:37:57 -05:00
Julian Lam
6a56105037 feat: expand postingRestrictedToMods mask testing, handle actor update for that prop 2025-12-04 11:00:41 -05:00
Barış Uşaklı
7d5402fe66 feat: setAddBulk (#13805)
* feat: setAddBulk

add some tests

* fix: sAdd with value array on redis
2025-12-03 18:18:14 -05:00
Julian Lam
d8e55d58de fix: use setsAdd 2025-12-03 15:21:49 -05:00
Julian Lam
4a6dcf1a21 fix: missing await 2025-12-03 15:21:49 -05:00
Julian Lam
2968772287 chore: allow direct testing in test/categories.js 2025-12-03 15:21:49 -05:00
Julian Lam
934e6be911 test: privilege masking tests 2025-12-03 15:21:49 -05:00
Julian Lam
f0a7a442db feat: save privilege masking set when asserting group 2025-12-03 15:21:49 -05:00
Julian Lam
7b194c6916 fix: admin privilege overrides only apply to local categories 2025-12-03 15:21:49 -05:00
Julian Lam
4020e1be35 feat: patch low-level privilege query calls to accept privilege masks at the cid level 2025-12-03 15:21:49 -05:00
renovate[bot]
76b6b3b259 chore(deps): update dependency lint-staged to v16.2.7 (#13785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 10:14:33 -05:00
Barış Soner Uşaklı
4cdb56904c Merge branch 'master' into develop 2025-12-03 10:14:13 -05:00
Barış Soner Uşaklı
70169758ec Merge branch 'master' into develop 2025-12-03 09:49:22 -05:00
renovate[bot]
7f21a17175 chore(deps): update actions/checkout action to v6 (#13802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 14:41:12 -05:00
renovate[bot]
93057306f4 fix(deps): update dependency ace-builds to v1.43.5 (#13797)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 14:36:08 -05:00
renovate[bot]
731933a66b fix(deps): update dependency lru-cache to v11.2.4 (#13798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 14:35:46 -05:00
renovate[bot]
38321220f2 fix(deps): update dependency express to v4.22.1 (#13800)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 14:35:32 -05:00
renovate[bot]
ad5cd27b37 fix(deps): update dependency ipaddr.js to v2.3.0 (#13801)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 14:27:07 -05:00
renovate[bot]
ecec1f4594 fix(deps): update dependency nodemailer to v7.0.11 (#13799)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 14:26:50 -05:00
Julian Lam
3b7bcba6c0 fix: have notes.assert call out.announce.topic only if uid is set (so, if note assertion is called via search; manual pull) 2025-12-02 14:15:13 -05:00
Julian Lam
a82e1f441c debug: still broken... more debug logs 2025-12-02 13:50:50 -05:00
Julian Lam
977a67f4cd fix: deep clone activity prop before execution; feps.announce 2025-12-02 13:42:06 -05:00
Julian Lam
8236b594af debug: log mock results 2025-12-02 13:19:48 -05:00
Barış Soner Uşaklı
22d3c52332 test: log label 2025-12-02 12:59:28 -05:00
Barış Soner Uşaklı
e39c91497f test: log activities 2025-12-02 12:47:47 -05:00
Barış Soner Uşaklı
841bd8252c test: on test fail show activities 2025-12-02 11:54:14 -05:00
renovate[bot]
5ba6bea049 fix(deps): update dependency cron to v4.3.5 (#13796)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 11:44:46 -05:00
renovate[bot]
624ef61655 fix(deps): update dependency body-parser to v2.2.1 (#13795)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 11:44:29 -05:00
Barış Uşaklı
287b25695d test: new mongodb deps (#13793) 2025-12-02 11:08:20 -05:00
renovate[bot]
5f55ca85e6 fix(deps): update dependency @isaacs/ttlcache to v2.1.3 (#13791)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 09:50:11 -05:00
renovate[bot]
1cb8b381d5 fix(deps): update dependency sass to v1.94.2 (#13786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 18:53:29 -05:00
renovate[bot]
1bcfe3f09e fix(deps): update dependency redis to v5.10.0 (#13787)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 18:53:11 -05:00
Barış Soner Uşaklı
936dede44f Merge branch 'master' into develop 2025-12-01 11:15:23 -05:00
Julian Lam
3ab61615af feat: federate out topic removal activities when topic is deleted and purged from a local category 2025-12-01 11:11:27 -05:00
Julian Lam
411baa21f4 fix: minor comment fix 2025-12-01 10:25:32 -05:00
Julian Lam
c365c1dc3e fix: publish postingRestrictedToMods property in group actor 2025-11-26 12:29:48 -05:00
25 changed files with 657 additions and 134 deletions

View File

@@ -34,7 +34,7 @@ jobs:
platform=${{ matrix.platforms }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY@L}" >> $GITHUB_ENV
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -53,7 +53,7 @@ jobs:
- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: var-cache-node-modules
key: var-cache-node-modules-${{ hashFiles('Dockerfile', 'install/package.json') }}
@@ -77,7 +77,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
@@ -93,7 +93,7 @@ jobs:
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY@L}" >> $GITHUB_ENV
echo "CURRENT_DATE_NST=$(date +'%Y%m%d-%H%M%S' -d '-3 hours -30 minutes')" >> $GITHUB_ENV
- name: Download digests
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
path: ${{ runner.temp }}/digests
pattern: digests-*

View File

@@ -81,7 +81,7 @@ jobs:
- 27017:27017
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- run: cp install/package.json package.json

View File

@@ -33,19 +33,19 @@
"@fontsource/inter": "5.2.8",
"@fontsource/poppins": "5.2.7",
"@fortawesome/fontawesome-free": "6.7.2",
"@isaacs/ttlcache": "2.1.2",
"@isaacs/ttlcache": "2.1.3",
"@nodebb/spider-detector": "2.0.3",
"@popperjs/core": "2.11.8",
"@textcomplete/contenteditable": "0.1.13",
"@textcomplete/core": "0.1.13",
"@textcomplete/textarea": "0.1.13",
"ace-builds": "1.43.4",
"ace-builds": "1.43.5",
"archiver": "7.0.1",
"async": "3.2.6",
"autoprefixer": "10.4.22",
"autoprefixer": "10.4.23",
"bcryptjs": "3.0.3",
"benchpressjs": "2.5.5",
"body-parser": "2.2.0",
"body-parser": "2.2.1",
"bootbox": "6.0.4",
"bootstrap": "5.3.8",
"bootswatch": "5.3.8",
@@ -57,27 +57,27 @@
"compare-versions": "6.1.1",
"compression": "1.8.1",
"connect-flash": "0.1.1",
"connect-mongo": "5.1.0",
"connect-mongo": "6.0.0",
"connect-pg-simple": "10.0.0",
"connect-redis": "9.0.0",
"cookie-parser": "1.4.7",
"cron": "4.3.4",
"cron": "4.4.0",
"cropperjs": "1.6.2",
"csrf-sync": "4.2.1",
"daemon": "1.1.0",
"diff": "8.0.2",
"esbuild": "0.27.0",
"express": "4.21.2",
"esbuild": "0.27.1",
"express": "4.22.1",
"express-session": "1.18.2",
"express-useragent": "2.0.2",
"fetch-cookie": "3.1.0",
"fetch-cookie": "3.2.0",
"file-loader": "6.2.0",
"fs-extra": "11.3.2",
"graceful-fs": "4.2.11",
"helmet": "7.2.0",
"html-to-text": "9.0.5",
"imagesloaded": "5.0.0",
"ipaddr.js": "2.2.0",
"ipaddr.js": "2.3.0",
"jquery": "3.7.1",
"jquery-deserialize": "2.0.0",
"jquery-form": "4.3.0",
@@ -85,13 +85,13 @@
"jquery-ui": "1.14.1",
"jsesc": "3.1.0",
"json2csv": "5.0.7",
"jsonwebtoken": "9.0.2",
"jsonwebtoken": "9.0.3",
"lodash": "4.17.21",
"logrotate-stream": "0.2.9",
"lru-cache": "11.2.2",
"lru-cache": "11.2.4",
"mime": "3.0.0",
"mkdirp": "3.0.1",
"mongodb": "6.21.0",
"mongodb": "7.0.0",
"morgan": "1.10.1",
"mousetrap": "1.6.5",
"multer": "2.0.2",
@@ -112,7 +112,7 @@
"nodebb-theme-peace": "2.2.49",
"nodebb-theme-persona": "14.1.18",
"nodebb-widget-essentials": "7.0.41",
"nodemailer": "7.0.10",
"nodemailer": "7.0.11",
"nprogress": "0.2.0",
"passport": "0.7.0",
"passport-http-bearer": "1.0.1",
@@ -124,12 +124,12 @@
"pretty": "^2.0.0",
"progress-webpack-plugin": "1.0.16",
"prompt": "1.3.0",
"redis": "5.9.0",
"redis": "5.10.0",
"rimraf": "6.1.2",
"rss": "1.2.2",
"rtlcss": "4.3.0",
"sanitize-html": "2.17.0",
"sass": "1.94.1",
"sass": "1.96.0",
"satori": "0.18.3",
"sbd": "^1.0.19",
"semver": "7.7.3",
@@ -141,7 +141,7 @@
"@socket.io/redis-adapter": "8.3.0",
"sortablejs": "1.15.6",
"spdx-license-list": "6.10.0",
"terser-webpack-plugin": "5.3.14",
"terser-webpack-plugin": "5.3.16",
"textcomplete": "0.18.2",
"textcomplete.contenteditable": "0.1.1",
"timeago": "1.6.7",
@@ -152,7 +152,7 @@
"validator": "13.15.23",
"webpack": "5.103.0",
"webpack-merge": "6.0.1",
"winston": "3.18.3",
"winston": "3.19.0",
"workerpool": "10.0.1",
"xml": "1.0.1",
"xregexp": "5.1.2",
@@ -161,26 +161,26 @@
},
"devDependencies": {
"@apidevtools/swagger-parser": "10.1.0",
"@commitlint/cli": "20.1.0",
"@commitlint/config-angular": "20.0.0",
"@commitlint/cli": "20.2.0",
"@commitlint/config-angular": "20.2.0",
"coveralls": "3.1.1",
"@eslint/js": "9.39.1",
"@eslint/js": "9.39.2",
"@stylistic/eslint-plugin": "5.6.1",
"eslint-config-nodebb": "1.1.11",
"eslint-plugin-import": "2.32.0",
"grunt": "1.6.1",
"grunt-contrib-watch": "1.1.0",
"husky": "8.0.3",
"jsdom": "27.2.0",
"lint-staged": "16.2.6",
"jsdom": "27.3.0",
"lint-staged": "16.2.7",
"mocha": "11.7.5",
"mocha-lcov-reporter": "1.3.0",
"mockdate": "3.0.5",
"nyc": "17.1.0",
"smtp-server": "3.16.1"
"smtp-server": "3.17.1"
},
"optionalDependencies": {
"sass-embedded": "1.93.3"
"sass-embedded": "1.96.0"
},
"resolutions": {
"*/jquery": "3.7.1"

View File

@@ -147,7 +147,7 @@ Actors.assert = async (ids, options = {}) => {
categories.add(actor.id);
}
}
if (
!typeOk ||
!activitypub._constants.requiredActorProps.every(prop => actor.hasOwnProperty(prop))
@@ -351,7 +351,7 @@ Actors.assertGroup = async (ids, options = {}) => {
}));
groups = groups.filter(Boolean); // remove unresolvable actors
// Build userData object for storage
// Build categoryData object for storage
const categoryObjs = (await activitypub.mocks.category(groups)).filter(Boolean);
const now = Date.now();
@@ -400,12 +400,20 @@ Actors.assertGroup = async (ids, options = {}) => {
db.deleteObjectFields('handle:cid', queries.handleRemove),
]);
// Privilege mask
const [masksAdd, masksRemove] = categoryObjs.reduce(([add, remove], category) => {
(category?._activitypub?.postingRestrictedToMods ? add : remove).push(`cid:${category.cid}:privilegeMask`);
return [add, remove];
}, [[], []]);
await Promise.all([
db.setObjectBulk(bulkSet),
db.sortedSetAdd('usersRemote:lastCrawled', groups.map(() => now), groups.map(p => p.id)),
db.sortedSetAddBulk(queries.searchAdd),
db.setObject('handle:cid', queries.handleAdd),
_migratePersonToGroup(categoryObjs),
db.setsAdd(masksAdd, 'topics:create'),
db.setsRemove(masksRemove, 'topics:create'),
]);
return categoryObjs;

View File

@@ -352,6 +352,26 @@ inbox.like = async (req) => {
socketHelpers.upvote(result, 'notifications:upvoted-your-post-in');
};
inbox.dislike = async (req) => {
const { actor, object } = req.body;
const { type, id } = await activitypub.helpers.resolveLocalId(object.id);
if (type !== 'post' || !(await posts.exists(id))) {
return reject('Dislike', object, actor);
}
const allowed = await privileges.posts.can('posts:downvote', id, activitypub._constants.uid);
if (!allowed) {
activitypub.helpers.log(`[activitypub/inbox.like] ${id} not allowed to be downvoted.`);
return reject('Dislike', object, actor);
}
activitypub.helpers.log(`[activitypub/inbox/dislike] id ${id} via ${actor}`);
await posts.downvote(id, actor);
await activitypub.feps.announce(object.id, req.body);
};
inbox.announce = async (req) => {
let { actor, object, published, to, cc } = req.body;
activitypub.helpers.log(`[activitypub/inbox/announce] Parsing Announce(${object.type}) from ${actor}`);

View File

@@ -13,6 +13,7 @@ const categories = require('../categories');
const posts = require('../posts');
const topics = require('../topics');
const messaging = require('../messaging');
const privileges = require('../privileges');
const plugins = require('../plugins');
const slugify = require('../slugify');
const translator = require('../translator');
@@ -280,6 +281,7 @@ Mocks.category = async (actors) => {
let {
url, preferredUsername, icon, /* image, */
name, summary, followers, inbox, endpoints, tag,
postingRestrictedToMods,
} = actor;
preferredUsername = slugify(preferredUsername || name);
/*
@@ -337,6 +339,10 @@ Mocks.category = async (actors) => {
inbox,
sharedInbox: endpoints ? endpoints.sharedInbox : null,
followersUrl: followers,
_activitypub: {
postingRestrictedToMods,
},
};
return payload;
@@ -524,12 +530,19 @@ Mocks.actors.user = async (uid) => {
};
Mocks.actors.category = async (cid) => {
const {
name, handle: preferredUsername, slug,
descriptionParsed: summary, backgroundImage,
} = await categories.getCategoryFields(cid,
['name', 'handle', 'slug', 'description', 'descriptionParsed', 'backgroundImage']);
const publicKey = await activitypub.getPublicKey('cid', cid);
const [
{
name, handle: preferredUsername, slug,
descriptionParsed: summary, backgroundImage,
},
publicKey,
canPost,
] = await Promise.all([
categories.getCategoryFields(cid,
['name', 'handle', 'slug', 'description', 'descriptionParsed', 'backgroundImage']),
activitypub.getPublicKey('cid', cid),
privileges.categories.can('topics:create', cid, -2),
]);
let icon;
if (backgroundImage) {
@@ -553,6 +566,7 @@ Mocks.actors.category = async (cid) => {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
'https://join-lemmy.org/context.json',
],
id: `${nconf.get('url')}/category/${cid}`,
url: `${nconf.get('url')}/category/${slug}`,
@@ -567,6 +581,7 @@ Mocks.actors.category = async (cid) => {
summary,
// image, // todo once categories have cover photos
icon,
postingRestrictedToMods: !canPost,
publicKey: {
id: `${nconf.get('url')}/category/${cid}#key`,

View File

@@ -265,8 +265,8 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => {
await Notes.syncUserInboxes(tid, uid);
if (!hasTid && options.cid) {
// New topic, have category announce it
if (!hasTid && uid && options.cid) {
// New topic via search/post-redirect, have category announce it
activitypub.out.announce.topic(tid);
}

View File

@@ -277,6 +277,32 @@ Out.like.note = enabledCheck(async (uid, pid) => {
]);
});
Out.dislike = {};
Out.dislike.note = enabledCheck(async (uid, pid) => {
const payload = {
id: `${nconf.get('url')}/uid/${uid}#activity/dislike/${encodeURIComponent(pid)}`,
type: 'Dislike',
actor: `${nconf.get('url')}/uid/${uid}`,
object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid,
};
if (!activitypub.helpers.isUri(pid)) { // only 1b12 announce for local likes
await activitypub.feps.announce(pid, payload);
return;
}
const recipient = await posts.getPostField(pid, 'uid');
if (!activitypub.helpers.isUri(recipient)) {
return;
}
await Promise.all([
activitypub.send('uid', uid, [recipient], payload),
activitypub.feps.announce(pid, payload),
]);
});
Out.announce = {};
Out.announce.topic = enabledCheck(async (tid) => {

View File

@@ -7,6 +7,7 @@ const posts = require('../posts');
const privileges = require('../privileges');
const plugins = require('../plugins');
const activitypub = require('../activitypub');
const utils = require('../utils');
const socketHelpers = require('../socket.io/helpers');
const websockets = require('../socket.io');
const events = require('../events');
@@ -66,11 +67,22 @@ exports.doTopicAction = async function (action, event, caller, { tids }) {
const uids = await user.getUidsFromSet('users:online', 0, -1);
await Promise.all(tids.map(async (tid) => {
const title = await topics.getTopicField(tid, 'title');
const { title, cid, mainPid } = await topics.getTopicFields(tid, ['title', 'cid', 'mainPid']);
const data = await topics.tools[action](tid, caller.uid);
const notifyUids = await privileges.categories.filterUids('topics:read', data.cid, uids);
socketHelpers.emitToUids(event, data, notifyUids);
await logTopicAction(action, caller, tid, title);
switch(action) {
case 'delete': // falls through
case 'purge': {
if (utils.isNumber(cid) && parseInt(cid, 10) > 0) {
activitypub.out.remove.context(caller.uid, tid); // 7888-style
activitypub.out.delete.note(caller.uid, mainPid); // 1b12-style
activitypub.out.undo.announce('cid', cid, tid); // microblogs
}
}
}
}));
};
@@ -135,14 +147,34 @@ async function executeCommand(caller, command, eventName, notification, data) {
websockets.in(`uid_${caller.uid}`).emit(`posts.${command}`, result);
websockets.in(data.room_id).emit(`event:${eventName}`, result);
}
if (result && command === 'upvote') {
socketHelpers.upvote(result, notification);
await activitypub.out.like.note(caller.uid, data.pid);
} else if (result && notification) {
socketHelpers.sendNotificationToPostOwner(data.pid, caller.uid, command, notification);
} else if (result && command === 'unvote') {
socketHelpers.rescindUpvoteNotification(data.pid, caller.uid);
await activitypub.out.undo.like(caller.uid, data.pid);
if (result) {
switch (command) {
case 'upvote': {
socketHelpers.upvote(result, notification);
await activitypub.out.like.note(caller.uid, data.pid);
break;
}
case 'downvote': {
await activitypub.out.dislike.note(caller.uid, data.pid);
break;
}
case 'unvote': {
socketHelpers.rescindUpvoteNotification(data.pid, caller.uid);
await activitypub.out.undo.like(caller.uid, data.pid);
break;
}
default: {
if (notification) {
socketHelpers.sendNotificationToPostOwner(data.pid, caller.uid, command, notification);
}
break;
}
}
}
return result;
}

View File

@@ -325,13 +325,12 @@ topicsAPI.move = async (caller, { tid, cid }) => {
if (utils.isNumber(cid) && parseInt(cid, 10) === -1) {
activitypub.out.remove.context(caller.uid, tid); // 7888-style
activitypub.out.delete.note(caller.uid, topicData.mainPid); // threadiverse
// tbd: activitypubApi.undo.announce? // microblogs
activitypub.out.delete.note(caller.uid, topicData.mainPid); // 1b12-style
} else {
activitypub.out.move.context(caller.uid, tid);
activitypub.out.announce.topic(tid);
}
activitypub.out.undo.announce('cid', topicData.cid, tid);
activitypub.out.undo.announce('cid', topicData.cid, tid); // microblogs
}
await events.log({

View File

@@ -65,7 +65,7 @@ mongoModule.init = async function (opts) {
};
mongoModule.createSessionStore = async function (options) {
const MongoStore = require('connect-mongo');
const { MongoStore } = require('connect-mongo');
const meta = require('../meta');
const store = MongoStore.create({

View File

@@ -68,6 +68,31 @@ module.exports = function (module) {
}
};
module.setAddBulk = async function (data) {
if (!data.length) {
return;
}
const bulk = module.client.collection('objects').initializeUnorderedBulkOp();
data.forEach(([key, member]) => {
bulk.find({ _key: key }).upsert().updateOne({
$addToSet: {
members: helpers.valueToString(member),
},
});
});
try {
await bulk.execute();
} catch (err) {
if (err && err.message.includes('E11000 duplicate key error')) {
console.log(new Error('e11000').stack, data);
return await module.setAddBulk(data);
}
throw err;
}
};
module.setRemove = async function (key, value) {
if (!Array.isArray(value)) {
value = [value];
@@ -75,11 +100,17 @@ module.exports = function (module) {
value = value.map(v => helpers.valueToString(v));
await module.client.collection('objects').updateMany({
const coll = module.client.collection('objects');
await coll.updateMany({
_key: Array.isArray(key) ? { $in: key } : key,
}, {
$pullAll: { members: value },
});
await coll.deleteMany({
_key: Array.isArray(key) ? { $in: key } : key,
members: { $size: 0 },
});
};
module.setsRemove = async function (keys, value) {
@@ -88,11 +119,17 @@ module.exports = function (module) {
}
value = helpers.valueToString(value);
await module.client.collection('objects').updateMany({
const coll = module.client.collection('objects');
await coll.updateMany({
_key: { $in: keys },
}, {
$pull: { members: value },
});
await coll.deleteMany({
_key: { $in: keys },
members: { $size: 0 },
});
};
module.isSetMember = async function (key, value) {

View File

@@ -28,6 +28,11 @@ module.exports = function (module) {
return members.map(member => member.length > 0);
}
async function checkIfSetsExist(keys) {
const members = await Promise.all(keys.map(module.getSetMembers));
return members.map(member => member.length > 0);
}
async function checkIfKeysExist(keys) {
const res = await module.pool.query({
name: 'existsArray',
@@ -44,13 +49,16 @@ module.exports = function (module) {
if (isArray) {
const types = await Promise.all(key.map(module.type));
const zsetKeys = key.filter((_key, i) => types[i] === 'zset');
const otherKeys = key.filter((_key, i) => types[i] !== 'zset');
const [zsetExits, otherExists] = await Promise.all([
const setKeys = key.filter((_key, i) => types[i] === 'set');
const otherKeys = key.filter((_key, i) => types[i] !== 'zset' && types[i] !== 'set');
const [zsetExits, setExists, otherExists] = await Promise.all([
checkIfzSetsExist(zsetKeys),
checkIfSetsExist(setKeys),
checkIfKeysExist(otherKeys),
]);
const existsMap = Object.create(null);
zsetKeys.forEach((k, i) => { existsMap[k] = zsetExits[i]; });
setKeys.forEach((k, i) => { existsMap[k] = setExists[i]; });
otherKeys.forEach((k, i) => { existsMap[k] = otherExists[i]; });
return key.map(k => existsMap[k]);
}
@@ -58,6 +66,9 @@ module.exports = function (module) {
if (type === 'zset') {
const members = await module.getSortedSetRange(key, 0, 0);
return members.length > 0;
} else if (type === 'set') {
const members = await module.getSetMembers(key);
return members.length > 0;
}
const res = await module.pool.query({
name: 'exists',

View File

@@ -54,6 +54,32 @@ DO NOTHING`,
});
};
module.setAddBulk = async function (data) {
if (!data.length) {
return;
}
const keys = [];
const members = [];
for (const [key, member] of data) {
keys.push(key);
members.push(member);
}
await module.transaction(async (client) => {
await helpers.ensureLegacyObjectsType(client, keys, 'set');
await client.query({
name: 'setAddBulk',
text: `
INSERT INTO "legacy_set" ("_key", "member")
SELECT k, m
FROM UNNEST($1::TEXT[], $2::TEXT[]) AS t(k, m)
ON CONFLICT ("_key", "member")
DO NOTHING;`,
values: [keys, members],
});
});
};
module.setRemove = async function (key, value) {
if (!Array.isArray(key)) {
key = [key];

View File

@@ -14,11 +14,29 @@ module.exports = function (module) {
};
module.setsAdd = async function (keys, value) {
if (!Array.isArray(keys) || !keys.length) {
if (!Array.isArray(keys) || !keys.length || !value) {
return;
}
if (!Array.isArray(value)) {
value = [value];
}
if (!value.length) {
return;
}
const batch = module.client.batch();
keys.forEach(k => batch.sAdd(String(k), String(value)));
keys.forEach((k) => {
value.forEach(v => batch.sAdd(String(k), String(v)));
});
await helpers.execBatch(batch);
};
module.setAddBulk = async function (data) {
if (!data.length) {
return;
}
const batch = module.client.batch();
data.forEach(([key, member]) => batch.sAdd(String(key), String(member)));
await helpers.execBatch(batch);
};

View File

@@ -177,16 +177,18 @@ module.exports = function (Posts) {
}
const now = Date.now();
if (type === 'upvote' && !unvote) {
await db.sortedSetAdd(`uid:${uid}:upvote`, now, pid);
} else {
await db.sortedSetRemove(`uid:${uid}:upvote`, pid);
}
if (utils.isNumber(uid)) {
if (type === 'upvote' && !unvote) {
await db.sortedSetAdd(`uid:${uid}:upvote`, now, pid);
} else {
await db.sortedSetRemove(`uid:${uid}:upvote`, pid);
}
if (type === 'upvote' || unvote) {
await db.sortedSetRemove(`uid:${uid}:downvote`, pid);
} else {
await db.sortedSetAdd(`uid:${uid}:downvote`, now, pid);
if (type === 'upvote' || unvote) {
await db.sortedSetRemove(`uid:${uid}:downvote`, pid);
} else {
await db.sortedSetAdd(`uid:${uid}:downvote`, now, pid);
}
}
const postData = await Posts.getPostFields(pid, ['pid', 'uid', 'tid']);

View File

@@ -96,14 +96,16 @@ privsCategories.get = async function (cid, uid) {
'topics:tag', 'read', 'posts:view_deleted',
];
const [userPrivileges, isAdministrator, isModerator] = await Promise.all([
let [userPrivileges, isAdministrator, isModerator] = await Promise.all([
helpers.isAllowedTo(privs, uid, cid),
user.isAdministrator(uid),
user.isModerator(uid, cid),
]);
const combined = userPrivileges.map(allowed => allowed || isAdministrator);
const privData = _.zipObject(privs, combined);
if (utils.isNumber(cid)) {
userPrivileges = userPrivileges.map(allowed => allowed || isAdministrator);
}
const privData = _.zipObject(privs, userPrivileges);
const isAdminOrMod = isAdministrator || isModerator;
return await plugins.hooks.fire('filter:privileges.categories.get', {

View File

@@ -4,6 +4,7 @@
const _ = require('lodash');
const validator = require('validator');
const db = require('../database');
const groups = require('../groups');
const user = require('../user');
const categories = require('../categories');
@@ -25,16 +26,25 @@ helpers.isUsersAllowedTo = async function (privilege, uids, cid) {
cid = -1;
}
const [hasUserPrivilege, hasGroupPrivilege] = await Promise.all([
groups.isMembers(uids, `cid:${cid}:privileges:${privilege}`),
groups.isMembersOfGroupList(uids, `cid:${cid}:privileges:groups:${privilege}`),
]);
const allowed = uids.map((uid, index) => hasUserPrivilege[index] || hasGroupPrivilege[index]);
const result = await plugins.hooks.fire('filter:privileges:isUsersAllowedTo', { allowed: allowed, privilege: privilege, uids: uids, cid: cid });
let allowed;
const masked = await db.isSetMember(`cid:${cid}:privilegeMask`, privilege);
if (!masked) {
const [hasUserPrivilege, hasGroupPrivilege] = await Promise.all([
groups.isMembers(uids, `cid:${cid}:privileges:${privilege}`),
groups.isMembersOfGroupList(uids, `cid:${cid}:privileges:groups:${privilege}`),
]);
allowed = uids.map((uid, index) => hasUserPrivilege[index] || hasGroupPrivilege[index]);
} else {
allowed = uids.map(() => false);
}
const result = await plugins.hooks.fire('filter:privileges:isUsersAllowedTo', { allowed, privilege, uids, cid });
return result.allowed;
};
helpers.isAllowedTo = async function (privilege, uidOrGroupName, cid) {
const _cid = cid; // original passed-in cid needed for privilege mask checks
// Remote categories (non-numeric) inherit world privileges
if (Array.isArray(cid)) {
cid = cid.map(cid => (utils.isNumber(cid) ? cid : -1));
@@ -44,10 +54,15 @@ helpers.isAllowedTo = async function (privilege, uidOrGroupName, cid) {
let allowed;
if (Array.isArray(privilege) && !Array.isArray(cid)) {
const mask = await db.isSetMembers(`cid:${_cid}:privilegeMask`, privilege);
allowed = await isAllowedToPrivileges(privilege, uidOrGroupName, cid);
allowed = allowed.map((allowed, idx) => mask[idx] ? false : allowed);
} else if (Array.isArray(cid) && !Array.isArray(privilege)) {
const mask = await db.isMemberOfSets(_cid.map(cid => `cid:${cid}:privilegeMask`), privilege);
allowed = await isAllowedToCids(privilege, uidOrGroupName, cid);
allowed = allowed.map((allowed, idx) => mask[idx] ? false : allowed);
}
if (allowed) {
({ allowed } = await plugins.hooks.fire('filter:privileges:isAllowedTo', { allowed: allowed, privilege: privilege, uid: uidOrGroupName, cid: cid }));
return allowed;

View File

@@ -290,7 +290,7 @@ module.exports = function (Topics) {
};
Topics.markAsRead = async function (tids, uid) {
if (!Array.isArray(tids) || !tids.length) {
if (!Array.isArray(tids) || !tids.length || !utils.isNumber(uid)) {
return false;
}

View File

@@ -0,0 +1,24 @@
'use strict';
const db = require('../../database');
const batch = require('../../batch');
module.exports = {
name: 'Remove extraneous upvote and tids_read data for remote users',
timestamp: Date.UTC(2025, 11, 11),
method: async function () {
const { progress } = this;
await batch.processSortedSet('usersRemote:lastCrawled', async (uids) => {
const readKeys = uids.map(uid => `uid:${uid}:tids_read`);
const voteKeys = uids.map(uid => `uid:${uid}:upvote`);
const combined = readKeys.concat(voteKeys);
await db.deleteAll(combined);
progress.incr(uids.length);
}, {
batch: 500,
progress,
});
},
};

View File

@@ -164,6 +164,7 @@ describe('FEPs', () => {
});
pid = id;
({ activity } = await helpers.mocks.create(note));
await activitypub.inbox.create({ body: activity });
const activities = Array.from(activitypub._sent);
@@ -180,7 +181,6 @@ describe('FEPs', () => {
return activity.type === 'Announce' &&
activity.object && activity.object.type === 'Note';
});
assert(test1 && test2);
});

View File

@@ -432,6 +432,7 @@ describe('Notes', () => {
describe('Create', () => {
let uid;
let cid;
before(async () => {
uid = await user.create({ username: utils.generateUUID() });
@@ -451,6 +452,17 @@ describe('Notes', () => {
assert.strictEqual(cid, -1);
});
it('should not append to the tids_read sorted set', async () => {
const { note, id } = helpers.mocks.note();
const { activity } = helpers.mocks.create(note);
await db.sortedSetAdd(`followersRemote:${note.attributedTo}`, Date.now(), uid);
await activitypub.inbox.create({ body: activity });
const exists = await db.exists(`uid:${note.attributedTo}:tids_read`);
assert(!exists);
});
it('should create a new topic in a remote category if addressed (category same-origin)', async () => {
const { id: remoteCid } = helpers.mocks.group();
const { note, id } = helpers.mocks.note({
@@ -467,40 +479,63 @@ describe('Notes', () => {
});
it('should create a new topic in cid -1 if a non-same origin remote category is addressed', async function () {
this.timeout(30000);
const start = Date.now();
const { id: remoteCid } = helpers.mocks.group({
id: `https://example.com/${utils.generateUUID()}`,
});
console.log('1', Date.now() - start);
const { note, id } = helpers.mocks.note({
audience: [remoteCid],
});
console.log('2', Date.now() - start);
const { activity } = helpers.mocks.create(note);
console.log('3', Date.now() - start);
try {
await activitypub.inbox.create({ body: activity });
} catch (err) {
console.log('error in test', err.stack);
assert(false);
}
console.log('4', Date.now() - start);
assert(await posts.exists(id));
console.log('5', Date.now() - start);
const cid = await posts.getCidByPid(id);
console.log('6', Date.now() - start);
assert.strictEqual(cid, -1);
});
});
describe('(Like)', () => {
let pid;
let voterUid;
before(async () => {
({ cid } = await categories.create({ name: utils.generateUUID() }));
const { postData } = await topics.post({
uid,
cid,
title: utils.generateUUID(),
content: utils.generateUUID(),
});
pid = postData.pid;
const object = await activitypub.mocks.notes.public(postData);
const { activity } = helpers.mocks.like({ object });
voterUid = activity.actor;
await activitypub.inbox.like({ body: activity });
});
it('should increment a like for the post', async () => {
const voted = await posts.hasVoted(pid, voterUid);
const count = await posts.getPostField(pid, 'upvotes');
assert(voted);
assert.strictEqual(count, 1);
});
it('should not append to the uid upvotes zset', async () => {
const exists = await db.exists(`uid:${voterUid}:upvote`);
assert(!exists);
});
});
});
describe('Announce', () => {
let cid;
before(async () => {
({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
({ cid } = await categories.create({ name: utils.generateUUID() }));
});
describe('(Create)', () => {

View File

@@ -203,4 +203,241 @@ describe('Privilege logic for remote users/content (ActivityPub)', () => {
});
});
});
});
describe('Privilege masking', () => {
before(async () => {
// Grant default fediverse privileges
await install.giveWorldPrivileges();
});
describe('control', () => {
let cid;
let uid;
before(async () => {
// Set up a standard mock group
({ id: cid } = helpers.mocks.group());
// Unprivileged user for testing
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
});
it('should properly assert the remote category', async () => {
const assertion = await activitypub.actors.assertGroup([cid]);
const exists = await categories.exists(cid);
assert(assertion);
assert(exists);
});
it('should not set up a privilege mask for that category', async () => {
const exists = await db.exists(`cid:${cid}:privilegeMask`);
assert(!exists);
});
it('should pass the privileges .can() check if requested', async () => {
const set = await privileges.categories.get(cid, uid);
const can = await privileges.categories.can('topics:create', cid, uid);
assert(can);
});
it('should return true in the privilege set when requested', async () => {
const set = await privileges.categories.get(cid, uid);
assert(set);
assert(set['topics:create']);
});
});
describe('postingRestrictedToMods (true on assert)', () => {
let cid;
let uid;
before(async () => {
// Set up a mock group with `postingRestrictedToMods` bit set
({ id: cid } = helpers.mocks.group({
postingRestrictedToMods: true,
}));
// Unprivileged user for testing
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
});
it('should properly assert the remote category', async () => {
const assertion = await activitypub.actors.assertGroup([cid]);
const exists = await categories.exists(cid);
assert(assertion);
assert(exists);
});
it('should set up a privilege mask for that category', async () => {
const exists = await db.exists(`cid:${cid}:privilegeMask`);
assert(exists);
});
it('should contain a single mask', async () => {
const members = await db.getSetMembers(`cid:${cid}:privilegeMask`);
assert(members);
assert.strictEqual(members.length, 1);
assert.strictEqual(members[0], 'topics:create');
});
it('should fail the privileges .can() check if requested', async () => {
const can = await privileges.categories.can('topics:create', cid, uid);
assert(!can);
});
it('should return false in the privilege set when requested', async () => {
const set = await privileges.categories.get(cid, uid);
assert(set);
assert(!set['topics:create']);
});
});
describe('postingRestrictedToMods (true on assert and re-assertion)', () => {
let cid;
let uid;
before(async () => {
// Set up a mock group with `postingRestrictedToMods` bit set
({ id: cid } = helpers.mocks.group({
postingRestrictedToMods: true,
}));
// Unprivileged user for testing
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
// Assert group, then re-assert again
await activitypub.actors.assertGroup([cid]);
await activitypub.actors.assertGroup([cid], { update: true });
});
it('should fail the privileges .can() check if requested', async () => {
const can = await privileges.categories.can('topics:create', cid, uid);
assert(!can);
});
it('should return false in the privilege set when requested', async () => {
const set = await privileges.categories.get(cid, uid);
assert(set);
assert(!set['topics:create']);
});
});
describe('postingRestrictedToMods (true on assert, false on update)', () => {
let cid;
let uid;
before(async () => {
// Set up a mock group with `postingRestrictedToMods` bit set
({ id: cid } = helpers.mocks.group({
postingRestrictedToMods: true,
}));
// Unprivileged user for testing
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
await activitypub.actors.assertGroup([cid]);
// Group updated "remotely"
helpers.mocks.group({
id: cid,
postingRestrictedToMods: false,
});
});
it('should remove the privilege mask if the bit is not present on group actor update', async () => {
let can = await privileges.categories.can('topics:create', cid, uid);
assert(!can, 'Initial state should be denied due to mask.');
// Group re-assertion
await activitypub.actors.assertGroup([cid], { update: true });
// Ensure mask is gone from db
const memberCount = await db.setCount(`cid:${cid}:privilegeMask`);
assert.strictEqual(memberCount, 0);
can = await privileges.categories.can('topics:create', cid, uid);
assert(can, 'Privilege should be restored after mask removal.');
});
});
describe('postingRestrictedToMods (true on assert, property missing on update)', () => {
let cid;
let uid;
before(async () => {
// Set up a mock group with `postingRestrictedToMods` bit set
({ id: cid } = helpers.mocks.group({
postingRestrictedToMods: true,
}));
// Unprivileged user for testing
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
await activitypub.actors.assertGroup([cid]);
// Group updated "remotely"
helpers.mocks.group({
id: cid,
});
});
it('should remove the privilege mask if the bit is not present on group actor update', async () => {
let can = await privileges.categories.can('topics:create', cid, uid);
assert(!can, 'Initial state should be denied due to mask.');
// Group re-assertion
await activitypub.actors.assertGroup([cid], { update: true });
// Ensure mask is gone from db
const memberCount = await db.setCount(`cid:${cid}:privilegeMask`);
assert.strictEqual(memberCount, 0);
can = await privileges.categories.can('topics:create', cid, uid);
assert(can, 'Privilege should be restored after mask removal.');
});
});
describe('postingRestrictedToMods (false on assert, true on update)', () => {
let cid;
let uid;
before(async () => {
// Set up a mock group with `postingRestrictedToMods` bit set to false
({ id: cid } = helpers.mocks.group({
postingRestrictedToMods: false,
}));
// Unprivileged user for testing
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
await activitypub.actors.assertGroup([cid]);
// Update group "remotely", re-assert
helpers.mocks.group({
id: cid,
postingRestrictedToMods: true,
});
await activitypub.actors.assertGroup([cid], { update: true });
});
it('should fail the privileges .can() check if requested', async () => {
const can = await privileges.categories.can('topics:create', cid, uid);
assert(!can);
});
it('should return false in the privilege set when requested', async () => {
const set = await privileges.categories.get(cid, uid);
assert(set);
assert(!set['topics:create']);
});
});
});

View File

@@ -1,10 +1,11 @@
'use strict';
const db = require('./mocks/databasemock');
const assert = require('assert');
const nconf = require('nconf');
const request = require('../src/request');
const db = require('./mocks/databasemock');
const Categories = require('../src/categories');
const Topics = require('../src/topics');
const User = require('../src/user');

View File

@@ -88,6 +88,36 @@ describe('Set methods', () => {
done();
});
});
it('should add the values to each set', async () => {
await db.setsAdd(['saddarray1', 'saddarray2', 'saddarray3'], ['v1', 'v2', 'v3']);
const data = await db.getSetsMembers(['saddarray1', 'saddarray2', 'saddarray3']);
data.forEach(members => members.sort());
assert.deepStrictEqual(data, [
['v1', 'v2', 'v3'],
['v1', 'v2', 'v3'],
['v1', 'v2', 'v3'],
]);
});
});
describe('setAddBulk()', () => {
it('should add multiple key-member pairs', async () => {
await db.setAddBulk([
['bulkSet1', 'value1'],
['bulkSet2', 'value2'],
]);
let data = await db.getSetMembers('bulkSet1');
assert.deepStrictEqual(data, ['value1']);
data = await db.getSetMembers('bulkSet2');
assert.deepStrictEqual(data, ['value2']);
await db.setAddBulk([
['bulkSet1', 'value1'],
['bulkSet1', 'value3'],
]);
data = await db.getSetMembers('bulkSet1');
assert.deepStrictEqual(data.sort(), ['value1', 'value3']);
});
});
describe('getSetsMembers()', () => {
@@ -208,57 +238,42 @@ describe('Set methods', () => {
});
describe('setRemove()', () => {
before((done) => {
db.setAdd('testSet6', [1, 2], done);
it('should remove an element from set', async () => {
await db.setAdd('testSet6', [1, 2]);
await db.setRemove('testSet6', '2');
const isMember = await db.isSetMember('testSet6', '2');
assert.equal(isMember, false);
});
it('should remove a element from set', (done) => {
db.setRemove('testSet6', '2', function (err) {
assert.equal(err, null);
assert.equal(arguments.length, 1);
it('should remove multiple elements from set', async () => {
await db.setAdd('multiRemoveSet', [1, 2, 3, 4, 5]);
await db.setRemove('multiRemoveSet', [1, 3, 5]);
db.isSetMember('testSet6', '2', (err, isMember) => {
assert.equal(err, null);
assert.equal(isMember, false);
done();
});
});
const members = await db.getSetMembers('multiRemoveSet');
assert(members.includes('2'));
assert(members.includes('4'));
});
it('should remove multiple elements from set', (done) => {
db.setAdd('multiRemoveSet', [1, 2, 3, 4, 5], (err) => {
assert.ifError(err);
db.setRemove('multiRemoveSet', [1, 3, 5], (err) => {
assert.ifError(err);
db.getSetMembers('multiRemoveSet', (err, members) => {
assert.ifError(err);
assert(members.includes('2'));
assert(members.includes('4'));
done();
});
});
});
it('should remove multiple values from multiple keys', async () => {
await db.setAdd('multiSetTest1', ['one', 'two', 'three', 'four']);
await db.setAdd('multiSetTest2', ['three', 'four', 'five', 'six']);
await db.setRemove(['multiSetTest1', 'multiSetTest2'], ['three', 'four', 'five', 'doesnt exist']);
const members = await db.getSetsMembers(['multiSetTest1', 'multiSetTest2']);
assert.equal(members[0].length, 2);
assert.equal(members[1].length, 1);
assert(members[0].includes('one'));
assert(members[0].includes('two'));
assert(members[1].includes('six'));
});
it('should remove multiple values from multiple keys', (done) => {
db.setAdd('multiSetTest1', ['one', 'two', 'three', 'four'], (err) => {
assert.ifError(err);
db.setAdd('multiSetTest2', ['three', 'four', 'five', 'six'], (err) => {
assert.ifError(err);
db.setRemove(['multiSetTest1', 'multiSetTest2'], ['three', 'four', 'five', 'doesnt exist'], (err) => {
assert.ifError(err);
db.getSetsMembers(['multiSetTest1', 'multiSetTest2'], (err, members) => {
assert.ifError(err);
assert.equal(members[0].length, 2);
assert.equal(members[1].length, 1);
assert(members[0].includes('one'));
assert(members[0].includes('two'));
assert(members[1].includes('six'));
done();
});
});
});
});
it('should remove set if all elements are removed', async () => {
await db.setAdd('toBeDeletedSet', ['a', 'b']);
await db.setRemove('toBeDeletedSet', ['a', 'b']);
const exists = await db.exists('toBeDeletedSet');
assert.equal(exists, false);
});
});