mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-05-07 11:17:29 +02:00
Merge commit '799b08db3a6f74bf32b4fc45fbb4e94441e3892d' into v4.x
This commit is contained in:
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
- 5432:5432
|
||||
|
||||
redis:
|
||||
image: 'redis:7.4.3'
|
||||
image: 'redis:8.0.1'
|
||||
# Set health checks to wait until redis has started
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,3 +1,42 @@
|
||||
#### v4.3.2 (2025-05-12)
|
||||
|
||||
##### Chores
|
||||
|
||||
* up mentions (fcf9e8b7)
|
||||
* incrementing version number - v4.3.1 (308e6b9f)
|
||||
* update changelog for v4.3.1 (2310a7b8)
|
||||
* incrementing version number - v4.3.0 (bff291db)
|
||||
* incrementing version number - v4.2.2 (17fecc24)
|
||||
* incrementing version number - v4.2.1 (852a270c)
|
||||
* incrementing version number - v4.2.0 (87581958)
|
||||
* incrementing version number - v4.1.1 (b2afbb16)
|
||||
* incrementing version number - v4.1.0 (36c80850)
|
||||
* incrementing version number - v4.0.6 (4a52fb2e)
|
||||
* incrementing version number - v4.0.5 (1792a62b)
|
||||
* incrementing version number - v4.0.4 (b1125cce)
|
||||
* incrementing version number - v4.0.3 (2b65c735)
|
||||
* incrementing version number - v4.0.2 (73fe5fcf)
|
||||
* incrementing version number - v4.0.1 (a461b758)
|
||||
* incrementing version number - v4.0.0 (c1eaee45)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* sql injection in sortedSetScan (16504bad)
|
||||
* escape flag filters (285d438c)
|
||||
* #13407, don't restart user jobs (31be083e)
|
||||
* closes #13405, catch errors in ap.verify (8174578c)
|
||||
* send proper accept header for outgoing webfinger requests (20ab9069)
|
||||
* wrap generateCollection calls in try..catch to send 404 if thrown (64fdf91b)
|
||||
* #13397, null values in category sync list (26e6a222)
|
||||
* #13392, regression from c6f2c87, unable to unfollow from pending follows (401ff797)
|
||||
* #13397, update getCidByHandle to work with remote categories, fix sync with handles causing issues with null entries (a9a5ab5e)
|
||||
* correct stage name in dev dockerfile (#13393) (10077d0f)
|
||||
|
||||
##### Refactors
|
||||
|
||||
* wrap ap routes in try/catch (00668bdc)
|
||||
* call verify if request is POST (dfa21329)
|
||||
|
||||
#### v4.3.1 (2025-05-07)
|
||||
|
||||
##### Chores
|
||||
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
- ./install/docker/setup.json:/usr/src/app/setup.json
|
||||
|
||||
postgres:
|
||||
image: postgres:17.4-alpine
|
||||
image: postgres:17.5-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: nodebb
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7.4.3-alpine
|
||||
image: redis:8.0.1-alpine
|
||||
restart: unless-stopped
|
||||
command: ['redis-server', '--appendonly', 'yes', '--loglevel', 'warning']
|
||||
# command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] # uncomment if you want to use snapshotting instead of AOF
|
||||
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
- ./install/docker/setup.json:/usr/src/app/setup.json
|
||||
|
||||
redis:
|
||||
image: redis:7.4.3-alpine
|
||||
image: redis:8.0.1-alpine
|
||||
restart: unless-stopped
|
||||
command: ['redis-server', '--appendonly', 'yes', '--loglevel', 'warning']
|
||||
# command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] # uncomment if you want to use snapshotting instead of AOF
|
||||
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
- mongo-data:/data/db
|
||||
- ./install/docker/mongodb-user-init.js:/docker-entrypoint-initdb.d/user-init.js
|
||||
redis:
|
||||
image: redis:7.4.3-alpine
|
||||
image: redis:8.0.1-alpine
|
||||
restart: unless-stopped
|
||||
command: ['redis-server', '--appendonly', 'yes', '--loglevel', 'warning']
|
||||
# command: ['redis-server', '--save', '60', '1', '--loglevel', 'warning'] # uncomment if you want to use snapshotting instead of AOF
|
||||
@@ -34,7 +34,7 @@ services:
|
||||
- redis
|
||||
|
||||
postgres:
|
||||
image: postgres:17.4-alpine
|
||||
image: postgres:17.5-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: nodebb
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"dependencies": {
|
||||
"@adactive/bootstrap-tagsinput": "0.8.2",
|
||||
"@fontsource/inter": "5.2.5",
|
||||
"@fontsource/poppins": "5.2.5",
|
||||
"@fontsource/poppins": "5.2.6",
|
||||
"@fortawesome/fontawesome-free": "6.7.2",
|
||||
"@isaacs/ttlcache": "1.4.1",
|
||||
"@nodebb/spider-detector": "2.0.3",
|
||||
@@ -39,7 +39,7 @@
|
||||
"@textcomplete/contenteditable": "0.1.13",
|
||||
"@textcomplete/core": "0.1.13",
|
||||
"@textcomplete/textarea": "0.1.13",
|
||||
"ace-builds": "1.40.1",
|
||||
"ace-builds": "1.41.0",
|
||||
"archiver": "7.0.1",
|
||||
"async": "3.2.6",
|
||||
"autoprefixer": "10.4.21",
|
||||
@@ -47,8 +47,8 @@
|
||||
"benchpressjs": "2.5.5",
|
||||
"body-parser": "2.2.0",
|
||||
"bootbox": "6.0.3",
|
||||
"bootstrap": "5.3.5",
|
||||
"bootswatch": "5.3.5",
|
||||
"bootstrap": "5.3.6",
|
||||
"bootswatch": "5.3.6",
|
||||
"chalk": "4.1.2",
|
||||
"chart.js": "4.4.9",
|
||||
"cli-graph": "3.2.2",
|
||||
@@ -64,10 +64,10 @@
|
||||
"cookie-parser": "1.4.7",
|
||||
"cron": "4.3.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"csrf-sync": "4.1.0",
|
||||
"csrf-sync": "4.2.1",
|
||||
"daemon": "1.1.0",
|
||||
"diff": "7.0.0",
|
||||
"esbuild": "0.25.3",
|
||||
"diff": "8.0.1",
|
||||
"esbuild": "0.25.4",
|
||||
"express": "4.21.2",
|
||||
"express-session": "1.18.1",
|
||||
"express-useragent": "1.0.15",
|
||||
@@ -89,7 +89,7 @@
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"lodash": "4.17.21",
|
||||
"logrotate-stream": "0.2.9",
|
||||
"lru-cache": "10.4.3",
|
||||
"lru-cache": "11.1.0",
|
||||
"mime": "3.0.0",
|
||||
"mkdirp": "3.0.1",
|
||||
"mongodb": "6.16.0",
|
||||
@@ -102,8 +102,8 @@
|
||||
"nodebb-plugin-dbsearch": "6.2.16",
|
||||
"nodebb-plugin-emoji": "6.0.2",
|
||||
"nodebb-plugin-emoji-android": "4.1.1",
|
||||
"nodebb-plugin-markdown": "13.1.2",
|
||||
"nodebb-plugin-mentions": "4.7.5",
|
||||
"nodebb-plugin-markdown": "13.2.1",
|
||||
"nodebb-plugin-mentions": "4.7.6",
|
||||
"nodebb-plugin-spam-be-gone": "2.3.2",
|
||||
"nodebb-plugin-web-push": "0.7.4",
|
||||
"nodebb-rewards-essentials": "1.0.2",
|
||||
@@ -112,25 +112,25 @@
|
||||
"nodebb-theme-peace": "2.2.42",
|
||||
"nodebb-theme-persona": "14.1.11",
|
||||
"nodebb-widget-essentials": "7.0.38",
|
||||
"nodemailer": "6.10.1",
|
||||
"nodemailer": "7.0.3",
|
||||
"nprogress": "0.2.0",
|
||||
"passport": "0.7.0",
|
||||
"passport-http-bearer": "1.0.1",
|
||||
"passport-local": "1.0.0",
|
||||
"pg": "8.15.6",
|
||||
"pg-cursor": "2.14.6",
|
||||
"pg": "8.16.0",
|
||||
"pg-cursor": "2.15.0",
|
||||
"postcss": "8.5.3",
|
||||
"postcss-clean": "1.2.0",
|
||||
"progress-webpack-plugin": "1.0.16",
|
||||
"prompt": "1.3.0",
|
||||
"ioredis": "5.6.1",
|
||||
"rimraf": "5.0.10",
|
||||
"rimraf": "6.0.1",
|
||||
"rss": "1.2.2",
|
||||
"rtlcss": "4.3.0",
|
||||
"sanitize-html": "2.16.0",
|
||||
"sass": "1.87.0",
|
||||
"satori": "0.12.2",
|
||||
"semver": "7.7.1",
|
||||
"sanitize-html": "2.17.0",
|
||||
"sass": "1.88.0",
|
||||
"satori": "0.13.1",
|
||||
"semver": "7.7.2",
|
||||
"serve-favicon": "2.5.0",
|
||||
"sharp": "0.32.6",
|
||||
"sitemap": "8.0.0",
|
||||
@@ -147,7 +147,7 @@
|
||||
"toobusy-js": "0.5.1",
|
||||
"tough-cookie": "5.1.2",
|
||||
"validator": "13.15.0",
|
||||
"webpack": "5.99.7",
|
||||
"webpack": "5.99.8",
|
||||
"webpack-merge": "6.0.1",
|
||||
"winston": "3.17.0",
|
||||
"workerpool": "9.2.0",
|
||||
@@ -158,10 +158,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apidevtools/swagger-parser": "10.1.0",
|
||||
"@commitlint/cli": "19.8.0",
|
||||
"@commitlint/config-angular": "19.8.0",
|
||||
"@commitlint/cli": "19.8.1",
|
||||
"@commitlint/config-angular": "19.8.1",
|
||||
"coveralls": "3.1.1",
|
||||
"@eslint/js": "9.25.1",
|
||||
"@eslint/js": "9.26.0",
|
||||
"@stylistic/eslint-plugin-js": "4.2.0",
|
||||
"eslint-config-nodebb": "1.1.4",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
@@ -169,15 +169,15 @@
|
||||
"grunt-contrib-watch": "1.1.0",
|
||||
"husky": "8.0.3",
|
||||
"jsdom": "26.1.0",
|
||||
"lint-staged": "15.5.1",
|
||||
"mocha": "11.1.0",
|
||||
"lint-staged": "16.0.0",
|
||||
"mocha": "11.2.2",
|
||||
"mocha-lcov-reporter": "1.3.0",
|
||||
"mockdate": "3.0.5",
|
||||
"nyc": "17.1.0",
|
||||
"smtp-server": "3.13.6"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sass-embedded": "1.87.0"
|
||||
"sass-embedded": "1.88.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"*/jquery": "3.7.1"
|
||||
@@ -200,4 +200,4 @@
|
||||
"url": "https://github.com/barisusakli"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"mongo.bytes-out": "ביטים יוצאים",
|
||||
"mongo.num-requests": "מספר בקשות",
|
||||
"mongo.raw-info": "מידע לא מעובד מ-MongoDB",
|
||||
"mongo.unauthorized": "NodeBB לא הצליחה לקבל את המידע הדרוש מ-MongoDB. אנא בדוק שלמשתמש יש הרשאת clusterMonitor ל-admin database.",
|
||||
"mongo.unauthorized": "NodeBB לא הצליחה לקבל את המידע הדרוש מ-MongoDB. אנא בדקו שלמשתמש יש הרשאת clusterMonitor ל-admin database.",
|
||||
|
||||
"redis": "Redis",
|
||||
"redis.version": "גרסת Redis",
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
"route": "נתיב",
|
||||
"count": "ספירה",
|
||||
"no-routes-not-found": "הידד! אין שגיאות 404!",
|
||||
"clear404-confirm": "האם אתה בטוח שאתה רוצה לנקות את לוג שגיאות 404?",
|
||||
"clear404-confirm": "האם לנקות את לוג שגיאות 404?",
|
||||
"clear404-success": "שגיאות \"404 לא נמצא\" נוקו"
|
||||
}
|
||||
@@ -3,15 +3,15 @@
|
||||
"no-events": "אין אירועים",
|
||||
"control-panel": "בקרת אירועים",
|
||||
"delete-events": "מחיקת אירועים",
|
||||
"confirm-delete-all-events": "האם אתה בטוח שאתה רוצה למחוק את כל האירועים שנרשמו?",
|
||||
"confirm-delete-all-events": "האם למחוק את כל האירועים שנרשמו?",
|
||||
"filters": "מסננים",
|
||||
"filters-apply": "החל מסננים",
|
||||
"filters-apply": "החלת מסננים",
|
||||
"filter-type": "סוג אירוע",
|
||||
"filter-start": "מתאריך",
|
||||
"filter-end": "עד תאריך",
|
||||
"filter-user": "סינון לפי משתמש",
|
||||
"filter-user.placeholder": "הקלד שם משתמש לסינון...",
|
||||
"filter-user.placeholder": "הקלידו שם משתמש לסינון...",
|
||||
"filter-group": "סינון לפי קבוצה",
|
||||
"filter-group.placeholder": "הקלד שם קבוצה לסינון...",
|
||||
"filter-group.placeholder": "הקלידו שם קבוצה לסינון...",
|
||||
"filter-per-page": "פריטים בכל דף"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"logs": "לוגים",
|
||||
"control-panel": "בקרת לוגים",
|
||||
"reload": "טען לוג מחדש",
|
||||
"clear": "נקה לוגים",
|
||||
"reload": "טעינת לוגים מחדש",
|
||||
"clear": "ניקוי לוגים",
|
||||
"clear-success": "הלוגים נוקו!"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"customise": "התאמה אישית",
|
||||
"custom-css": "CSS/SASS מותאם אישית",
|
||||
"custom-css.description": "הזן כאן הצהרות CSS/SASS משלך, שיוחלו לאחר כל הסגנונות האחרים.",
|
||||
"custom-css.enable": "הפעל CSS/SASS מותאם אישית",
|
||||
"custom-css.description": "הזינו כאן הצהרות CSS/SASS משלך, שיוחלו לאחר כל הסגנונות האחרים.",
|
||||
"custom-css.enable": "הפעלת CSS/SASS מותאם אישית",
|
||||
|
||||
"custom-js": "Javascript מותאם אישית",
|
||||
"custom-js.description": "הכניסו כאן JavaScript משלכם, שיבוצע לאחר טעינת הדף לחלוטין.",
|
||||
@@ -15,6 +15,6 @@
|
||||
"custom-css.livereload": "הפעלת טעינה מחדש אוטומטית.",
|
||||
"custom-css.livereload.description": "הפעלה זו נועדה כדי לרענן את כל החיבורים מכל מכשיר, כאשר תשמרו את הדף המותאם אישית.",
|
||||
"bsvariables": "_variables.scss",
|
||||
"bsvariables.description": "ניתן לעקוף משתני Bootstrap כאן. אתה יכול גם להשתמש בכלי כמו <a href=\"https://bootstrap.build/app\">bootstrap.build</a> ולהדביק את הפלט כאן.<br/> שינויים דורשים בנייה מחדש והפעלה מחדש.",
|
||||
"bsvariables.description": "ניתן לעקוף משתני Bootstrap כאן. תוכלו גם להשתמש בכלי כמו <a href=\"https://bootstrap.build/app\">bootstrap.build</a> ולהדביק את הפלט כאן.<br/> שינויים דורשים בנייה מחדש והפעלה מחדש.",
|
||||
"bsvariables.enable": "Enable _variables.scss"
|
||||
}
|
||||
@@ -2,15 +2,15 @@
|
||||
"skins": "עיצובים",
|
||||
"bootswatch-skins": "עיצובי Bootswatch",
|
||||
"custom-skins": "עיצובים מותאמים אישית",
|
||||
"add-skin": "הוסף עיצוב",
|
||||
"save-custom-skins": "שמור עיצוב מותאם אישית",
|
||||
"add-skin": "הוספת עיצוב",
|
||||
"save-custom-skins": "שמירת עיצוב מותאם אישית",
|
||||
"save-custom-skins-success": "עיצובים מותאמים אישית נשמרו בהצלחה",
|
||||
"custom-skin-name": "שם עיצוב מותאם אישית",
|
||||
"custom-skin-variables": "משתני עיצוב מותאם אישית",
|
||||
"loading": "טוען עיצובים",
|
||||
"homepage": "דף הפרוייקט",
|
||||
"select-skin": "בחר עיצוב זה",
|
||||
"revert-skin": "חזור לעיצוב מקורי",
|
||||
"select-skin": "בחירת עיצוב זה",
|
||||
"revert-skin": "חזרה לעיצוב מקורי",
|
||||
"current-skin": "עיצוב נוכחי",
|
||||
"skin-updated": "עיצוב עודכן",
|
||||
"applied-success": "עיצוב %1 הוחל בהצלחה",
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"themes": "ערכות נושא",
|
||||
"checking-for-installed": "בודק ערכות נושא מותקנות...",
|
||||
"homepage": "דף הבית",
|
||||
"select-theme": "בחר ערכת נושא",
|
||||
"revert-theme": "חזור לערכת נושא מקורית",
|
||||
"select-theme": "בחירת ערכת נושא",
|
||||
"revert-theme": "חזרה לערכת נושא מקורית",
|
||||
"current-theme": "ערכת נושא נוכחית",
|
||||
"no-themes": "לא נמצאו ערכות נושא מותקנות",
|
||||
"revert-confirm": "האם אתה בטוח שאתה רוצה לשחזר את ערכת הנושא הרגילה של NodeBB?",
|
||||
"revert-confirm": "האם לשחזר את ערכת הנושא הרגילה של NodeBB?",
|
||||
"theme-changed": "ערכת הנושא שונתה",
|
||||
"revert-success": "החזרת בהצלחה את הפורום שלך לערכת הנושא ברירת המחדל.",
|
||||
"restart-to-activate": "אנא בצע בנייה והפעלה מחדש כדי להחיל את ערכת הנושא הזו."
|
||||
"revert-success": "החזרתם בהצלחה את הפורום שלכם לערכת הנושא ברירת המחדל.",
|
||||
"restart-to-activate": "אנא בצעו בנייה והפעלה מחדש כדי להחיל את ערכת הנושא הזו."
|
||||
}
|
||||
@@ -12,8 +12,8 @@
|
||||
"page-views-custom": "טווח תאריכים מותאם אישית",
|
||||
"page-views-custom-start": "תחילת טווח",
|
||||
"page-views-custom-end": "סוף טווח",
|
||||
"page-views-custom-help": "הכנס טווח תאריכים של התקופה בה תרצה לצפות בתעבורת הפורום. הפורמט הנדרש הוא <code>YYYY-MM-DD</code>",
|
||||
"page-views-custom-error": "הזן טווח תאריכים תקין כדלהלן <code>YYYY-MM-DD</code>",
|
||||
"page-views-custom-help": "הכניסו טווח תאריכים של התקופה בה תרצו לצפות בתעבורת הפורום. הפורמט הנדרש הוא <code>YYYY-MM-DD</code>",
|
||||
"page-views-custom-error": "הזינו טווח תאריכים תקין כדלהלן <code>YYYY-MM-DD</code>",
|
||||
|
||||
"stats.yesterday": "אתמול",
|
||||
"stats.today": "היום",
|
||||
@@ -25,21 +25,21 @@
|
||||
|
||||
"updates": "עדכונים",
|
||||
"running-version": "הפורום מעודכן ל<strong>גרסה <span id=\"version\">%1</span></strong>",
|
||||
"keep-updated": "לעדכוני אבטחה ותיקוני באגים, וודא שהפורום שלך עדכני לגרסה האחרונה.",
|
||||
"up-to-date": "אתה <strong>מעודכן</strong> <i class=\"fa fa-check\"></i>",
|
||||
"keep-updated": "לעדכוני אבטחה ותיקוני באגים, וודאו שהפורום שלכם עדכני לגרסה האחרונה.",
|
||||
"up-to-date": "הינך <strong>מעודכן</strong> <i class=\"fa fa-check\"></i>",
|
||||
"upgrade-available": "גרסה חדשה (v%1) שוחררה. שקול <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">לעדכן את הפורום שלך</a>.",
|
||||
"prerelease-upgrade-available": "זוהי גרסת טרום-הפצה מיושנת של NodeBB. גרסה חדשה (v%1) שוחררה. שקול <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">לשדרג את ה-NodeBB שלך</a>.",
|
||||
"prerelease-warning": "<i class=\"fa fa-exclamation-triangle\"></i> זוהי גרסת <strong>טרום-הפצה</strong> של NodeBB. עלולים להתרחש באגים לא מכוונים.",
|
||||
"fallback-emailer-not-found": "Fallback emailer לא נמצא!",
|
||||
"running-in-development": "הפורום פועל במצב פיתוח. הפורום עשוי להיות חשוף לפגיעויות פוטנציאליות; אנא פנה למנהל המערכת שלך",
|
||||
"latest-lookup-failed": "לא הצליח לחפש את הגרסה האחרונה הזמינה של NodeBB",
|
||||
"running-in-development": "הפורום פועל במצב פיתוח. הפורום עשוי להיות חשוף לפגיעויות פוטנציאליות; אנא פנו למנהל המערכת שלכם",
|
||||
"latest-lookup-failed": "לא הצליח למצוא את הגרסה האחרונה הזמינה של NodeBB",
|
||||
|
||||
"notices": "התראות",
|
||||
"restart-not-required": "לא נדרשת הפעלה מחדש",
|
||||
"restart-required": "נדרשת הפעלה מחדש",
|
||||
"search-plugin-installed": "תוסף חיפוש הותקן",
|
||||
"search-plugin-not-installed": "תוסף חיפוש לא הותקן",
|
||||
"search-plugin-tooltip": "התקן את תוסף החיפוש מעמוד התוספים על מנת להפעיל את אפשרות החיפוש",
|
||||
"search-plugin-tooltip": "התקינו את תוסף החיפוש מעמוד התוספים על מנת להפעיל את אפשרות החיפוש",
|
||||
|
||||
"control-panel": "שליטת מערכת",
|
||||
"rebuild-and-restart": "בנייה והפעלה מחדש",
|
||||
@@ -47,9 +47,9 @@
|
||||
"restart-warning": "הפעלה או בניה מחדש של הפורום תנתק את כל החיבורים הקיימים למספר שניות",
|
||||
"restart-disabled": "הפעלה או בניה מחדש של הפורום בוטלה, נראה שאינך מפעיל את הפורום דרך שרת מתאים.",
|
||||
"maintenance-mode": "מצב תחזוקה",
|
||||
"maintenance-mode-title": "לחץ כאן על מנת להכניס את הפורום למצב תחזוקה",
|
||||
"maintenance-mode-title": "לחצו כאן על מנת להכניס את הפורום למצב תחזוקה",
|
||||
"dark-mode": "מצב כהה",
|
||||
"realtime-chart-updates": "עדכן תרשים בזמן אמת",
|
||||
"realtime-chart-updates": "עדכון תרשים בזמן אמת",
|
||||
|
||||
"active-users": "משתמשים פעילים",
|
||||
"active-users.users": "משתמשים",
|
||||
@@ -91,11 +91,11 @@
|
||||
"start": "התחלה",
|
||||
"end": "סיום",
|
||||
"filter": "סינון",
|
||||
"view-as-json": "הצג כ-JSON",
|
||||
"expand-analytics": "הרחב ניתוח",
|
||||
"clear-search-history": "מחק היסטוריית חיפושים",
|
||||
"clear-search-history-confirm": "האם אתה בטוח שברצונך למחוק את כל היסטוריית החיפושים?",
|
||||
"view-as-json": "הצגה כ-JSON",
|
||||
"expand-analytics": "הרחבת ניתוח",
|
||||
"clear-search-history": "מחיקת היסטוריית חיפושים",
|
||||
"clear-search-history-confirm": "האם למחוק את כל היסטוריית החיפושים?",
|
||||
"search-term": "מונח",
|
||||
"search-count": "כמות",
|
||||
"view-all": "הצג הכל"
|
||||
"view-all": "הצגת הכל"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"you-are-on": "אתה נמצא ב <strong>%1:%2</strong>",
|
||||
"you-are-on": "הינכם נמצאים ב <strong>%1:%2</strong>",
|
||||
"ip": "IP <strong>%1</strong>",
|
||||
"nodes-responded": "%1 nodes הגיבו בתוך %2 מילי שניות!",
|
||||
"host": "host",
|
||||
@@ -17,7 +17,7 @@
|
||||
"cpu-usage": "שימוש ב-CPU",
|
||||
"uptime": "משך זמן פעולת המערכת ללא השבתה",
|
||||
|
||||
"registered": "רשום",
|
||||
"registered": "רשומים",
|
||||
"sockets": "Sockets",
|
||||
"connection-count": "כמות חיבורים",
|
||||
"guests": "אורחים",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"enable-http": "הפעלת רישום HTTP",
|
||||
"enable-socket": "הפעלת רישום אירועים ב-socket.io",
|
||||
"file-path": "נתיב קובץ לוג",
|
||||
"file-path-placeholder": "/path/to/log/file.log ::: השאירו ריק כדי שהלוג ישמר בטרמינל שלך",
|
||||
"file-path-placeholder": "/path/to/log/file.log ::: השאירו ריק כדי שהלוג ישמר בטרמינל שלכם",
|
||||
|
||||
"control-panel": "ניהול לוגים",
|
||||
"update-settings": "עידכון הגדרות לוגים"
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"condition-is": "הינו:",
|
||||
"condition-then": "תגמל ב:",
|
||||
"max-claims": "מספר פעמים בה ניתן לדרוש תגמול",
|
||||
"zero-infinite": "הזן 0 ללא הגבלה",
|
||||
"select-reward": "בחר תגמול",
|
||||
"delete": "מחק",
|
||||
"enable": "הפעל",
|
||||
"disable": "השבת",
|
||||
"zero-infinite": "הזינו 0 ללא הגבלה",
|
||||
"select-reward": "בחירת תגמול",
|
||||
"delete": "מחיקה",
|
||||
"enable": "הפעלה",
|
||||
"disable": "השבתה",
|
||||
|
||||
"alert.delete-success": "תגמול נמחק בהצלחה",
|
||||
"alert.no-inputs-found": "תגמול לא חוקי - לא נמצא מידע!",
|
||||
|
||||
@@ -16,22 +16,22 @@
|
||||
"container.body": "גוף",
|
||||
"container.alert": "התראה",
|
||||
|
||||
"alert.confirm-delete": "האם אתה בטוח שאתה רוצה למחוק את הווידג'ט?",
|
||||
"alert.confirm-delete": "האם למחוק את הווידג'ט?",
|
||||
"alert.updated": "העלאת ווידג'טים",
|
||||
"alert.update-success": "הווידג'טים הועלו בהצלחה",
|
||||
"alert.clone-success": "הווידג'טים שוכפלו בהצלחה",
|
||||
|
||||
"error.select-clone": "בחר דף לשכפל ממנו",
|
||||
"error.select-clone": "בחרו דף לשכפל ממנו",
|
||||
|
||||
"title": "כותרת",
|
||||
"title.placeholder": "כותרת (מוצגת רק בגורמים מכילים מסוימים)",
|
||||
"container": "גורם מכיל",
|
||||
"container.placeholder": "גרור ושחרר גורם מכיל (container) או הזן HTML כאן.",
|
||||
"title.placeholder": "כותרת (מוצגת רק ב-containers מסוימים)",
|
||||
"container": "גורם מכיל - Container",
|
||||
"container.placeholder": "גררו ושחררו גורם מכיל (container) או הזינו HTML כאן.",
|
||||
"show-to-groups": "יוצג בקבוצות",
|
||||
"hide-from-groups": "יוסתר מקבוצות",
|
||||
"start-date": "תאריך התחלה",
|
||||
"end-date": "תאריך סיום",
|
||||
"hide-on-mobile": "הסתר במובייל",
|
||||
"hide-drafts": "הסתר טיוטות",
|
||||
"show-drafts": "הצג טיוטות"
|
||||
"hide-on-mobile": "הסתרה במובייל",
|
||||
"hide-drafts": "הסתרת טיוטות",
|
||||
"show-drafts": "הצגת טיוטות"
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"manage-categories": "ניהול קטגוריות",
|
||||
"add-category": "הוסף קטגוריה",
|
||||
"jump-to": "קפוץ אל...",
|
||||
"add-category": "הוספת קטגוריה",
|
||||
"jump-to": "קפיצה אל...",
|
||||
"settings": "הגדרות קטגוריות",
|
||||
"edit-category": "ערוך קטגוריה",
|
||||
"edit-category": "עריכת קטגוריה",
|
||||
"privileges": "הרשאות",
|
||||
"back-to-categories": "חזרה לרשימת הקטגוריות",
|
||||
"name": "שם קטגוריה",
|
||||
@@ -23,7 +23,7 @@
|
||||
"is-section": "הגדר קטגוריה זו כמקטע ללא אפשרות כניסה, רק לתתי קטגוריות.",
|
||||
"post-queue": "תור פוסטים",
|
||||
"tag-whitelist": "רשימה לבנה של תגיות",
|
||||
"upload-image": "העלה תמונה",
|
||||
"upload-image": "העלו תמונה",
|
||||
"upload": "העלאה",
|
||||
"delete-image": "הסרה",
|
||||
"category-image": "תמונת קטגוריה",
|
||||
@@ -32,8 +32,8 @@
|
||||
"optional-parent-category": "קטגוריית הורים (אופציונלי)",
|
||||
"top-level": "רמה עליונה",
|
||||
"parent-category-none": "(ללא)",
|
||||
"copy-parent": "העתק אב",
|
||||
"copy-settings": "העתק הגדרות מ:",
|
||||
"copy-parent": "העתקת אב",
|
||||
"copy-settings": "העתקת הגדרות מ:",
|
||||
"optional-clone-settings": "שכפול הגדרות מקטגוריה (אופציונלי)",
|
||||
"clone-children": "שכפול קטגוריות והגדרות של צאצאים",
|
||||
"purge": "מחיקת קטגוריה",
|
||||
@@ -59,7 +59,7 @@
|
||||
"privileges.section-moderation": "הרשאות מנחה",
|
||||
"privileges.section-other": "אחר",
|
||||
"privileges.section-user": "משתמש",
|
||||
"privileges.search-user": "הוסף משתמש",
|
||||
"privileges.search-user": "הוספת משתמש",
|
||||
"privileges.no-users": "אין הרשאות ספציפיות למשתמש בקטגוריה זו.",
|
||||
"privileges.section-group": "קבוצה",
|
||||
"privileges.group-private": "קבוצה זו פרטית",
|
||||
@@ -89,7 +89,7 @@
|
||||
"federation.syncing-intro": "קטגוריה יכולה לעקוב אחר \"שחקן קבוצתי\" באמצעות פרוטוקול ActivityPub. אם יתקבל תוכן מאחד השחקנים המפורטים למטה, הוא יתווסף אוטומטית לקטגוריה זו.",
|
||||
"federation.syncing-caveat": "הגדרת סנכרון N.B. כאן יוצרת סנכרון חד כיווני. NodeBB מנסה להירשם/לעקוב אחרי השחקן, אך לא ניתן להניח את ההיפך.",
|
||||
"federation.syncing-none": "קטגוריה זו אינה עוקבת כעת אחר אף אחד.",
|
||||
"federation.syncing-add": "סנכרן עם...",
|
||||
"federation.syncing-add": "סינכרון עם...",
|
||||
"federation.syncing-actorUri": "שחקן",
|
||||
"federation.syncing-follow": "עקוב",
|
||||
"federation.syncing-unfollow": "הפסקת מעקב",
|
||||
@@ -101,7 +101,7 @@
|
||||
|
||||
"alert.created": "נוצר",
|
||||
"alert.create-success": "קטגוריה נוצרה בהצלחה!",
|
||||
"alert.none-active": "אין לך קטגוריות פעילות.",
|
||||
"alert.none-active": "אין לכם קטגוריות פעילות.",
|
||||
"alert.create": "יצירת קטגוריה",
|
||||
"alert.confirm-purge": "<p class=\"lead\">האם אתם בטוחים שאתם רוצים למחוק את קטגוריית \"%1\"?</p><h5><strong class=\"text-danger\">אזהרה!</strong> כל הנושאים והפוסטים בקטגוריה זו ימחקו!</h5> <p class=\"help-block\">מחיקת קטגוריה תסיר את כל הנושאים והפוסטים ותמחק את הקטגוריה ממסד הנתונים. אם ברצונכם להסיר את הקטגוריה <em>באופן זמני</em>, בחרו ב\"השבתת\" הקטגוריה.</p>",
|
||||
"alert.purge-success": "הקטגוריה נמחקה!",
|
||||
|
||||
@@ -83,11 +83,11 @@
|
||||
"search.placeholder": "חיפוש הגדרות",
|
||||
"search.no-results": "אין תוצאות...",
|
||||
"search.search-forum": "חפש בפורום <strong></strong>",
|
||||
"search.keep-typing": "המשך להקליד על מנת למצוא תוצאות...",
|
||||
"search.start-typing": "התחל להקליד על מנת לראות תוצאות...",
|
||||
"search.keep-typing": "המשיכו להקליד על מנת למצוא תוצאות...",
|
||||
"search.start-typing": "התחילו להקליד על מנת לראות תוצאות...",
|
||||
|
||||
"connection-lost": "החיבור ל-%1 אבד, מנסה להתחבר מחדש...",
|
||||
|
||||
"alerts.version": "מעודכן ל-<strong>NodeBB v%1</strong>",
|
||||
"alerts.upgrade": "שדרג ל v%1"
|
||||
"alerts.upgrade": "שדרגו ל v%1"
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"notifications": "התראות",
|
||||
"chat-messages": "הודעות צ'אט",
|
||||
"play-sound": "נגן",
|
||||
"play-sound": "נגינה",
|
||||
"incoming-message": "הודעה נכנסת",
|
||||
"outgoing-message": "הודעה יוצאת",
|
||||
"upload-new-sound": "העלה צליל חדש",
|
||||
"upload-new-sound": "העלאת צליל חדש",
|
||||
"saved": "הגדרות נשמרו"
|
||||
}
|
||||
@@ -64,7 +64,7 @@
|
||||
"show-email": "הצג כתובת אימייל",
|
||||
"show-fullname": "הצג שם מלא",
|
||||
"restrict-chat": "אשר הודעות צ'אט רק ממשתמשים שאני עוקב אחריהם",
|
||||
"disable-incoming-chats": "Disable incoming chat messages",
|
||||
"disable-incoming-chats": "השבתת הודעות צ'אט נכנסות",
|
||||
"outgoing-new-tab": "פתח קישורים חיצוניים בכרטיסייה חדשה",
|
||||
"topic-search": "הפעל חיפוש בתוך נושא",
|
||||
"update-url-with-post-index": "עדכן את כתובת הURL עם מספר הפוסט הנוכחי בזמן גלישה בנושאים",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"new-topic-button": "נושא חדש",
|
||||
"guest-login-post": "התחברו כדי לפרסם",
|
||||
"no-topics": "<strong>קטגוריה זו ריקה מנושאים.</strong><br />למה שלא תנסו להוסיף נושא חדש?",
|
||||
"no-followers": "Nobody on this website is tracking or watching this category. Track or watch this category in order to begin receiving updates.",
|
||||
"no-followers": "אף אחד באתר זה לא עוקב אחר או צופה בקטגוריה זו. עקבו אחר או צפו בקטגוריה זו כדי להתחיל לקבל עדכונים.",
|
||||
"browsing": "צופים בנושא זה כעת",
|
||||
"no-replies": "אין תגובות",
|
||||
"no-new-posts": "אין פוסטים חדשים.",
|
||||
|
||||
@@ -5,29 +5,29 @@
|
||||
"invite": "הזמנה מ%1",
|
||||
"greeting-no-name": "שלום",
|
||||
"greeting-with-name": "שלום %1",
|
||||
"email.verify-your-email.subject": "בבקשה אמת את המייל שלך.",
|
||||
"email.verify-your-email.subject": "נא לאמת את המייל שלך.",
|
||||
"email.verify.text1": "ביקשת לשנות או לאמת את כתובת הדוא\"ל שלך",
|
||||
"email.verify.text2": "לצורכי אבטחה, אנו משנים או מאמתים את כתובת הדוא\"ל רק לאחר שבעלותך מאומת ע\"י דוא\"ל.<strong> אם לא אתה ביקשת זאת, תוכל להתעלם ממייל זו.</strong>",
|
||||
"email.verify.text3": "לאחר שתאשר את כתובת הדוא\"ל, נשנה את כתובת דוא\"ל הנוכחית בכתובת הזו (%1).",
|
||||
"welcome.text1": "תודה שנרשמת ל%1!",
|
||||
"welcome.text2": "על מנת להפעיל את החשבון שלך, אנו צריכים לוודא שאתה בעל חשבון המייל שנרשמת איתו.",
|
||||
"welcome.text3": "מנהל אישר את ההרשמה שלך.\nאתה יכול להתחבר עם השם משתמש והסיסמא שלך מעכשיו.",
|
||||
"welcome.cta": "לחץ כאן על מנת לאשר את כתובת המייל שלך.",
|
||||
"welcome.text2": "להפעלת החשבון, נדרש אימות שהאימייל אתו נרשמתם, הוא בבעלותכם.",
|
||||
"welcome.text3": "מנהל אישר את ההרשמה שלך.\nניתן להתחבר עם השם משתמש והסיסמא שלך מעכשיו.",
|
||||
"welcome.cta": "לחצו כאן על מנת לאשר את כתובת המייל שלכם.",
|
||||
"invitation.text1": "%1 הזמין אותך להצטרף ל%2",
|
||||
"invitation.text2": "ההזמנה של תפוג ב %1 ימים",
|
||||
"invitation.cta": "לחץ כאן ליצירת החשבון שלך.",
|
||||
"reset.text1": "קיבלנו בקשה לאפס את הסיסמה לחשבון שלך, כנראה מפני ששכחת אותה. אם לא ביקשת לאפס את הסיסמה, אנא התעלם ממייל זה.",
|
||||
"reset.text2": "על מנת להמשיך עם תהליך איפוס הסיסמה, אנא לחץ על הלינק הבא:",
|
||||
"reset.cta": "לחץ כאן לאפס את הסיסמה שלך.",
|
||||
"invitation.cta": "לחצו כאן ליצירת החשבון שלכם.",
|
||||
"reset.text1": "קיבלנו בקשה לאפס את הסיסמה לחשבון שלכם, כנראה מפני ששכחתם אותה. אם לא ביקשתם לאפס את הסיסמה, אנא התעלמו מאימייל זה.",
|
||||
"reset.text2": "על מנת להמשיך עם תהליך איפוס הסיסמה, אנא לחצו על הלינק הבא:",
|
||||
"reset.cta": "לחצו כאן לאפס את הסיסמה שלכם.",
|
||||
"reset.notify.subject": "הסיסמה שונתה בהצלחה.",
|
||||
"reset.notify.text1": "אנו מודיעים לך שב%1, סיסמתך שונתה בהצלחה.",
|
||||
"reset.notify.text2": "אם לא אישרת בקשה זו, אנא הודע למנהל מיד.",
|
||||
"reset.notify.text1": "רצינו להסב את תשומת ליבך שב%1, סיסמתך שונתה בהצלחה.",
|
||||
"reset.notify.text2": "אם לא אישרתם בקשה זו, אנא הודיעו למנהל בהקדם.",
|
||||
"digest.unread-rooms": "חדרים שלא נקראו",
|
||||
"digest.room-name-unreadcount": "%1 (%2 לא נקראו)",
|
||||
"digest.latest-topics": "נושאים אחרונים מ%1",
|
||||
"digest.top-topics": "נושאים עם הכי הרבה הצבעות מ-%1",
|
||||
"digest.popular-topics": "הנושאים הכי פופולריים מ-%1",
|
||||
"digest.cta": "לחץ כאן כדי לבקר ב %1",
|
||||
"digest.cta": "לחצו כאן כדי לבקר ב %1",
|
||||
"digest.unsub.info": "תקציר זה נשלח אליך על-פי הגדרות החשבון שלך.",
|
||||
"digest.day": "יום",
|
||||
"digest.week": "שבוע",
|
||||
@@ -36,23 +36,23 @@
|
||||
"digest.title.day": "התקציר היומי שלך",
|
||||
"digest.title.week": "התקציר השבועי שלך",
|
||||
"digest.title.month": "התקציר החודשי שלך",
|
||||
"notif.chat.new-message-from-user": "New message from \"%1\"",
|
||||
"notif.chat.new-message-from-user-in-room": "New message from %1 in room %2",
|
||||
"notif.chat.cta": "לחץ כאן כדי להמשיך את השיחה",
|
||||
"notif.chat.unsub.info": "התראה הצ'אט הזו נשלחה אליך על-פי הגדרות החשבון שלך.",
|
||||
"notif.post.unsub.info": "התראת הפוסט הזו נשלחה אליך על-פי הגדרות החשבון שלך.",
|
||||
"notif.post.unsub.one-click": "ניתן גם להפסיק את קבלת המיילים כמו זה, בלחיצה על",
|
||||
"notif.chat.new-message-from-user": "הודעה חדשה מ-\"%1\"",
|
||||
"notif.chat.new-message-from-user-in-room": "הודעה חדשה מ-%1 בחדר %2",
|
||||
"notif.chat.cta": "לחצו כאן כדי להמשיך את השיחה",
|
||||
"notif.chat.unsub.info": "הודעת צ'אט זו נשלחה אליך על-פי הגדרות החשבון שלך.",
|
||||
"notif.post.unsub.info": "התראת פוסט זו נשלחה אליך על-פי הגדרות החשבון שלך.",
|
||||
"notif.post.unsub.one-click": "ניתן גם להפסיק את קבלת האימיילים כמו זה, בלחיצה על",
|
||||
"notif.cta": "כניסה לפורום",
|
||||
"notif.cta-new-reply": "הצג פוסט",
|
||||
"notif.cta-new-chat": "הצג צ'אט",
|
||||
"notif.test.short": "בדיקת התראות.",
|
||||
"notif.test.long": "זוהי בדיקה של ההתראות במייל.",
|
||||
"notif.cta-new-reply": "הצגת פוסט",
|
||||
"notif.cta-new-chat": "הצגת צ'אט",
|
||||
"notif.test.short": "בדיקת התראות",
|
||||
"notif.test.long": "זוהי בדיקה של ההתראות באימייל.",
|
||||
"test.text1": "זהו אימייל ניסיון על מנת לוודא שהגדרות המייל בוצעו כהלכה בהגדרות NodeBB.",
|
||||
"unsub.cta": "לחץ כאן לשנות הגדרות אלו",
|
||||
"unsubscribe": "בטל רישום",
|
||||
"unsub.success": "אתה לא תקבל יותר מיילים מרשימת התפוצה של <strong>%1</strong>",
|
||||
"unsub.cta": "לחצו כאן לשנות הגדרות אלו",
|
||||
"unsubscribe": "ביטול רישום",
|
||||
"unsub.success": "לא תקבלו יותר אימיילים מרשימת התפוצה של <strong>%1</strong>",
|
||||
"unsub.failure.title": "לא ניתן לבטל את המנוי",
|
||||
"unsub.failure.message": "למרבה הצער, לא הצלחנו לבטל את הרישום שלך מרשימת התפוצה, מכיוון שהייתה בעיה בקישור. עם זאת, אתה יכול לשנות את העדפות הדוא\"ל שלך על ידי מעבר ל<a href=\"%2\">הגדרות המשתמש שלך</a>.<br /><br />(שגיאה: <code>%1</code>)",
|
||||
"unsub.failure.message": "למרבה הצער, לא הצלחנו לבטל את הרישום שלך מרשימת התפוצה, מכיוון שהייתה בעיה בקישור. עם זאת, תוכלו לשנות את העדפות הדוא\"ל שלכם על ידי מעבר ל<a href=\"%2\">הגדרות המשתמש שלכם</a>.<br /><br />(שגיאה: <code>%1</code>)",
|
||||
"banned.subject": "הורחקת מ %1",
|
||||
"banned.text1": "המשתמש %1 הורחק מ %2.",
|
||||
"banned.text2": "הרחקה זו תמשך עד %1",
|
||||
|
||||
@@ -67,8 +67,8 @@
|
||||
"no-chat-room": "חדר צ'אט לא קיים",
|
||||
"no-privileges": "ההרשאות שלכם אינן מספיקות לביצוע פעולה זו.",
|
||||
"category-disabled": "קטגוריה לא פעילה",
|
||||
"post-deleted": "Post deleted",
|
||||
"topic-locked": "Topic locked",
|
||||
"post-deleted": "הפוסט נמחק",
|
||||
"topic-locked": "הנושא נעול",
|
||||
"post-edit-duration-expired": "ניתן לערוך פוסטים עד %1 שניות מרגע כתיבת הפוסט",
|
||||
"post-edit-duration-expired-minutes": "ניתן לערוך פוסטים עד %1 דקות מרגע כתיבת הפוסט",
|
||||
"post-edit-duration-expired-minutes-seconds": "ניתן לערוך פוסטים עד %1 דקות %2 שניות מרגע כתיבת הפוסט",
|
||||
@@ -142,20 +142,20 @@
|
||||
"group-leave-disabled": "אינכם רשאים לעזוב את הקבוצה כרגע",
|
||||
"group-user-not-pending": "למשתמש אין בקשה ממתינה להצטרף לקבוצה זו.",
|
||||
"gorup-user-not-invited": "המשתמש לא הוזמן להצטרף לקבוצה זו.",
|
||||
"post-already-deleted": "פוסט זה נמחק כבר",
|
||||
"post-already-deleted": "פוסט זה כבר נמחק",
|
||||
"post-already-restored": "פוסט זה כבר שוחזר",
|
||||
"topic-already-deleted": "נושא זה כבר נמחק",
|
||||
"topic-already-restored": "נושא זה כבר שוחזר",
|
||||
"cant-purge-main-post": "אינכם יכולים למחוק את הפוסט הראשי, אנא מחקו את הנושא במקום",
|
||||
"cant-purge-main-post": "לא ניתן למחוק את הפוסט הראשי, ניתן למחוק את הנושא במקום זה",
|
||||
"topic-thumbnails-are-disabled": "תמונות ממוזערות לנושא אינן מאופשרות.",
|
||||
"invalid-file": "קובץ לא תקין",
|
||||
"uploads-are-disabled": "העלאת קבצים אינה מאופשרת",
|
||||
"signature-too-long": "מצטערים, החתימה שלכם אינה יכולה להכיל יותר מ-%1 תווים.",
|
||||
"about-me-too-long": "מצטערים, דף האודות שלכם אינו יכול להיות ארוך מ-%1 תווים.",
|
||||
"signature-too-long": "מצטערים, החתימה אינה יכולה להכיל יותר מ-%1 תווים.",
|
||||
"about-me-too-long": "מצטערים, דף האודות אינו יכול להיות ארוך מ-%1 תווים.",
|
||||
"cant-chat-with-yourself": "לא ניתן לעשות צ'אט עם עצמכם!",
|
||||
"chat-restricted": "משתמש זה חסם את הודעות הצ'אט שלו ממשתמשים זרים. המשתמש חייב לעקוב אחריכם לפני שתוכלו לשוחח איתו בצ'אט",
|
||||
"chat-allow-list-user-already-added": "This user is already in your allow list",
|
||||
"chat-deny-list-user-already-added": "This user is already in your deny list",
|
||||
"chat-allow-list-user-already-added": "משתמש זה כבר נמצא ברשימה הלבנה שלכם",
|
||||
"chat-deny-list-user-already-added": "משתמש זה כבר נמצא ברשימת הדחייה שלכם",
|
||||
"chat-user-blocked": "נחסמתם על ידי משתמש זה.",
|
||||
"chat-disabled": "מערכת הצ'אט לא פעילה",
|
||||
"too-many-messages": "שלחתם יותר מדי הודעות, אנא המתינו זמן מה.",
|
||||
@@ -171,21 +171,21 @@
|
||||
"cant-add-users-to-chat-room": "לא ניתן להוסיף משתמשים לחדר הצ'אט.",
|
||||
"cant-remove-users-from-chat-room": "לא ניתן להסיר משתמשים מחדר הצ'אט.",
|
||||
"chat-room-name-too-long": "שם החדר ארוך מדי. השם לא יכול להיות ארוך מ-%1 תווים.",
|
||||
"remote-chat-received-too-long": "קיבלת הודעת צ'אט מ %1, אבל זה היה ארוך מדי ונדחה.",
|
||||
"remote-chat-received-too-long": "התקבלה הודעת צ'אט מ %1, אבל זה היה ארוך מדי ונדחה.",
|
||||
"already-voting-for-this-post": "הצבעתם כבר בנושא זה.",
|
||||
"reputation-system-disabled": "מערכת המוניטין לא פעילה.",
|
||||
"downvoting-disabled": "היכולת להצביע נגד מושבתת",
|
||||
"not-enough-reputation-to-chat": "נדרש %1 מוניטין כדי לכתוב בצ'אט",
|
||||
"not-enough-reputation-to-upvote": "נדרש %1 מוניטין כדי להצביע בעד",
|
||||
"not-enough-reputation-to-downvote": "נדרש %1 מוניטין כדי להצביע למטה",
|
||||
"not-enough-reputation-to-post-links": "אתה צריך %1 מוניטין כדי לפרסם קישורים",
|
||||
"not-enough-reputation-to-post-links": "נדרש %1 מוניטין כדי לפרסם קישורים",
|
||||
"not-enough-reputation-to-flag": "נדרש %1 מוניטין כדי לדווח על פוסט",
|
||||
"not-enough-reputation-min-rep-website": "נרדש %1 מוניטין כדי להוסיף אתר אינטרנט",
|
||||
"not-enough-reputation-min-rep-aboutme": "נדרש %1 מוניטין כדי להוסיף תיאור",
|
||||
"not-enough-reputation-min-rep-signature": "נדרש %1 מוניטין כדי להוסיף חתימה",
|
||||
"not-enough-reputation-min-rep-profile-picture": "נדרש %1 מוניטין כדי להוסיף תמונת פרופיל",
|
||||
"not-enough-reputation-min-rep-cover-picture": "נדרש %1 מוניטין כדי להוסיף תמונת רקע לפרופיל",
|
||||
"not-enough-reputation-custom-field": "אתה צריך %1 מוניטין עבור %2",
|
||||
"not-enough-reputation-custom-field": "נדרש %1 מוניטין עבור %2",
|
||||
"custom-user-field-value-too-long": "ערך שדה מותאם אישית ארוך מדי, %1",
|
||||
"custom-user-field-select-value-invalid": "האפשרות שנבחרה בשדה מותאם אישית אינה חוקית, %1",
|
||||
"custom-user-field-invalid-text": "טקסט שדה מותאם אישית אינו חוקי, %1",
|
||||
@@ -201,12 +201,12 @@
|
||||
"too-many-user-flags-per-day": "ניתן לדווח רק על %1 משתמשים כל יום",
|
||||
"cant-flag-privileged": "לא ניתן לדווח על מנהלים או על תוכן שנכתב על ידי מנהלים.",
|
||||
"cant-locate-flag-report": "לא ניתן לאתר דוח דיווח",
|
||||
"self-vote": "אי אפשר להצביע על פוסט שיצרתם",
|
||||
"too-many-upvotes-today": "ביכולתכם להצביע בעד רק %1 פעמים ביום",
|
||||
"too-many-upvotes-today-user": "ביכולתכם להצביע בעד משתמש מסוים רק %1 פעמים ביום",
|
||||
"too-many-downvotes-today": "ביכולתכם להצביע נגד %1 פעמים ביום",
|
||||
"too-many-downvotes-today-user": "ביכולתכם להצביע נגד משתמש %1 פעמים ביום",
|
||||
"reload-failed": "אירעה תקלה ב NodeBB בזמן הטעינה של: \"%1\". המערכת תמשיך להגיש דפים קיימים, אבל כדאי שתשחזרו את הפעולות שלכם מהפעם האחרונה בה המערכת עבדה כראוי.",
|
||||
"self-vote": "לא ניתן להצביע לפוסט שלך",
|
||||
"too-many-upvotes-today": "ניתן להצביע בעד רק %1 פעמים ביום",
|
||||
"too-many-upvotes-today-user": "ניתן להצביע בעד משתמש מסוים רק %1 פעמים ביום",
|
||||
"too-many-downvotes-today": "ניתן להצביע נגד %1 פעמים ביום",
|
||||
"too-many-downvotes-today-user": "ניתן להצביע נגד משתמש %1 פעמים ביום",
|
||||
"reload-failed": "אירעה תקלה ב-NodeBB בזמן הטעינה של: \"%1\". המערכת תמשיך להציג דפים קיימים, אבל כדאי שתשחזרו את הפעולות שלכם מהפעם האחרונה בה המערכת עבדה כראוי.",
|
||||
"registration-error": "שגיאה בהרשמה",
|
||||
"parse-error": "אירעה שגיאה בעת ניתוח תגובת השרת",
|
||||
"wrong-login-type-email": "אנא השתמשו בכתובת המייל שלכם להתחברות",
|
||||
@@ -225,10 +225,10 @@
|
||||
"session-mismatch": "סשן לא תואם",
|
||||
"session-mismatch-text": "נראה שסשן ההתחברות שלכם אינו תואם לשרת. אנא רעננו את הדף.",
|
||||
"no-topics-selected": "לא נבחרו נושאים!",
|
||||
"cant-move-to-same-topic": "אינכם יכולים להעביר פוסט לאותו נושא!",
|
||||
"cant-move-topic-to-same-category": "אינכם יכולים להעביר נושא לאותה קטגוריה!",
|
||||
"cannot-block-self": "אינכם יכולים לחסום את עצמכם!",
|
||||
"cannot-block-privileged": "אינך יכול לחסום מנהלים או מנחים גלובליים",
|
||||
"cant-move-to-same-topic": "לא ניתן להעביר פוסט לאותו נושא!",
|
||||
"cant-move-topic-to-same-category": "לא ניתן להעביר נושא לאותה קטגוריה!",
|
||||
"cannot-block-self": "לא ניתן לחסום את עצמך!",
|
||||
"cannot-block-privileged": "לא ניתן לחסום מנהלים או מנחים גלובליים",
|
||||
"cannot-block-guest": "אורחים אינם יכולים לחסום משתמשים אחרים",
|
||||
"already-blocked": "המשתמש חסום כבר",
|
||||
"already-unblocked": "המשתמש שוחרר כבר מהחסימה",
|
||||
@@ -237,7 +237,7 @@
|
||||
"invalid-plugin-id": "מזהה תוסף לא תקין",
|
||||
"plugin-not-whitelisted": "לא ניתן להתקין את התוסף – ניתן להתקין דרך הניהול רק תוספים שנמצאים ברשימה הלבנה של מנהל החבילות של NodeBB.",
|
||||
"plugin-installation-via-acp-disabled": "התקנת תוסף באמצעות ACP מושבתת",
|
||||
"plugins-set-in-configuration": "אינך רשאי לשנות את מצב הפלאגין כפי שהם מוגדרים בזמן ריצה (config.json, משתני סביבה או ארגומנטים של מסוף), אנא שנה את התצורה במקום זאת.",
|
||||
"plugins-set-in-configuration": "לא ניתן לשנות את מצב התוסף כפי שהם מוגדרים בזמן ריצה (config.json, משתני סביבה או ארגומנטים של מסוף), שנו את התצורה במקום זאת.",
|
||||
"theme-not-set-in-configuration": "כאשר מגדירים תוספים פעילים בתצורה, שינוי ערכות נושא מחייב הוספת ערכת הנושא החדשה לרשימת התוספים הפעילים לפני עדכון שלו ב-ACP",
|
||||
"topic-event-unrecognized": "אירוע הנושא '%1' לא מזוהה",
|
||||
"category.handle-taken": "קישור הקטגוריה כבר נלקחה, אנא בחרו אחרת.",
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"downvoted": "הוצבע נגד",
|
||||
"views": "צפיות",
|
||||
"posters": "כותבים",
|
||||
"watching": "Watching",
|
||||
"watching": "עוקבים",
|
||||
"reputation": "מוניטין",
|
||||
"lastpost": "פוסט אחרון",
|
||||
"firstpost": "פוסט ראשון",
|
||||
|
||||
@@ -39,28 +39,28 @@
|
||||
"details.description": "תיאור",
|
||||
"details.member-post-cids": "מזהי קטגוריות להצגת פוסטים מהם",
|
||||
"details.badge-preview": "תצוגה מקדימה של התג",
|
||||
"details.change-icon": "שנה אייקון",
|
||||
"details.change-label-colour": "שנה צבע תווית",
|
||||
"details.change-text-colour": "שנה צבע טקסט",
|
||||
"details.change-icon": "שינוי אייקון",
|
||||
"details.change-label-colour": "שינוי צבע תווית",
|
||||
"details.change-text-colour": "שינוי צבע טקסט",
|
||||
"details.badge-text": "טקסט תגית",
|
||||
"details.userTitleEnabled": "הצג תגית",
|
||||
"details.userTitleEnabled": "הצגת תגית",
|
||||
"details.private-help": "אם אפשרות זו מופעלת, הצטרפות לקבוצות ידרוש אישור מבעל הקבוצה.",
|
||||
"details.hidden": "מוסתר",
|
||||
"details.hidden-help": "אם אפשרות זו מופעלת, קבוצה זו לא תימצא ברשימת הקבוצות, יהיה ניתן להזמין משתמשים רק באופן ידני",
|
||||
"details.delete-group": "מחק קבוצה",
|
||||
"details.delete-group": "מחיקת קבוצה",
|
||||
"details.private-system-help": "קבוצות פרטיות מושבתות ברמת המערכת, אפשרות זו אינה עושה דבר",
|
||||
"event.updated": "פרטי הקבוצה עודכנו",
|
||||
"event.deleted": "קבוצת \"%1\" נמחקה",
|
||||
"membership.accept-invitation": "קבל הזמנה",
|
||||
"membership.accept.notification-title": "אתה עכשיו חבר ב<strong>%1</strong>",
|
||||
"membership.accept-invitation": "קבלת הזמנה",
|
||||
"membership.accept.notification-title": "מעכשיו הנך חבר ב<strong>%1</strong>",
|
||||
"membership.invitation-pending": "הזמנה ממתינה",
|
||||
"membership.join-group": "הצטרפו לקבוצה",
|
||||
"membership.leave-group": "עזבו קבוצה",
|
||||
"membership.join-group": "הצטרפות לקבוצה",
|
||||
"membership.leave-group": "עזיבת קבוצה",
|
||||
"membership.leave.notification-title": "<strong>%1</strong> עזב את קבוצת <strong>%2</strong>",
|
||||
"membership.reject": "דחייה",
|
||||
"new-group.group-name": "שם קבוצה",
|
||||
"upload-group-cover": "העלאת תמונת נושא לקבוצה",
|
||||
"bulk-invite-instructions": "הזינו רשימה מופרדת בפסיק של משתמשים אותם תרצו להזמין לקבוצה זו.",
|
||||
"bulk-invite": "הזמנת מספר משתמשים",
|
||||
"remove-group-cover-confirm": "האם להסיר תמונת נושא?"
|
||||
"remove-group-cover-confirm": "להסיר תמונת נושא?"
|
||||
}
|
||||
@@ -63,7 +63,7 @@
|
||||
"time-older-than-15552000": "זמן: ישן מחצי שנה",
|
||||
"time-newer-than-31104000": "זמן: חדש משנה",
|
||||
"time-older-than-31104000": "זמן: ישן משנה",
|
||||
"sort-by": "סדר על-פי",
|
||||
"sort-by": "מיון לפי",
|
||||
"sort": "מיון",
|
||||
"last-reply-time": "תאריך תגובה אחרון",
|
||||
"topic-title": "כותרת הנושא",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"settings.stickyToolbar": "הצמד את סרגל הכלים בעת גלילה",
|
||||
"settings.stickyToolbar.help": "סרגל הכלים בדפי נושאים וקטגוריות ייצמד לראש העמוד בעת גלילה",
|
||||
"settings.topicSidebarTools": "כלי סרגל הצד",
|
||||
"settings.topicSidebarTools.help": "אפשרות זו תעביר את כלי הנושא לסרגל הצד במחשבים שולחניים",
|
||||
"settings.topicSidebarTools.help": "אפשרות זו תעביר את כלי הנושא לסרגל הצד במחשבים",
|
||||
"settings.autohideBottombar": "הסתרה אוטומטי של סרגל ניווט בנייד",
|
||||
"settings.autohideBottombar.help": "הסרגל בתצוגת הנייד יוסתר כאשר הדף ייגלל מטה",
|
||||
"settings.topMobilebar": "העברת סרגל הניווט בנייד לראש הדף",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"uploading-file": "מעלה את הקובץ...",
|
||||
"select-file-to-upload": "בחר קובץ להעלאה!",
|
||||
"select-file-to-upload": "בחרו קובץ להעלאה!",
|
||||
"upload-success": "הקובץ הועלה בהצלחה!",
|
||||
"maximum-file-size": "מקסימום %1 קילובייט",
|
||||
"no-uploads-found": "לא נמצאו העלאות!",
|
||||
"public-uploads-info": "העלאות הינם ציבוריות. כל מי שיש ברשותו לינק לקובץ יוכל לראות אותו.",
|
||||
"public-uploads-info": "העלאות הינם ציבוריות. כל מי שיש ברשותו קישור לקובץ יוכל לראות אותו.",
|
||||
"private-uploads-info": "העלאות הינם פרטיות. רק משתמשים מחוברים יוכלו לראותם."
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
"account-info": "פרטי חשבון",
|
||||
"admin-actions-label": "פעולות ניהול",
|
||||
"ban-account": "הרחקת חשבון",
|
||||
"ban-account-confirm": "האם אתהם בטוחים שאתם רוצים להרחיק משתמש זה?",
|
||||
"ban-account-confirm": "האם להרחיק משתמש זה?",
|
||||
"unban-account": "ביטול הרחקת חשבון",
|
||||
"mute-account": "השתקת חשבון",
|
||||
"unmute-account": "ביטול השתקת חשבון",
|
||||
@@ -105,10 +105,10 @@
|
||||
"show-email": "הצגת כתובת האימייל שלי",
|
||||
"show-fullname": "הצגת שמי המלא",
|
||||
"restrict-chats": "אפשר רק הודעות צ'אט ממשתמשים שאני עוקב אחריהם",
|
||||
"disable-incoming-chats": "Disable incoming chat messages <a data-bs-toggle=\"tooltip\" href=\"#\" title=\"Admins and moderators can still send you messages\"><i class=\"fa-solid fa-circle-info\"></i></a>",
|
||||
"chat-allow-list": "Allow chat messages from the following users",
|
||||
"chat-deny-list": "Deny chat messages from the following users",
|
||||
"chat-list-add-user": "Add user",
|
||||
"disable-incoming-chats": "השבתת הודעות צ'אט נכנסות <a data-bs-toggle=\"tooltip\" href=\"#\" title=\"מנהלים עדיין יוכלו לשלוח לך הודעות\"><i class=\"fa-solid fa-circle-info\"></i></a>",
|
||||
"chat-allow-list": "אפשור הודעות צ'אט מהמשתמשים הבאים",
|
||||
"chat-deny-list": "דחיית הודעות צ'אט מהמשתמשים הבאים",
|
||||
"chat-list-add-user": "הוספת משתמש",
|
||||
"digest-label": "הרשמה לקבלת תקציר",
|
||||
"digest-description": "הרשמה לקבלת עדכונים בדואר אלקטרוני מפורום זה (הודעות ונושאים חדשים) בהתאם ללוח זמנים מוגדר מראש",
|
||||
"digest-off": "כבוי",
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
"onboard.why": "יש הרבה דברים שקורים מחוץ לפורום הזה, ולא כל זה רלוונטי לתחומי העניין שלכם. לכן מעקב אחר אנשים הוא הדרך הטובה ביותר לאותת שאתם רוצים לראות יותר ממישהו אחר.",
|
||||
"onboard.how": "בינתיים, תוכלו ללחוץ על כפתורי הקיצור בחלק העליון כדי לראות על מה עוד הפורום הזה יודע, ולהתחיל לגלות תוכן חדש!",
|
||||
|
||||
"show-categories": "Show categories",
|
||||
"hide-categories": "Hide categories"
|
||||
"show-categories": "הצגת קטגוריות",
|
||||
"hide-categories": "הסתרת קטגוריות"
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"new-topic-button": "Nuova Discussione",
|
||||
"guest-login-post": "Accedi per postare",
|
||||
"no-topics": "<strong>Non ci sono discussioni in questa categoria.</strong><br />Perché non ne posti una?",
|
||||
"no-followers": "Nobody on this website is tracking or watching this category. Track or watch this category in order to begin receiving updates.",
|
||||
"no-followers": "Nessuno su questo sito web sta monitorando o guardando questa categoria. Tieni traccia o guarda questa categoria per iniziare a ricevere aggiornamenti.",
|
||||
"browsing": "navigazione",
|
||||
"no-replies": "Nessuno ha risposto",
|
||||
"no-new-posts": "Nessun nuovo post.",
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
"onboard.why": "Ci sono molte cose che accadono al di fuori di questo forum e non tutte sono rilevanti per i tuoi interessi. Ecco perché seguire le persone è il modo migliore per segnalare che vuoi vedere di più da qualcuno.",
|
||||
"onboard.how": "Nel frattempo, puoi cliccare sui pulsanti di scelta rapida in alto per vedere cos'altro conosce questo forum e iniziare a scoprire nuovi contenuti!",
|
||||
|
||||
"show-categories": "Show categories",
|
||||
"hide-categories": "Hide categories"
|
||||
"show-categories": "Mostra categorie",
|
||||
"hide-categories": "Nascondi categorie"
|
||||
}
|
||||
@@ -3,5 +3,5 @@
|
||||
"control-panel": "Kontrollpanel for logg",
|
||||
"reload": "Last inn logg på nytt",
|
||||
"clear": "Tøm logg",
|
||||
"clear-success": "Logg er tømt!"
|
||||
"clear-success": "Logg er tømt"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"forgot-password": "Glemt passord?",
|
||||
"alternative-logins": "Alternativ innlogging",
|
||||
"failed-login-attempt": "Innlogging mislyktes",
|
||||
"login-successful": "Du har blitt logget inn!",
|
||||
"login-successful": "Du er innlogget!",
|
||||
"dont-have-account": "Har du ikke en konto?",
|
||||
"logged-out-due-to-inactivity": "Du har blitt logget ut av administratorsidene fordi du har vært inaktiv for lenge",
|
||||
"caps-lock-enabled": "Caps Lock er skrudd på"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type-to-search": "Skriv for å søke",
|
||||
"results-matching": "%1 resultat(er) samsvarer med \"%2\", (%3 sekunder)",
|
||||
"no-matches": "Ingen treff funnet",
|
||||
"no-matches": "Ingen treff",
|
||||
"advanced-search": "Avansert søk",
|
||||
"in": "I",
|
||||
"in-titles": "I titler",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"success": "Suksess",
|
||||
"topic-post": "Du har nå publisert.",
|
||||
"post-queued": "Innlegget ditt er satt i kø for godkjenning. Du vil få en melding når den har blitt godkjent eller avvist.",
|
||||
"post-queued": "Innlegget ditt er satt i kø for godkjenning. Du vil få en melding når det har blitt godkjent eller avvist.",
|
||||
"authentication-successful": "Innlogging vellykket!",
|
||||
"settings-saved": "Innstillinger lagret!"
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"topic": "Emne",
|
||||
"topic": "Innlegg",
|
||||
"title": "Tittel",
|
||||
"no-topics-found": "Ingen tråder funnet!",
|
||||
"no-posts-found": "Ingen innlegg funnet!",
|
||||
"post-is-deleted": "Dette innlegget er slettet!",
|
||||
"topic-is-deleted": "Denne tråden er slettet!",
|
||||
"no-topics-found": "Ingen innlegg funnet!",
|
||||
"no-posts-found": "Ingen poster funnet!",
|
||||
"post-is-deleted": "Dette svaret er slettet!",
|
||||
"topic-is-deleted": "Dette innlegget er slettet!",
|
||||
"profile": "Profil",
|
||||
"posted-by": "Opprettet av %1",
|
||||
"posted-by-guest": "Opprettet av Gjest",
|
||||
@@ -16,7 +16,7 @@
|
||||
"one-reply-to-this-post": "1 svar",
|
||||
"last-reply-time": "Siste svar",
|
||||
"reply-options": "Alternativer for svar",
|
||||
"reply-as-topic": "Svar som tråd",
|
||||
"reply-as-topic": "Svar som innlegg",
|
||||
"guest-login-reply": "Logg inn for å besvare",
|
||||
"login-to-view": "🔒 Logg inn for å se",
|
||||
"edit": "Endre",
|
||||
@@ -58,7 +58,7 @@
|
||||
"user-deleted-topic-ago": "%1 slett dette emnet %2",
|
||||
"user-deleted-topic-on": "%1 slett dette emnet på %2",
|
||||
"user-restored-topic-ago": "%1 gjenopprettet dette emnet %2",
|
||||
"user-restored-topic-on": "%1 gjenopprettet dette menet på %2",
|
||||
"user-restored-topic-on": "%1 gjenopprettet dette emnet på %2",
|
||||
"user-moved-topic-from-ago": "%1 flyttet dette emnet fra %2 %3",
|
||||
"user-moved-topic-from-on": "%1 flyttet dette emnet fra %2 på %3",
|
||||
"user-shared-topic-ago": "%1 delte dette emnet %2",
|
||||
@@ -66,21 +66,21 @@
|
||||
"user-queued-post-ago": "%1 <a href=\"%2\">i kø</a> post til godkjenning %3",
|
||||
"user-queued-post-on": "%1 <a href=\"%2\">i kø</a>post til godkjenning %3",
|
||||
"user-referenced-topic-ago": "%1 <a href=\"%2\">refererte</a> dette emnet %3",
|
||||
"user-referenced-topic-on": "%1 <a href=\"%2\">refererte dette emnet</a> dette emnet på %3",
|
||||
"user-referenced-topic-on": "%1 <a href=\"%2\">refererte dette innlegget</a> dette innlegget på %3",
|
||||
"user-forked-topic-ago": "%1 <a href=\"%2\">gaflet</a> dette emnet %3",
|
||||
"user-forked-topic-on": "%1 <a href=\"%2\">gaflet</a> dette emnet på %3",
|
||||
"bookmark-instructions": "Klikk her for å gå tilbake til det siste innlegget i denne tråden.",
|
||||
"flag-post": "Flagg denne posten",
|
||||
"flag-user": "Flagg denne brukeren",
|
||||
"already-flagged": "Allerede flagget",
|
||||
"view-flag-report": "Vis flaggrapport",
|
||||
"resolve-flag": "Løs flagg",
|
||||
"bookmark-instructions": "Klikk her for å gå tilbake til det siste svaret i denne tråden.",
|
||||
"flag-post": "Rapporter denne posten",
|
||||
"flag-user": "Rapporter denne brukeren",
|
||||
"already-flagged": "Allerede rapportert",
|
||||
"view-flag-report": "Vis rapporteringsoversikt",
|
||||
"resolve-flag": "Behandle rapport",
|
||||
"merged-message": "Dette emnet er slått sammen med <a href=\"%1\">%2</a>",
|
||||
"forked-message": "This topic was forked from <a href=\"%1\">%2</a>",
|
||||
"deleted-message": "Denne tråden har blitt slettet. Bare brukere med trådhåndterings-privilegier kan se den.",
|
||||
"following-topic.message": "Du vil nå motta varsler når noen skriver i denne tråden.",
|
||||
"not-following-topic.message": "Du vil se denne tråden i trådlisten, men du vil ikke motta varslinger når noen skriver i den.",
|
||||
"ignoring-topic.message": "Du vil ikke lenger se denne tråden blant de uleste trådene. Du vil få et varsel når du blir nevnt eller din tråd blir tilrådd.",
|
||||
"deleted-message": "Denne innlegget har blitt slettet. ",
|
||||
"following-topic.message": "Du vil nå motta varsler når noen svarer på dette innlegget.",
|
||||
"not-following-topic.message": "Du vil se dette innlegget i oversikten over uleste innlegg, men du vil ikke motta varslinger når noen skriver et svar.",
|
||||
"ignoring-topic.message": "Du vil ikke lenger se dette innlegget blant uleste innlegg. Du vil få et varsel når du blir nevnt eller innlegget blir anbefalt.",
|
||||
"login-to-subscribe": "Vennligst registrer deg eller logg inn for å abonnere på denne tråden.",
|
||||
"markAsUnreadForAll.success": "Tråd markert som ulest for alle.",
|
||||
"mark-unread": "Merk som ulest",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"pwa": "Progressiv webapp",
|
||||
"touch-icon": "Touch-ikon",
|
||||
"touch-icon.upload": "Last opp touch-ikon",
|
||||
"touch-icon.help": "Bruk touch-ikonet for å visast på mobile einingar.",
|
||||
"touch-icon.help": "Bruk touch-ikonet for å vise på mobile einingar.",
|
||||
"maskable-icon": "Maskerbart ikon",
|
||||
"maskable-icon.help": "Maskerbare ikon vert brukt for å tilpasse webappen til ulike skjermstorleikar.",
|
||||
"outgoing-links": "Utgåande lenkjer",
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
"restrictions.seconds-edit-after-new": "Sekund til redigering for nye brukarar",
|
||||
"restrictions.milliseconds-between-messages": "Millisekund mellom meldingar",
|
||||
"restrictions.groups-exempt-from-new-user-restrictions": "Grupper unnateke frå nye brukarrestriksjonar",
|
||||
"guest-settings": "Gjestinnstillingar",
|
||||
"guest-settings": "Gjesteinnstillingar",
|
||||
"handles.enabled": "Aktiver handtering av gjestar",
|
||||
"handles.enabled-help": "Aktiver for å la gjestar kunne sjå og interagere med forumet innanfor visse grenser.",
|
||||
"topic-views.enabled": "Aktiver visning av emne for gjestar",
|
||||
|
||||
@@ -188,7 +188,7 @@
|
||||
"not-enough-reputation-custom-field": "Du treng %1 i omdømme for å %2",
|
||||
"custom-user-field-value-too-long": "Tilpassa felt er er for langt, %1",
|
||||
"custom-user-field-select-value-invalid": "Alternativet i tilpassa felt er ugyldig, %1 ",
|
||||
"custom-user-field-invalid-text": "Tilpassa tekst felt er ugyldig, %1",
|
||||
"custom-user-field-invalid-text": "Tilpassa tekstfelt er ugyldig, %1",
|
||||
"custom-user-field-invalid-link": "Tilpassa lenke felt er ugyldig, %1",
|
||||
"custom-user-field-invalid-number": "Tilpassa felt for tal er ugyldig, %1",
|
||||
"custom-user-field-invalid-date": "Tilpassa felt for dato er ugyldig, %1",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"new-topic-button": "Nowy temat",
|
||||
"guest-login-post": "Zaloguj się, aby napisać post",
|
||||
"no-topics": "<strong>W tej kategorii nie ma jeszcze żadnych tematów.</strong><br />Może pora na napisanie pierwszego?",
|
||||
"no-followers": "Nobody on this website is tracking or watching this category. Track or watch this category in order to begin receiving updates.",
|
||||
"no-followers": "Nikt na tej witrynie nie śledzi ani nie obserwuje tej kategorii. Zacznij śledzić lub obserwować tę kategorię aby zacząć odbierać aktualizacje.",
|
||||
"browsing": "przegląda",
|
||||
"no-replies": "Nikt jeszcze nie odpowiedział",
|
||||
"no-new-posts": "Brak nowych postów.",
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
"onboard.why": "Mnóstwo rzeczy dzieje się poza tym forum ale niekoniecznie zgodnych z Twoimi zainteresowaniami. Dlatego śledzenie konkretnych użytkowników jest dobrą metodą aby uzyskać więcej zawartości przez nich dodawanych.",
|
||||
"onboard.how": "W międzyczasie możesz użyć przycisków skrótów na górze aby przekonać się jak forum jest powiązane a okaże się, że zapewnia wiele dodatkowych treści!",
|
||||
|
||||
"show-categories": "Show categories",
|
||||
"hide-categories": "Hide categories"
|
||||
"show-categories": "Pokaż kategorie",
|
||||
"hide-categories": "Ukryj kategorie"
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"new-topic-button": "Chủ Đề Mới",
|
||||
"guest-login-post": "Đăng nhập để đăng bài",
|
||||
"no-topics": "<strong>Không có chủ đề nào trong danh mục này.</strong><br />Sao bạn không thử đăng?",
|
||||
"no-followers": "Nobody on this website is tracking or watching this category. Track or watch this category in order to begin receiving updates.",
|
||||
"no-followers": "Không ai trên trang web này đang theo dõi hoặc xem danh mục này. Theo dõi hoặc xem danh mục này để bắt đầu nhận cập nhật.",
|
||||
"browsing": "lướt xem",
|
||||
"no-replies": "Không có ai trả lời",
|
||||
"no-new-posts": "Không bài đăng mới.",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"filter-assignee": "Ủy nhiệm",
|
||||
"filter-cid": "Chuyên mục",
|
||||
"filter-quick-mine": "Được giao cho tôi",
|
||||
"filter-cid-all": "Tất cả chuyên mục",
|
||||
"filter-cid-all": "Tất cả các danh mục",
|
||||
"apply-filters": "Áp Dụng Bộ Lọc",
|
||||
"more-filters": "Thêm Bộ Lọc",
|
||||
"fewer-filters": "Bớt Bộ Lọc",
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
"onboard.why": "Có rất nhiều điều diễn ra bên ngoài diễn đàn này và không phải tất cả đều phù hợp với sở thích của bạn. Đó là lý do tại sao theo dõi mọi người là cách tốt nhất để báo hiệu rằng bạn muốn biết thêm thông tin từ ai đó.",
|
||||
"onboard.how": "Trong thời gian chờ đợi, bạn có thể nhấp vào các nút tắt ở trên cùng để xem diễn đàn này biết thêm những gì và bắt đầu khám phá một số nội dung mới!",
|
||||
|
||||
"show-categories": "Show categories",
|
||||
"hide-categories": "Hide categories"
|
||||
"show-categories": "Hiển thị các danh mục",
|
||||
"hide-categories": "Ẩn các danh mục"
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"new-topic-button": "发表主题",
|
||||
"guest-login-post": "登录以发布",
|
||||
"no-topics": "<strong>此版块还没有任何内容。</strong><br />赶紧来发帖吧!",
|
||||
"no-followers": "Nobody on this website is tracking or watching this category. Track or watch this category in order to begin receiving updates.",
|
||||
"no-followers": "本网站无人跟踪或关注此版块。跟踪或关注此版块,以便开始接收更新。",
|
||||
"browsing": "正在浏览",
|
||||
"no-replies": "尚无回复",
|
||||
"no-new-posts": "没有新主题",
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
"onboard.why": "论坛之外的事情很多,而且并非所有事情都与您的兴趣相关。因此,关注他人是表明您想从某人那里了解更多信息的最佳方式。",
|
||||
"onboard.how": "在此期间,您可以点击顶部的快捷按钮,了解本论坛的其他内容,并开始发现一些新内容!",
|
||||
|
||||
"show-categories": "Show categories",
|
||||
"hide-categories": "Hide categories"
|
||||
"show-categories": "显示版块",
|
||||
"hide-categories": "隐藏版块"
|
||||
}
|
||||
@@ -180,6 +180,8 @@ PostDataObject:
|
||||
type: number
|
||||
downvotes:
|
||||
type: number
|
||||
announces:
|
||||
type: number
|
||||
bookmarks:
|
||||
type: number
|
||||
deleterUid:
|
||||
|
||||
@@ -21,19 +21,6 @@ get:
|
||||
privileges:
|
||||
type: object
|
||||
properties:
|
||||
labels:
|
||||
type: object
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Language key of the privilege name's user-friendly name
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Language key of the privilege name's user-friendly name
|
||||
labelData:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -31,19 +31,6 @@ put:
|
||||
response:
|
||||
type: object
|
||||
properties:
|
||||
labels:
|
||||
type: object
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Language key of the privilege name's user-friendly name
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Language key of the privilege name's user-friendly name
|
||||
labelData:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -24,19 +24,6 @@ get:
|
||||
response:
|
||||
type: object
|
||||
properties:
|
||||
labels:
|
||||
type: object
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Language key of the privilege name's user-friendly name
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Language key of the privilege name's user-friendly name
|
||||
labelData:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -41,19 +41,6 @@ put:
|
||||
response:
|
||||
type: object
|
||||
properties:
|
||||
labels:
|
||||
type: object
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Language key of the privilege name's user-friendly name
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Language key of the privilege name's user-friendly name
|
||||
labelData:
|
||||
type: array
|
||||
items:
|
||||
@@ -191,19 +178,6 @@ delete:
|
||||
response:
|
||||
type: object
|
||||
properties:
|
||||
labels:
|
||||
type: object
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Language key of the privilege name's user-friendly name
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Language key of the privilege name's user-friendly name
|
||||
labelData:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -397,7 +397,7 @@ define('forum/topic/postTools', [
|
||||
return;
|
||||
}
|
||||
const post = button.parents('[data-pid]');
|
||||
if (post.length) {
|
||||
if (post.length && !post.hasClass('self-post')) {
|
||||
require(['slugify'], function (slugify) {
|
||||
slug = slugify(post.attr('data-username'), true);
|
||||
if (!slug) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const nconf = require('nconf');
|
||||
|
||||
const posts = require('../posts');
|
||||
const utils = require('../utils');
|
||||
|
||||
const activitypub = module.parent.exports;
|
||||
const Feps = module.exports;
|
||||
@@ -13,7 +14,7 @@ Feps.announce = async function announce(id, activity) {
|
||||
({ id: localId } = await activitypub.helpers.resolveLocalId(id));
|
||||
}
|
||||
const cid = await posts.getCidByPid(localId || id);
|
||||
if (cid === -1) {
|
||||
if (cid === -1 || !utils.isNumber(cid)) { // local cids only
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -452,11 +452,13 @@ Helpers.makeSet = (object, properties) => new Set(properties.reduce((memo, prope
|
||||
[object[property]] :
|
||||
[]), []));
|
||||
|
||||
Helpers.generateCollection = async ({ set, method, page, perPage, url }) => {
|
||||
Helpers.generateCollection = async ({ set, method, count, page, perPage, url }) => {
|
||||
if (!method) {
|
||||
method = db.getSortedSetRange;
|
||||
method = db.getSortedSetRange.bind(null, set);
|
||||
} else if (set) {
|
||||
method = method.bind(null, set);
|
||||
}
|
||||
const count = await db.sortedSetCard(set);
|
||||
count = count || await db.sortedSetCard(set);
|
||||
const pageCount = Math.max(1, Math.ceil(count / perPage));
|
||||
let items = [];
|
||||
let paginate = true;
|
||||
@@ -474,7 +476,7 @@ Helpers.generateCollection = async ({ set, method, page, perPage, url }) => {
|
||||
|
||||
const start = Math.max(0, ((page - 1) * perPage) - 1);
|
||||
const stop = Math.max(0, start + perPage - 1);
|
||||
items = await method(set, start, stop);
|
||||
items = await method.call(null, start, stop);
|
||||
}
|
||||
|
||||
const object = {
|
||||
@@ -490,8 +492,6 @@ Helpers.generateCollection = async ({ set, method, page, perPage, url }) => {
|
||||
object.next = page < pageCount ? `${url}?page=${page + 1}` : null;
|
||||
object.prev = page > 1 ? `${url}?page=${page - 1}` : null;
|
||||
}
|
||||
} else {
|
||||
object.orderedItems = [];
|
||||
}
|
||||
|
||||
if (paginate) {
|
||||
|
||||
@@ -41,6 +41,7 @@ inbox.create = async (req) => {
|
||||
return await activitypub.notes.assertPrivate(object);
|
||||
}
|
||||
|
||||
// Category sync, remove when cross-posting available
|
||||
const { cids } = await activitypub.actors.getLocalFollowers(actor);
|
||||
let cid = null;
|
||||
if (cids.size > 0) {
|
||||
@@ -49,7 +50,7 @@ inbox.create = async (req) => {
|
||||
|
||||
const asserted = await activitypub.notes.assert(0, object, { cid });
|
||||
if (asserted) {
|
||||
activitypub.feps.announce(object.id, req.body);
|
||||
await activitypub.feps.announce(object.id, req.body);
|
||||
// api.activitypub.add(req, { pid: object.id });
|
||||
}
|
||||
};
|
||||
@@ -84,8 +85,8 @@ inbox.update = async (req) => {
|
||||
throw new Error('[[error:activitypub.origin-mismatch]]');
|
||||
}
|
||||
|
||||
switch (object.type) {
|
||||
case 'Note': {
|
||||
switch (true) {
|
||||
case activitypub._constants.acceptedPostTypes.includes(object.type): {
|
||||
const [isNote, isMessage] = await Promise.all([
|
||||
posts.exists(object.id),
|
||||
messaging.messageExists(object.id),
|
||||
@@ -95,6 +96,7 @@ inbox.update = async (req) => {
|
||||
switch (true) {
|
||||
case isNote: {
|
||||
const postData = await activitypub.mocks.post(object);
|
||||
postData.tags = await activitypub.notes._normalizeTags(postData._activitypub.tag, postData.cid);
|
||||
await posts.edit(postData);
|
||||
const isDeleted = await posts.getPostField(object.id, 'deleted');
|
||||
if (isDeleted) {
|
||||
@@ -136,16 +138,12 @@ inbox.update = async (req) => {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Application': // falls through
|
||||
case 'Group': // falls through
|
||||
case 'Organization': // falls through
|
||||
case 'Service': // falls through
|
||||
case 'Person': {
|
||||
case activitypub._constants.acceptableActorTypes.has(object.type): {
|
||||
await activitypub.actors.assert(object.id, { update: true });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Tombstone': {
|
||||
case object.type === 'Tombstone': {
|
||||
const [isNote, isMessage/* , isActor */] = await Promise.all([
|
||||
posts.exists(object.id),
|
||||
messaging.messageExists(object.id),
|
||||
@@ -247,12 +245,12 @@ inbox.like = async (req) => {
|
||||
activitypub.helpers.log(`[activitypub/inbox/like] id ${id} via ${actor}`);
|
||||
|
||||
const result = await posts.upvote(id, actor);
|
||||
activitypub.feps.announce(object.id, req.body);
|
||||
await activitypub.feps.announce(object.id, req.body);
|
||||
socketHelpers.upvote(result, 'notifications:upvoted-your-post-in');
|
||||
};
|
||||
|
||||
inbox.announce = async (req) => {
|
||||
const { actor, object, published, to, cc } = req.body;
|
||||
let { actor, object, published, to, cc } = req.body;
|
||||
activitypub.helpers.log(`[activitypub/inbox/announce] Parsing Announce(${object.type}) from ${actor}`);
|
||||
let timestamp = new Date(published);
|
||||
timestamp = timestamp.toString() !== 'Invalid Date' ? timestamp.getTime() : Date.now();
|
||||
@@ -265,12 +263,19 @@ inbox.announce = async (req) => {
|
||||
let tid;
|
||||
let pid;
|
||||
|
||||
// Category sync, remove when cross-posting available
|
||||
const { cids } = await activitypub.actors.getLocalFollowers(actor);
|
||||
let cid = null;
|
||||
if (cids.size > 0) {
|
||||
cid = Array.from(cids)[0];
|
||||
}
|
||||
|
||||
// 1b12 announce
|
||||
const categoryActor = await categories.exists(actor);
|
||||
if (categoryActor) {
|
||||
cid = actor;
|
||||
}
|
||||
|
||||
switch(true) {
|
||||
case object.type === 'Like': {
|
||||
const id = object.object.id || object.object;
|
||||
@@ -292,6 +297,12 @@ inbox.announce = async (req) => {
|
||||
break;
|
||||
}
|
||||
|
||||
case object.type === 'Create': {
|
||||
object = object.object;
|
||||
// falls through
|
||||
}
|
||||
|
||||
// Announce(Object)
|
||||
case activitypub._constants.acceptedPostTypes.includes(object.type): {
|
||||
if (String(object.id).startsWith(nconf.get('url'))) { // Local object
|
||||
const { type, id } = await activitypub.helpers.resolveLocalId(object.id);
|
||||
@@ -315,13 +326,7 @@ inbox.announce = async (req) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle case where Announce(Create(Note-ish)) is received
|
||||
if (object.type === 'Create' && activitypub._constants.acceptedPostTypes.includes(object.object.type)) {
|
||||
pid = object.object.id;
|
||||
} else {
|
||||
pid = object.id;
|
||||
}
|
||||
|
||||
pid = object.id;
|
||||
pid = await activitypub.resolveId(0, pid); // in case wrong id is passed-in; unlikely, but still.
|
||||
if (!pid) {
|
||||
return;
|
||||
|
||||
@@ -42,7 +42,7 @@ const sanitizeConfig = {
|
||||
|
||||
Mocks._normalize = async (object) => {
|
||||
// Normalized incoming AP objects into expected types for easier mocking
|
||||
let { attributedTo, url, image, content, source } = object;
|
||||
let { type, attributedTo, url, image, content, source, attachment } = object;
|
||||
|
||||
switch (true) { // non-string attributedTo handling
|
||||
case Array.isArray(attributedTo): {
|
||||
@@ -102,6 +102,30 @@ Mocks._normalize = async (object) => {
|
||||
|
||||
if (url) { // Handle url array
|
||||
if (Array.isArray(url)) {
|
||||
// Special handling for Video type (from PeerTube specifically)
|
||||
if (type === 'Video') {
|
||||
const stream = url.reduce((memo, { type, mediaType, tag }) => {
|
||||
if (!memo) {
|
||||
if (type === 'Link' && mediaType === 'application/x-mpegURL') {
|
||||
memo = tag.reduce((memo, { type, mediaType, href }) => {
|
||||
if (!memo && (type === 'Link' && mediaType === 'video/mp4')) {
|
||||
memo = { type, mediaType, href };
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, null);
|
||||
}
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, null);
|
||||
|
||||
if (stream) {
|
||||
attachment = attachment || [];
|
||||
attachment.push(stream);
|
||||
}
|
||||
}
|
||||
|
||||
url = url.reduce((valid, cur) => {
|
||||
if (typeof cur === 'string') {
|
||||
valid.push(cur);
|
||||
@@ -126,6 +150,7 @@ Mocks._normalize = async (object) => {
|
||||
sourceContent,
|
||||
image,
|
||||
url,
|
||||
attachment,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -332,7 +357,7 @@ Mocks.post = async (objects) => {
|
||||
attributedTo: uid,
|
||||
inReplyTo: toPid,
|
||||
published, updated, name, content, sourceContent,
|
||||
type, to, cc, audience, attachment, tag, image,
|
||||
to, cc, audience, attachment, tag, image,
|
||||
} = object;
|
||||
|
||||
await activitypub.actors.assert(uid);
|
||||
@@ -346,21 +371,17 @@ Mocks.post = async (objects) => {
|
||||
let edited = new Date(updated);
|
||||
edited = Number.isNaN(edited.valueOf()) ? undefined : edited;
|
||||
|
||||
if (type === 'Video') {
|
||||
attachment = attachment || [];
|
||||
attachment.push({ url });
|
||||
}
|
||||
|
||||
const payload = {
|
||||
uid,
|
||||
pid,
|
||||
// tid, --> purposely omitted
|
||||
name,
|
||||
content,
|
||||
sourceContent,
|
||||
timestamp,
|
||||
toPid,
|
||||
|
||||
title: name, // used in post.edit
|
||||
|
||||
edited,
|
||||
editor: edited ? uid : undefined,
|
||||
_activitypub: { to, cc, audience, attachment, tag, url, image },
|
||||
|
||||
@@ -27,6 +27,24 @@ async function unlock(value) {
|
||||
await db.deleteObjectField('locks', value);
|
||||
}
|
||||
|
||||
Notes._normalizeTags = async (tag, cid) => {
|
||||
const systemTags = (meta.config.systemTags || '').split(',');
|
||||
const maxTags = await categories.getCategoryField(cid, 'maxTags');
|
||||
const tags = (tag || [])
|
||||
.map((tag) => {
|
||||
tag.name = tag.name.startsWith('#') ? tag.name.slice(1) : tag.name;
|
||||
return tag;
|
||||
})
|
||||
.filter(o => o.type === 'Hashtag' && !systemTags.includes(o.name))
|
||||
.map(t => t.name);
|
||||
|
||||
if (tags.length > maxTags) {
|
||||
tags.length = maxTags;
|
||||
}
|
||||
|
||||
return tags;
|
||||
};
|
||||
|
||||
Notes.assert = async (uid, input, options = { skipChecks: false }) => {
|
||||
/**
|
||||
* Given the id or object of any as:Note, either retrieves the full context (if resolvable),
|
||||
@@ -75,7 +93,7 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => {
|
||||
chain = chain.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
const mainPost = chain[0];
|
||||
let { pid: mainPid, tid, uid: authorId, timestamp, name, content, sourceContent, _activitypub } = mainPost;
|
||||
let { pid: mainPid, tid, uid: authorId, timestamp, title, content, sourceContent, _activitypub } = mainPost;
|
||||
const hasTid = !!tid;
|
||||
|
||||
const cid = hasTid ? await topics.getTopicField(tid, 'cid') : options.cid || -1;
|
||||
@@ -94,32 +112,25 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => {
|
||||
return { tid, count: 0 };
|
||||
}
|
||||
|
||||
let title;
|
||||
if (hasTid) {
|
||||
mainPid = await topics.getTopicField(tid, 'mainPid');
|
||||
} else {
|
||||
// Check recipients/audience for category (local or remote)
|
||||
// Check recipients/audience for local category
|
||||
const set = activitypub.helpers.makeSet(_activitypub, ['to', 'cc', 'audience']);
|
||||
await activitypub.actors.assert(Array.from(set));
|
||||
|
||||
// Local
|
||||
const resolved = await Promise.all(Array.from(set).map(async id => await activitypub.helpers.resolveLocalId(id)));
|
||||
const recipientCids = resolved
|
||||
.filter(Boolean)
|
||||
.filter(({ type }) => type === 'category')
|
||||
.map(obj => obj.id);
|
||||
|
||||
// Remote
|
||||
const assertedGroups = await db.exists(Array.from(set).map(id => `categoryRemote:${id}`));
|
||||
const remoteCid = Array.from(set).filter((_, idx) => assertedGroups[idx]).shift();
|
||||
|
||||
if (remoteCid || recipientCids.length) {
|
||||
if (recipientCids.length) {
|
||||
// Overrides passed-in value, respect addressing from main post over booster
|
||||
options.cid = remoteCid || recipientCids.shift();
|
||||
options.cid = recipientCids.shift();
|
||||
}
|
||||
|
||||
// mainPid ok to leave as-is
|
||||
title = name || activitypub.helpers.generateTitle(utils.decodeHTMLEntities(content || sourceContent));
|
||||
title = title || activitypub.helpers.generateTitle(utils.decodeHTMLEntities(content || sourceContent));
|
||||
|
||||
// Remove any custom emoji from title
|
||||
if (_activitypub && _activitypub.tag && Array.isArray(_activitypub.tag)) {
|
||||
@@ -166,22 +177,9 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => {
|
||||
const count = unprocessed.length;
|
||||
activitypub.helpers.log(`[notes/assert] ${count} new note(s) found.`);
|
||||
|
||||
let tags;
|
||||
if (!hasTid) {
|
||||
const { to, cc, attachment } = mainPost._activitypub;
|
||||
const systemTags = (meta.config.systemTags || '').split(',');
|
||||
const maxTags = await categories.getCategoryField(cid, 'maxTags');
|
||||
tags = (mainPost._activitypub.tag || [])
|
||||
.map((tag) => {
|
||||
tag.name = tag.name.startsWith('#') ? tag.name.slice(1) : tag.name;
|
||||
return tag;
|
||||
})
|
||||
.filter(o => o.type === 'Hashtag' && !systemTags.includes(o.name))
|
||||
.map(t => t.name);
|
||||
|
||||
if (tags.length > maxTags) {
|
||||
tags.length = maxTags;
|
||||
}
|
||||
const tags = await Notes._normalizeTags(mainPost._activitypub.tag || []);
|
||||
|
||||
await Promise.all([
|
||||
topics.post({
|
||||
|
||||
@@ -52,11 +52,8 @@ activitypubApi.follow = enabledCheck(async (caller, { type, id, actor } = {}) =>
|
||||
|
||||
actor = uid || cid;
|
||||
}
|
||||
const [handle, isFollowing] = await Promise.all([
|
||||
user.getUserField(actor, 'username'),
|
||||
db.isSortedSetMember(type === 'uid' ? `followingRemote:${id}` : `cid:${id}:following`, actor),
|
||||
]);
|
||||
|
||||
const isFollowing = await db.isSortedSetMember(type === 'uid' ? `followingRemote:${id}` : `cid:${id}:following`, actor);
|
||||
if (isFollowing) { // already following
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +63,7 @@ activitypubApi.follow = enabledCheck(async (caller, { type, id, actor } = {}) =>
|
||||
await db.sortedSetAdd(`followRequests:${type}.${id}`, timestamp, actor);
|
||||
try {
|
||||
await activitypub.send(type, id, [actor], {
|
||||
id: `${nconf.get('url')}/${type}/${id}#activity/follow/${handle}/${timestamp}`,
|
||||
id: `${nconf.get('url')}/${type}/${id}#activity/follow/${encodeURIComponent(actor)}/${timestamp}`,
|
||||
type: 'Follow',
|
||||
object: actor,
|
||||
});
|
||||
@@ -93,8 +90,7 @@ activitypubApi.unfollow = enabledCheck(async (caller, { type, id, actor }) => {
|
||||
actor = uid || cid;
|
||||
}
|
||||
|
||||
const [handle, isFollowing, isPending] = await Promise.all([
|
||||
user.getUserField(actor, 'username'),
|
||||
const [isFollowing, isPending] = await Promise.all([
|
||||
db.isSortedSetMember(type === 'uid' ? `followingRemote:${id}` : `cid:${id}:following`, actor),
|
||||
db.isSortedSetMember(`followRequests:${type === 'uid' ? 'uid' : 'cid'}.${id}`, actor),
|
||||
]);
|
||||
@@ -110,7 +106,7 @@ activitypubApi.unfollow = enabledCheck(async (caller, { type, id, actor }) => {
|
||||
const timestamp = timestamps[0] || timestamps[1];
|
||||
|
||||
const object = {
|
||||
id: `${nconf.get('url')}/${type}/${id}#activity/follow/${handle}/${timestamp}`,
|
||||
id: `${nconf.get('url')}/${type}/${id}#activity/follow/${encodeURIComponent(actor)}/${timestamp}`,
|
||||
type: 'Follow',
|
||||
object: actor,
|
||||
};
|
||||
@@ -121,7 +117,7 @@ activitypubApi.unfollow = enabledCheck(async (caller, { type, id, actor }) => {
|
||||
}
|
||||
|
||||
await activitypub.send(type, id, [actor], {
|
||||
id: `${nconf.get('url')}/${type}/${id}#activity/undo:follow/${handle}/${timestamp}`,
|
||||
id: `${nconf.get('url')}/${type}/${id}#activity/undo:follow/${encodeURIComponent(actor)}/${timestamp}`,
|
||||
type: 'Undo',
|
||||
object,
|
||||
});
|
||||
@@ -314,7 +310,15 @@ activitypubApi.delete.note = enabledCheck(async (caller, { pid }) => {
|
||||
activitypubApi.like = {};
|
||||
|
||||
activitypubApi.like.note = enabledCheck(async (caller, { pid }) => {
|
||||
if (!activitypub.helpers.isUri(pid)) { // remote only
|
||||
const payload = {
|
||||
id: `${nconf.get('url')}/uid/${caller.uid}#activity/like/${encodeURIComponent(pid)}`,
|
||||
type: 'Like',
|
||||
actor: `${nconf.get('url')}/uid/${caller.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;
|
||||
}
|
||||
|
||||
@@ -323,13 +327,6 @@ activitypubApi.like.note = enabledCheck(async (caller, { pid }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
id: `${nconf.get('url')}/uid/${caller.uid}#activity/like/${encodeURIComponent(pid)}`,
|
||||
type: 'Like',
|
||||
actor: `${nconf.get('url')}/uid/${caller.uid}`,
|
||||
object: pid,
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
activitypub.send('uid', caller.uid, [uid], payload),
|
||||
activitypub.feps.announce(pid, payload),
|
||||
|
||||
@@ -137,12 +137,12 @@ async function executeCommand(caller, command, eventName, notification, data) {
|
||||
}
|
||||
if (result && command === 'upvote') {
|
||||
socketHelpers.upvote(result, notification);
|
||||
api.activitypub.like.note(caller, { pid: data.pid });
|
||||
await api.activitypub.like.note(caller, { pid: 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);
|
||||
api.activitypub.undo.like(caller, { pid: data.pid });
|
||||
await api.activitypub.undo.like(caller, { pid: data.pid });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -105,7 +105,9 @@ Actors.replies = async function (req, res, next) {
|
||||
}
|
||||
|
||||
// Convert pids to urls
|
||||
replies.orderedItems = replies.orderedItems.map(pid => (utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid));
|
||||
if (replies.orderedItems) {
|
||||
replies.orderedItems = replies.orderedItems.map(pid => (utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid));
|
||||
}
|
||||
|
||||
const object = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
@@ -167,7 +169,7 @@ Actors.topic = async function (req, res, next) {
|
||||
res.set('ETag', digest);
|
||||
|
||||
// Convert pids to urls
|
||||
if (page || collection.totalItems < meta.config.postsPerPage) {
|
||||
if (page || collection.totalItems < perPage) {
|
||||
collection.orderedItems = collection.orderedItems || [];
|
||||
if (!page || page === 1) { // add OP to collection
|
||||
collection.orderedItems.unshift(mainPid);
|
||||
|
||||
@@ -6,6 +6,7 @@ const winston = require('winston');
|
||||
const meta = require('../../meta');
|
||||
const user = require('../../user');
|
||||
const activitypub = require('../../activitypub');
|
||||
const utils = require('../../utils');
|
||||
const helpers = require('../helpers');
|
||||
|
||||
const Controller = module.exports;
|
||||
@@ -60,71 +61,53 @@ Controller.fetch = async (req, res, next) => {
|
||||
Controller.getFollowing = async (req, res) => {
|
||||
const { followingCount, followingRemoteCount } = await user.getUserFields(req.params.uid, ['followingCount', 'followingRemoteCount']);
|
||||
const totalItems = parseInt(followingCount || 0, 10) + parseInt(followingRemoteCount || 0, 10);
|
||||
let orderedItems;
|
||||
let next = (totalItems && `${nconf.get('url')}/uid/${req.params.uid}/following?page=`) || null;
|
||||
|
||||
if (totalItems) {
|
||||
if (req.query.page) {
|
||||
const page = parseInt(req.query.page, 10) || 1;
|
||||
const resultsPerPage = 50;
|
||||
const start = Math.max(0, page - 1) * resultsPerPage;
|
||||
const stop = start + resultsPerPage - 1;
|
||||
const count = totalItems;
|
||||
const collection = await activitypub.helpers.generateCollection({
|
||||
method: user.getFollowing.bind(null, req.params.uid),
|
||||
count,
|
||||
perPage: 50,
|
||||
page: req.query.page,
|
||||
url: `${nconf.get('url')}/uid/${req.params.uid}/following`,
|
||||
});
|
||||
|
||||
orderedItems = await user.getFollowing(req.params.uid, start, stop);
|
||||
orderedItems = orderedItems.map(({ userslug }) => `${nconf.get('url')}/user/${userslug}`);
|
||||
if (stop < totalItems - 1) {
|
||||
next = `${next}${page + 1}`;
|
||||
} else {
|
||||
next = null;
|
||||
if (collection.hasOwnProperty('orderedItems')) {
|
||||
collection.orderedItems = collection.orderedItems.map(({ uid }) => {
|
||||
if (utils.isNumber(uid)) {
|
||||
return `${nconf.get('url')}/uid/${uid}`;
|
||||
}
|
||||
} else {
|
||||
orderedItems = [];
|
||||
next = `${next}1`;
|
||||
}
|
||||
|
||||
return uid;
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'OrderedCollection',
|
||||
totalItems,
|
||||
orderedItems,
|
||||
next,
|
||||
});
|
||||
res.status(200).json(collection);
|
||||
};
|
||||
|
||||
Controller.getFollowers = async (req, res) => {
|
||||
const { followerCount, followerRemoteCount } = await user.getUserFields(req.params.uid, ['followerCount', 'followerRemoteCount']);
|
||||
const totalItems = parseInt(followerCount || 0, 10) + parseInt(followerRemoteCount || 0, 10);
|
||||
let orderedItems = [];
|
||||
let next = (totalItems && `${nconf.get('url')}/uid/${req.params.uid}/followers?page=`) || null;
|
||||
|
||||
if (totalItems) {
|
||||
if (req.query.page) {
|
||||
const page = parseInt(req.query.page, 10) || 1;
|
||||
const resultsPerPage = 50;
|
||||
const start = Math.max(0, page - 1) * resultsPerPage;
|
||||
const stop = start + resultsPerPage - 1;
|
||||
const count = totalItems;
|
||||
const collection = await activitypub.helpers.generateCollection({
|
||||
method: user.getFollowers.bind(null, req.params.uid),
|
||||
count,
|
||||
perPage: 50,
|
||||
page: req.query.page,
|
||||
url: `${nconf.get('url')}/uid/${req.params.uid}/followers`,
|
||||
});
|
||||
|
||||
orderedItems = await user.getFollowers(req.params.uid, start, stop);
|
||||
orderedItems = orderedItems.map(({ userslug }) => `${nconf.get('url')}/user/${userslug}`);
|
||||
if (stop < totalItems - 1) {
|
||||
next = `${next}${page + 1}`;
|
||||
} else {
|
||||
next = null;
|
||||
if (collection.hasOwnProperty('orderedItems')) {
|
||||
collection.orderedItems = collection.orderedItems.map(({ uid }) => {
|
||||
if (utils.isNumber(uid)) {
|
||||
return `${nconf.get('url')}/uid/${uid}`;
|
||||
}
|
||||
} else {
|
||||
orderedItems = [];
|
||||
next = `${next}1`;
|
||||
}
|
||||
|
||||
return uid;
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'OrderedCollection',
|
||||
totalItems,
|
||||
orderedItems,
|
||||
next,
|
||||
});
|
||||
res.status(200).json(collection);
|
||||
};
|
||||
|
||||
Controller.getOutbox = async (req, res) => {
|
||||
|
||||
@@ -344,10 +344,7 @@ Emailer.sendToEmail = async (template, email, language, params) => {
|
||||
const usingFallback = !Plugins.hooks.hasListeners('filter:email.send') &&
|
||||
!Plugins.hooks.hasListeners('static:email.send');
|
||||
try {
|
||||
if (Plugins.hooks.hasListeners('filter:email.send')) {
|
||||
// Deprecated, remove in v1.19.0
|
||||
await Plugins.hooks.fire('filter:email.send', data);
|
||||
} else if (Plugins.hooks.hasListeners('static:email.send')) {
|
||||
if (Plugins.hooks.hasListeners('static:email.send')) {
|
||||
await Plugins.hooks.fire('static:email.send', data);
|
||||
} else {
|
||||
await Emailer.sendViaFallback(data);
|
||||
|
||||
@@ -96,7 +96,6 @@ Flags.init = async function () {
|
||||
};
|
||||
|
||||
try {
|
||||
({ filters: Flags._filters } = await plugins.hooks.fire('filter:flags.getFilters', hookData));
|
||||
({ filters: Flags._filters, states: Flags._states } = await plugins.hooks.fire('filter:flags.init', hookData));
|
||||
} catch (err) {
|
||||
winston.error(`[flags/init] Could not retrieve filters\n${err.stack}`);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
const path = require('path');
|
||||
const validator = require('validator');
|
||||
const nconf = require('nconf');
|
||||
@@ -91,11 +90,6 @@ middleware.pageView = helpers.try(async (req, res, next) => {
|
||||
});
|
||||
|
||||
middleware.pluginHooks = helpers.try(async (req, res, next) => {
|
||||
// TODO: Deprecate in v2.0
|
||||
await async.each(plugins.loadedHooks['filter:router.page'] || [], (hookObj, next) => {
|
||||
hookObj.method(req, res, next);
|
||||
});
|
||||
|
||||
await plugins.hooks.fire('response:router.page', {
|
||||
req: req,
|
||||
res: res,
|
||||
|
||||
@@ -8,97 +8,11 @@ const als = require('../als');
|
||||
const Hooks = module.exports;
|
||||
|
||||
Hooks._deprecated = new Map([
|
||||
['filter:email.send', {
|
||||
new: 'static:email.send',
|
||||
since: 'v1.17.0',
|
||||
until: 'v2.0.0',
|
||||
}],
|
||||
['filter:router.page', {
|
||||
new: 'response:router.page',
|
||||
since: 'v1.15.3',
|
||||
until: 'v2.1.0',
|
||||
}],
|
||||
['filter:post.purge', {
|
||||
new: 'filter:posts.purge',
|
||||
since: 'v1.19.6',
|
||||
until: 'v2.1.0',
|
||||
}],
|
||||
['action:post.purge', {
|
||||
new: 'action:posts.purge',
|
||||
since: 'v1.19.6',
|
||||
until: 'v2.1.0',
|
||||
}],
|
||||
['filter:user.verify.code', {
|
||||
new: 'filter:user.verify',
|
||||
since: 'v2.2.0',
|
||||
until: 'v3.0.0',
|
||||
}],
|
||||
['filter:flags.getFilters', {
|
||||
new: 'filter:flags.init',
|
||||
since: 'v2.7.0',
|
||||
until: 'v3.0.0',
|
||||
}],
|
||||
['filter:privileges.global.list', {
|
||||
new: 'static:privileges.global.init',
|
||||
since: 'v3.5.0',
|
||||
until: 'v4.0.0',
|
||||
}],
|
||||
['filter:privileges.global.groups.list', {
|
||||
new: 'static:privileges.global.init',
|
||||
since: 'v3.5.0',
|
||||
until: 'v4.0.0',
|
||||
}],
|
||||
['filter:privileges.global.list_human', {
|
||||
new: 'static:privileges.global.init',
|
||||
since: 'v3.5.0',
|
||||
until: 'v4.0.0',
|
||||
}],
|
||||
['filter:privileges.global.groups.list_human', {
|
||||
new: 'static:privileges.global.init',
|
||||
since: 'v3.5.0',
|
||||
until: 'v4.0.0',
|
||||
}],
|
||||
['filter:privileges.list', {
|
||||
new: 'static:privileges.categories.init',
|
||||
since: 'v3.5.0',
|
||||
until: 'v4.0.0',
|
||||
}],
|
||||
['filter:privileges.groups.list', {
|
||||
new: 'static:privileges.categories.init',
|
||||
since: 'v3.5.0',
|
||||
until: 'v4.0.0',
|
||||
}],
|
||||
['filter:privileges.list_human', {
|
||||
new: 'static:privileges.categories.init',
|
||||
since: 'v3.5.0',
|
||||
until: 'v4.0.0',
|
||||
}],
|
||||
['filter:privileges.groups.list_human', {
|
||||
new: 'static:privileges.categories.init',
|
||||
since: 'v3.5.0',
|
||||
until: 'v4.0.0',
|
||||
}],
|
||||
|
||||
['filter:privileges.admin.list', {
|
||||
new: 'static:privileges.admin.init',
|
||||
since: 'v3.5.0',
|
||||
until: 'v4.0.0',
|
||||
}],
|
||||
['filter:privileges.admin.groups.list', {
|
||||
new: 'static:privileges.admin.init',
|
||||
since: 'v3.5.0',
|
||||
until: 'v4.0.0',
|
||||
}],
|
||||
['filter:privileges.admin.list_human', {
|
||||
new: 'static:privileges.admin.init',
|
||||
since: 'v3.5.0',
|
||||
until: 'v4.0.0',
|
||||
}],
|
||||
['filter:privileges.admin.groups.list_human', {
|
||||
new: 'static:privileges.admin.init',
|
||||
since: 'v3.5.0',
|
||||
until: 'v4.0.0',
|
||||
}],
|
||||
/* ['filter:old.hook.name', {
|
||||
new: 'filter:new.hook.name',
|
||||
since: 'v4.0.0',
|
||||
until: 'v5.0.0',
|
||||
}], */
|
||||
]);
|
||||
|
||||
Hooks.internals = {
|
||||
|
||||
@@ -7,7 +7,7 @@ const utils = require('../utils');
|
||||
const intFields = [
|
||||
'uid', 'pid', 'tid', 'deleted', 'timestamp',
|
||||
'upvotes', 'downvotes', 'deleterUid', 'edited',
|
||||
'replies', 'bookmarks',
|
||||
'replies', 'bookmarks', 'announces',
|
||||
];
|
||||
|
||||
module.exports = function (Posts) {
|
||||
|
||||
@@ -63,10 +63,6 @@ module.exports = function (Posts) {
|
||||
p.cid = tidToTopic[p.tid] && tidToTopic[p.tid].cid;
|
||||
});
|
||||
|
||||
// deprecated hook
|
||||
await Promise.all(postData.map(p => plugins.hooks.fire('filter:post.purge', { post: p, pid: p.pid, uid: uid })));
|
||||
|
||||
// new hook
|
||||
await plugins.hooks.fire('filter:posts.purge', {
|
||||
posts: postData,
|
||||
pids: postData.map(p => p.pid),
|
||||
@@ -90,10 +86,6 @@ module.exports = function (Posts) {
|
||||
|
||||
await resolveFlags(postData, uid);
|
||||
|
||||
// deprecated hook
|
||||
Promise.all(postData.map(p => plugins.hooks.fire('action:post.purge', { post: p, uid: uid })));
|
||||
|
||||
// new hook
|
||||
plugins.hooks.fire('action:posts.purge', { posts: postData, uid: uid });
|
||||
|
||||
await db.deleteAll(postData.map(p => `post:${p.pid}`));
|
||||
|
||||
@@ -35,7 +35,7 @@ module.exports = function (Posts) {
|
||||
await scheduledTopicCheck(data, topicData);
|
||||
|
||||
data.content = data.content === null ? postData.content : data.content;
|
||||
const oldContent = postData.content; // for diffing purposes
|
||||
const oldContent = postData.sourceContent || postData.content; // for diffing purposes
|
||||
const editPostData = getEditPostData(data, topicData, postData);
|
||||
|
||||
if (data.handle) {
|
||||
@@ -55,7 +55,7 @@ module.exports = function (Posts) {
|
||||
]);
|
||||
|
||||
await Posts.setPostFields(data.pid, result.post);
|
||||
const contentChanged = data.content !== oldContent ||
|
||||
const contentChanged = ((data.sourceContent || data.content) !== oldContent) ||
|
||||
topic.renamed ||
|
||||
topic.tagsupdated;
|
||||
|
||||
@@ -194,6 +194,7 @@ module.exports = function (Posts) {
|
||||
function getEditPostData(data, topicData, postData) {
|
||||
const editPostData = {
|
||||
content: data.content,
|
||||
sourceContent: data.sourceContent,
|
||||
editor: data.uid,
|
||||
};
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ privsAdmin.init = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
privsAdmin.getUserPrivilegeList = async () => await plugins.hooks.fire('filter:privileges.admin.list', Array.from(_privilegeMap.keys()));
|
||||
privsAdmin.getGroupPrivilegeList = async () => await plugins.hooks.fire('filter:privileges.admin.groups.list', Array.from(_privilegeMap.keys()).map(privilege => `groups:${privilege}`));
|
||||
privsAdmin.getUserPrivilegeList = () => Array.from(_privilegeMap.keys());
|
||||
privsAdmin.getGroupPrivilegeList = () => Array.from(_privilegeMap.keys()).map(privilege => `groups:${privilege}`);
|
||||
privsAdmin.getPrivilegeList = async () => {
|
||||
const [user, group] = await Promise.all([
|
||||
privsAdmin.getUserPrivilegeList(),
|
||||
@@ -149,18 +149,12 @@ privsAdmin.list = async function (uid) {
|
||||
groupPrivilegeList.splice(idx, 1);
|
||||
}
|
||||
|
||||
const labels = await utils.promiseParallel({
|
||||
users: plugins.hooks.fire('filter:privileges.admin.list_human', privilegeLabels.slice()),
|
||||
groups: plugins.hooks.fire('filter:privileges.admin.groups.list_human', privilegeLabels.slice()),
|
||||
});
|
||||
|
||||
const keys = {
|
||||
users: userPrivilegeList,
|
||||
groups: groupPrivilegeList,
|
||||
};
|
||||
|
||||
const payload = await utils.promiseParallel({
|
||||
labels,
|
||||
labelData: Array.from(_privilegeMap.values()),
|
||||
users: helpers.getUserPrivileges(0, keys.users),
|
||||
groups: helpers.getGroupPrivileges(0, keys.groups),
|
||||
|
||||
@@ -53,8 +53,8 @@ privsCategories.getType = function (privilege) {
|
||||
return priv && priv.type ? priv.type : '';
|
||||
};
|
||||
|
||||
privsCategories.getUserPrivilegeList = async () => await plugins.hooks.fire('filter:privileges.list', Array.from(_privilegeMap.keys()));
|
||||
privsCategories.getGroupPrivilegeList = async () => await plugins.hooks.fire('filter:privileges.groups.list', Array.from(_privilegeMap.keys()).map(privilege => `groups:${privilege}`));
|
||||
privsCategories.getUserPrivilegeList = () => Array.from(_privilegeMap.keys());
|
||||
privsCategories.getGroupPrivilegeList = () => Array.from(_privilegeMap.keys()).map(privilege => `groups:${privilege}`);
|
||||
|
||||
privsCategories.getPrivilegeList = async () => {
|
||||
const [user, group] = await Promise.all([
|
||||
@@ -72,27 +72,20 @@ privsCategories.getPrivilegesByFilter = function (filter) {
|
||||
|
||||
// Method used in admin/category controller to show all users/groups with privs in that given cid
|
||||
privsCategories.list = async function (cid) {
|
||||
let labels = Array.from(_privilegeMap.values()).map(data => data.label);
|
||||
labels = await utils.promiseParallel({
|
||||
users: plugins.hooks.fire('filter:privileges.list_human', labels.slice()),
|
||||
groups: plugins.hooks.fire('filter:privileges.groups.list_human', labels.slice()),
|
||||
});
|
||||
|
||||
const keys = await utils.promiseParallel({
|
||||
users: privsCategories.getUserPrivilegeList(),
|
||||
groups: privsCategories.getGroupPrivilegeList(),
|
||||
});
|
||||
|
||||
const payload = await utils.promiseParallel({
|
||||
labels,
|
||||
labelData: Array.from(_privilegeMap.values()),
|
||||
users: helpers.getUserPrivileges(cid, keys.users),
|
||||
groups: helpers.getGroupPrivileges(cid, keys.groups),
|
||||
});
|
||||
payload.keys = keys;
|
||||
|
||||
payload.columnCountUserOther = payload.labels.users.length - privsCategories._coreSize;
|
||||
payload.columnCountGroupOther = payload.labels.groups.length - privsCategories._coreSize;
|
||||
payload.columnCountUserOther = payload.labelData.length - privsCategories._coreSize;
|
||||
payload.columnCountGroupOther = payload.labelData.length - privsCategories._coreSize;
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
@@ -54,8 +54,8 @@ privsGlobal.getType = function (privilege) {
|
||||
return priv && priv.type ? priv.type : '';
|
||||
};
|
||||
|
||||
privsGlobal.getUserPrivilegeList = async () => await plugins.hooks.fire('filter:privileges.global.list', Array.from(_privilegeMap.keys()));
|
||||
privsGlobal.getGroupPrivilegeList = async () => await plugins.hooks.fire('filter:privileges.global.groups.list', Array.from(_privilegeMap.keys()).map(privilege => `groups:${privilege}`));
|
||||
privsGlobal.getUserPrivilegeList = () => Array.from(_privilegeMap.keys());
|
||||
privsGlobal.getGroupPrivilegeList = () => Array.from(_privilegeMap.keys()).map(privilege => `groups:${privilege}`);
|
||||
privsGlobal.getPrivilegeList = async () => {
|
||||
const [user, group] = await Promise.all([
|
||||
privsGlobal.getUserPrivilegeList(),
|
||||
@@ -65,21 +65,12 @@ privsGlobal.getPrivilegeList = async () => {
|
||||
};
|
||||
|
||||
privsGlobal.list = async function () {
|
||||
async function getLabels() {
|
||||
const labels = Array.from(_privilegeMap.values()).map(data => data.label);
|
||||
return await utils.promiseParallel({
|
||||
users: plugins.hooks.fire('filter:privileges.global.list_human', labels.slice()),
|
||||
groups: plugins.hooks.fire('filter:privileges.global.groups.list_human', labels.slice()),
|
||||
});
|
||||
}
|
||||
|
||||
const keys = await utils.promiseParallel({
|
||||
users: privsGlobal.getUserPrivilegeList(),
|
||||
groups: privsGlobal.getGroupPrivilegeList(),
|
||||
});
|
||||
|
||||
const payload = await utils.promiseParallel({
|
||||
labels: getLabels(),
|
||||
labelData: Array.from(_privilegeMap.values()),
|
||||
users: helpers.getUserPrivileges(0, keys.users),
|
||||
groups: helpers.getGroupPrivileges(0, keys.groups),
|
||||
|
||||
@@ -18,14 +18,18 @@ Analytics.get = async function (socket, data) {
|
||||
data.amount = 24;
|
||||
}
|
||||
}
|
||||
const getStats = data.units === 'days' ? analytics.getDailyStatsForSet : analytics.getHourlyStatsForSet;
|
||||
const getStats = data.units === 'days' ?
|
||||
analytics.getDailyStatsForSet :
|
||||
analytics.getHourlyStatsForSet;
|
||||
|
||||
if (data.graph === 'traffic') {
|
||||
const until = data.until || Date.now();
|
||||
const result = await utils.promiseParallel({
|
||||
uniqueVisitors: getStats('analytics:uniquevisitors', data.until || Date.now(), data.amount),
|
||||
pageviews: getStats('analytics:pageviews', data.until || Date.now(), data.amount),
|
||||
pageviewsRegistered: getStats('analytics:pageviews:registered', data.until || Date.now(), data.amount),
|
||||
pageviewsGuest: getStats('analytics:pageviews:guest', data.until || Date.now(), data.amount),
|
||||
pageviewsBot: getStats('analytics:pageviews:bot', data.until || Date.now(), data.amount),
|
||||
uniqueVisitors: getStats('analytics:uniquevisitors', until, data.amount),
|
||||
pageviews: getStats('analytics:pageviews', until, data.amount),
|
||||
pageviewsRegistered: getStats('analytics:pageviews:registered', until, data.amount),
|
||||
pageviewsGuest: getStats('analytics:pageviews:guest', until, data.amount),
|
||||
pageviewsBot: getStats('analytics:pageviews:bot', until, data.amount),
|
||||
summary: analytics.getSummary(),
|
||||
});
|
||||
result.pastDay = result.pageviews.reduce((a, b) => parseInt(a, 10) + parseInt(b, 10));
|
||||
|
||||
@@ -145,7 +145,7 @@ UserEmail.sendValidationEmail = async function (uid, options) {
|
||||
uid,
|
||||
username,
|
||||
confirm_link,
|
||||
confirm_code: await plugins.hooks.fire('filter:user.verify.code', confirm_code),
|
||||
confirm_code,
|
||||
email: options.email,
|
||||
|
||||
subject: options.subject || '[[email:email.verify-your-email.subject]]',
|
||||
|
||||
@@ -895,7 +895,7 @@ describe('Pruning', () => {
|
||||
const { id } = helpers.mocks.note({
|
||||
cc: [cid],
|
||||
});
|
||||
await activitypub.notes.assert(0, id);
|
||||
await activitypub.notes.assert(0, id, { cid });
|
||||
|
||||
const total = await db.sortedSetCard('usersRemote:lastCrawled');
|
||||
const result = await activitypub.actors.prune();
|
||||
|
||||
@@ -12,6 +12,7 @@ const user = require('../../src/user');
|
||||
const groups = require('../../src/groups');
|
||||
const categories = require('../../src/categories');
|
||||
const topics = require('../../src/topics');
|
||||
const posts = require('../../src/posts');
|
||||
const api = require('../../src/api');
|
||||
|
||||
const helpers = require('./helpers');
|
||||
@@ -47,84 +48,258 @@ describe('FEPs', () => {
|
||||
activitypub._sent.clear();
|
||||
});
|
||||
|
||||
it('should be called when a topic is moved from uncategorized to another category', async () => {
|
||||
const { topicData, postData } = await topics.post({
|
||||
uid,
|
||||
cid: -1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
describe('local actions (create, reply, vote)', () => {
|
||||
let topicData;
|
||||
|
||||
assert(topicData);
|
||||
|
||||
await api.topics.move({ uid: adminUid }, {
|
||||
tid: topicData.tid,
|
||||
cid,
|
||||
});
|
||||
|
||||
assert.strictEqual(activitypub._sent.size, 2);
|
||||
|
||||
const key = Array.from(activitypub._sent.keys())[0];
|
||||
const activity = activitypub._sent.get(key);
|
||||
|
||||
assert(activity && activity.object && typeof activity.object === 'object');
|
||||
assert.strictEqual(activity.object.id, `${nconf.get('url')}/post/${postData.pid}`);
|
||||
});
|
||||
|
||||
it('should be called for a newly forked topic', async () => {
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: -1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
const { tid } = topicData;
|
||||
const { pid: reply1Pid } = await topics.reply({ uid, tid, content: utils.generateUUID() });
|
||||
const { pid: reply2Pid } = await topics.reply({ uid, tid, content: utils.generateUUID() });
|
||||
await topics.createTopicFromPosts(
|
||||
adminUid, utils.generateUUID(), [reply1Pid, reply2Pid], tid, cid
|
||||
);
|
||||
|
||||
assert.strictEqual(activitypub._sent.size, 2, activitypub._sent.keys());
|
||||
|
||||
const key = Array.from(activitypub._sent.keys())[0];
|
||||
const activity = activitypub._sent.get(key);
|
||||
|
||||
assert(activity && activity.object && typeof activity.object === 'object');
|
||||
assert.strictEqual(activity.object.id, `${nconf.get('url')}/post/${reply1Pid}`);
|
||||
});
|
||||
|
||||
it('should be called when a post is moved to another topic', async () => {
|
||||
const [{ topicData: topic1 }, { topicData: topic2 }] = await Promise.all([
|
||||
topics.post({
|
||||
uid,
|
||||
before(async () => {
|
||||
topicData = await api.topics.create({ uid }, {
|
||||
cid,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
}),
|
||||
topics.post({
|
||||
uid,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
activitypub._sent.clear();
|
||||
});
|
||||
|
||||
it('should have federated out both Announce(Create(Article)) and Announce(Article)', () => {
|
||||
const activities = Array.from(activitypub._sent);
|
||||
|
||||
const test1 = activities.some((activity) => {
|
||||
[, activity] = activity;
|
||||
return activity.type === 'Announce' &&
|
||||
activity.object && activity.object.type === 'Create' &&
|
||||
activity.object.object && activity.object.object.type === 'Article';
|
||||
});
|
||||
|
||||
const test2 = activities.some((activity) => {
|
||||
[, activity] = activity;
|
||||
return activity.type === 'Announce' &&
|
||||
activity.object && activity.object.type === 'Article';
|
||||
});
|
||||
|
||||
assert(test1 && test2);
|
||||
});
|
||||
|
||||
it('should federate out Announce(Create(Note)) on local reply', async () => {
|
||||
await api.topics.reply({ uid }, {
|
||||
tid: topicData.tid,
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
|
||||
const activities = Array.from(activitypub._sent);
|
||||
|
||||
assert(activities.some((activity) => {
|
||||
[, activity] = activity;
|
||||
return activity.type === 'Announce' &&
|
||||
activity.object && activity.object.type === 'Create' &&
|
||||
activity.object.object && activity.object.object.type === 'Note';
|
||||
}));
|
||||
});
|
||||
|
||||
it('should NOT federate out Announce(Note) on local reply', async () => {
|
||||
await api.topics.reply({ uid }, {
|
||||
tid: topicData.tid,
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
|
||||
const activities = Array.from(activitypub._sent);
|
||||
|
||||
assert(activities.every((activity) => {
|
||||
[, activity] = activity;
|
||||
if (activity.type === 'Announce' && activity.object && activity.object.type === 'Note') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}));
|
||||
});
|
||||
|
||||
it('should federate out Announce(Like) on local vote', async () => {
|
||||
activitypub._sent.clear();
|
||||
await api.posts.upvote({ uid: adminUid }, { pid: topicData.mainPid, room_id: `topic_${topicData.tid}` });
|
||||
const activities = Array.from(activitypub._sent);
|
||||
|
||||
assert(activities.some((activity) => {
|
||||
[, activity] = activity;
|
||||
return activity.type === 'Announce' &&
|
||||
activity.object && activity.object.type === 'Like';
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote actions (create, reply, vote)', () => {
|
||||
let activity;
|
||||
let pid;
|
||||
let topicData;
|
||||
|
||||
before(async () => {
|
||||
topicData = await api.topics.create({ uid }, {
|
||||
cid,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
assert(topic1 && topic2);
|
||||
afterEach(() => {
|
||||
activitypub._sent.clear();
|
||||
});
|
||||
|
||||
// Create new reply and move it to topic 2
|
||||
const { pid } = await topics.reply({ uid, tid: topic1.tid, content: utils.generateUUID() });
|
||||
await api.posts.move({ uid: adminUid }, { pid, tid: topic2.tid });
|
||||
it('should have slotted the note into the test category', async () => {
|
||||
const { id, note } = await helpers.mocks.note({
|
||||
cc: [`${nconf.get('url')}/category/${cid}`],
|
||||
});
|
||||
pid = id;
|
||||
({ activity } = await helpers.mocks.create(note));
|
||||
await activitypub.inbox.create({ body: activity });
|
||||
|
||||
assert.strictEqual(activitypub._sent.size, 1);
|
||||
const activities = Array.from(activitypub._sent.keys()).map(key => activitypub._sent.get(key));
|
||||
const noteCid = await posts.getCidByPid(pid);
|
||||
assert.strictEqual(noteCid, cid);
|
||||
});
|
||||
|
||||
const activity = activities.pop();
|
||||
assert.strictEqual(activity.type, 'Announce');
|
||||
assert(activity.object && activity.object.type);
|
||||
assert.strictEqual(activity.object.type, 'Create');
|
||||
assert(activity.object.object && activity.object.object.type);
|
||||
assert.strictEqual(activity.object.object.type, 'Note');
|
||||
it('should federate out an Announce(Create(Note)) and Announce(Note) on new topic', async () => {
|
||||
const { id, note } = await helpers.mocks.note({
|
||||
cc: [`${nconf.get('url')}/category/${cid}`],
|
||||
});
|
||||
pid = id;
|
||||
({ activity } = await helpers.mocks.create(note));
|
||||
await activitypub.inbox.create({ body: activity });
|
||||
|
||||
const activities = Array.from(activitypub._sent);
|
||||
|
||||
const test1 = activities.some((activity) => {
|
||||
[, activity] = activity;
|
||||
return activity.type === 'Announce' &&
|
||||
activity.object && activity.object.type === 'Create' &&
|
||||
activity.object.object && activity.object.object.type === 'Note';
|
||||
});
|
||||
|
||||
const test2 = activities.some((activity) => {
|
||||
[, activity] = activity;
|
||||
return activity.type === 'Announce' &&
|
||||
activity.object && activity.object.type === 'Note';
|
||||
});
|
||||
|
||||
assert(test1 && test2);
|
||||
});
|
||||
|
||||
it('should federate out an Announce(Create(Note)) on reply', async () => {
|
||||
const { id, note } = await helpers.mocks.note({
|
||||
cc: [`${nconf.get('url')}/category/${cid}`],
|
||||
inReplyTo: `${nconf.get('url')}/post/${topicData.mainPid}`,
|
||||
});
|
||||
pid = id;
|
||||
({ activity } = await helpers.mocks.create(note));
|
||||
await activitypub.inbox.create({ body: activity });
|
||||
|
||||
const activities = Array.from(activitypub._sent);
|
||||
|
||||
assert(activities.some((activity) => {
|
||||
[, activity] = activity;
|
||||
return activity.type === 'Announce' &&
|
||||
activity.object && activity.object.type === 'Create' &&
|
||||
activity.object.object && activity.object.object.type === 'Note';
|
||||
}));
|
||||
});
|
||||
|
||||
it('should federate out an Announce(Like) on vote', async () => {
|
||||
const { activity } = await helpers.mocks.like({
|
||||
object: {
|
||||
id: `${nconf.get('url')}/post/${topicData.mainPid}`,
|
||||
},
|
||||
});
|
||||
await activitypub.inbox.like({ body: activity });
|
||||
|
||||
const activities = Array.from(activitypub._sent);
|
||||
assert(activities.some((activity) => {
|
||||
[, activity] = activity;
|
||||
return activity.type === 'Announce' &&
|
||||
activity.object && activity.object.type === 'Like';
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('extended actions not explicitly specified in 1b12', () => {
|
||||
it('should be called when a topic is moved from uncategorized to another category', async () => {
|
||||
const { topicData, postData } = await topics.post({
|
||||
uid,
|
||||
cid: -1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
|
||||
assert(topicData);
|
||||
|
||||
await api.topics.move({ uid: adminUid }, {
|
||||
tid: topicData.tid,
|
||||
cid,
|
||||
});
|
||||
|
||||
assert.strictEqual(activitypub._sent.size, 2);
|
||||
|
||||
const key = Array.from(activitypub._sent.keys())[0];
|
||||
const activity = activitypub._sent.get(key);
|
||||
|
||||
assert(activity && activity.object && typeof activity.object === 'object');
|
||||
assert.strictEqual(activity.object.id, `${nconf.get('url')}/post/${postData.pid}`);
|
||||
});
|
||||
|
||||
it('should be called for a newly forked topic', async () => {
|
||||
const { topicData } = await topics.post({
|
||||
uid,
|
||||
cid: -1,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
});
|
||||
const { tid } = topicData;
|
||||
const { pid: reply1Pid } = await topics.reply({ uid, tid, content: utils.generateUUID() });
|
||||
const { pid: reply2Pid } = await topics.reply({ uid, tid, content: utils.generateUUID() });
|
||||
await topics.createTopicFromPosts(
|
||||
adminUid, utils.generateUUID(), [reply1Pid, reply2Pid], tid, cid
|
||||
);
|
||||
|
||||
assert.strictEqual(activitypub._sent.size, 2, activitypub._sent.keys());
|
||||
|
||||
const key = Array.from(activitypub._sent.keys())[0];
|
||||
const activity = activitypub._sent.get(key);
|
||||
|
||||
assert(activity && activity.object && typeof activity.object === 'object');
|
||||
assert.strictEqual(activity.object.id, `${nconf.get('url')}/post/${reply1Pid}`);
|
||||
});
|
||||
|
||||
it('should be called when a post is moved to another topic', async () => {
|
||||
const [{ topicData: topic1 }, { topicData: topic2 }] = await Promise.all([
|
||||
topics.post({
|
||||
uid,
|
||||
cid,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
}),
|
||||
topics.post({
|
||||
uid,
|
||||
cid,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
}),
|
||||
]);
|
||||
|
||||
assert(topic1 && topic2);
|
||||
|
||||
// Create new reply and move it to topic 2
|
||||
const { pid } = await topics.reply({ uid, tid: topic1.tid, content: utils.generateUUID() });
|
||||
await api.posts.move({ uid: adminUid }, { pid, tid: topic2.tid });
|
||||
|
||||
assert.strictEqual(activitypub._sent.size, 1);
|
||||
const activities = Array.from(activitypub._sent.keys()).map(key => activitypub._sent.get(key));
|
||||
|
||||
const activity = activities.pop();
|
||||
assert.strictEqual(activity.type, 'Announce');
|
||||
assert(activity.object && activity.object.type);
|
||||
assert.strictEqual(activity.object.type, 'Create');
|
||||
assert(activity.object.object && activity.object.object.type);
|
||||
assert.strictEqual(activity.object.object.type, 'Note');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,7 +144,7 @@ Helpers.mocks.like = (override = {}) => {
|
||||
|
||||
const activity = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: `${Helpers.mocks._baseUrl}/like/${encodeURIComponent(object)}`,
|
||||
id: `${Helpers.mocks._baseUrl}/like/${encodeURIComponent(object.id || object)}`,
|
||||
type: 'Like',
|
||||
actor,
|
||||
object,
|
||||
@@ -162,6 +162,8 @@ Helpers.mocks.announce = (override = {}) => {
|
||||
if (!object) {
|
||||
({ id: object } = Helpers.mocks.note());
|
||||
}
|
||||
delete override.actor;
|
||||
delete override.object;
|
||||
|
||||
const activity = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
@@ -171,6 +173,7 @@ Helpers.mocks.announce = (override = {}) => {
|
||||
cc: [`${actor}/followers`],
|
||||
actor,
|
||||
object,
|
||||
...override,
|
||||
};
|
||||
|
||||
return { activity };
|
||||
|
||||
@@ -83,33 +83,6 @@ describe('Notes', () => {
|
||||
assert.strictEqual(topic.cid, cid);
|
||||
});
|
||||
|
||||
it('should slot newly created topic in remote category if addressed', async () => {
|
||||
const { id: cid, actor } = helpers.mocks.group();
|
||||
await activitypub.actors.assertGroup([cid]);
|
||||
|
||||
const { id } = helpers.mocks.note({
|
||||
cc: [cid],
|
||||
});
|
||||
|
||||
const assertion = await activitypub.notes.assert(0, id);
|
||||
assert(assertion);
|
||||
|
||||
const { tid, count } = assertion;
|
||||
assert(tid);
|
||||
assert.strictEqual(count, 1);
|
||||
|
||||
const topic = await topics.getTopicData(tid);
|
||||
assert.strictEqual(topic.cid, cid);
|
||||
|
||||
const tids = await db.getSortedSetMembers(`cid:${cid}:tids`);
|
||||
assert(tids.includes(tid));
|
||||
|
||||
const category = await categories.getCategoryData(cid);
|
||||
['topic_count', 'post_count', 'totalPostCount', 'totalTopicCount'].forEach((prop) => {
|
||||
assert.strictEqual(category[prop], 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a remote category topic to a user\'s inbox if they are following the category', async () => {
|
||||
const { id: cid, actor } = helpers.mocks.group();
|
||||
await activitypub.actors.assertGroup([cid]);
|
||||
@@ -120,7 +93,7 @@ describe('Notes', () => {
|
||||
const { id } = helpers.mocks.note({
|
||||
cc: [cid],
|
||||
});
|
||||
const { tid } = await activitypub.notes.assert(0, id);
|
||||
const { tid } = await activitypub.notes.assert(0, id, { cid });
|
||||
|
||||
const inInbox = await db.isSortedSetMember(`uid:${uid}:inbox`, tid);
|
||||
assert(inInbox);
|
||||
@@ -161,7 +134,7 @@ describe('Notes', () => {
|
||||
const { id } = helpers.mocks.note({
|
||||
cc: [remoteCid],
|
||||
});
|
||||
const assertion = await activitypub.notes.assert(0, id);
|
||||
const assertion = await activitypub.notes.assert(0, id, { cid: remoteCid });
|
||||
assert(assertion);
|
||||
|
||||
const unread = await topics.getTotalUnread(uid);
|
||||
@@ -180,7 +153,7 @@ describe('Notes', () => {
|
||||
const { id, note } = helpers.mocks.note({
|
||||
cc: [remoteCid],
|
||||
});
|
||||
const assertion = await activitypub.notes.assert(0, id);
|
||||
const assertion = await activitypub.notes.assert(0, id, { cid: remoteCid });
|
||||
assert(assertion);
|
||||
|
||||
const unread = await topics.getTotalUnread(uid);
|
||||
@@ -203,7 +176,7 @@ describe('Notes', () => {
|
||||
const { id, note } = helpers.mocks.note({
|
||||
cc: [remoteCid],
|
||||
});
|
||||
const assertion = await activitypub.notes.assert(0, id);
|
||||
const assertion = await activitypub.notes.assert(0, id, { cid: remoteCid });
|
||||
assert(assertion);
|
||||
|
||||
const unread = await topics.getTotalUnread(uid);
|
||||
@@ -457,6 +430,44 @@ describe('Notes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Create', () => {
|
||||
let uid;
|
||||
|
||||
before(async () => {
|
||||
uid = await user.create({ username: utils.generateUUID() });
|
||||
});
|
||||
|
||||
describe('(Note)', () => {
|
||||
it('should create a new topic in cid -1', 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 });
|
||||
|
||||
assert(await posts.exists(id));
|
||||
|
||||
const cid = await posts.getCidByPid(id);
|
||||
assert.strictEqual(cid, -1);
|
||||
});
|
||||
|
||||
it('should create a new topic in cid -1 even if a remote category is addressed', async () => {
|
||||
const { id: remoteCid } = helpers.mocks.group();
|
||||
const { note, id } = helpers.mocks.note({
|
||||
audience: [remoteCid],
|
||||
});
|
||||
const { activity } = helpers.mocks.create(note);
|
||||
|
||||
await activitypub.inbox.create({ body: activity });
|
||||
|
||||
assert(await posts.exists(id));
|
||||
|
||||
const cid = await posts.getCidByPid(id);
|
||||
assert.strictEqual(cid, -1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Announce', () => {
|
||||
let cid;
|
||||
|
||||
@@ -464,6 +475,101 @@ describe('Notes', () => {
|
||||
({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
|
||||
});
|
||||
|
||||
describe('(Create)', () => {
|
||||
it('should create a new topic in a remote category if addressed', async () => {
|
||||
const { id: remoteCid } = helpers.mocks.group();
|
||||
const { id, note } = helpers.mocks.note({
|
||||
audience: [remoteCid],
|
||||
});
|
||||
let { activity } = helpers.mocks.create(note);
|
||||
({ activity } = helpers.mocks.announce({ actor: remoteCid, object: activity }));
|
||||
|
||||
await activitypub.inbox.announce({ body: activity });
|
||||
|
||||
assert(await posts.exists(id));
|
||||
|
||||
const cid = await posts.getCidByPid(id);
|
||||
assert.strictEqual(cid, remoteCid);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(Create) or (Note) referencing local post', () => {
|
||||
let uid;
|
||||
let topicData;
|
||||
let postData;
|
||||
let localNote;
|
||||
let announces = 0;
|
||||
|
||||
before(async () => {
|
||||
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
|
||||
({ topicData, postData } = await topics.post({
|
||||
cid,
|
||||
uid,
|
||||
title: utils.generateUUID(),
|
||||
content: utils.generateUUID(),
|
||||
}));
|
||||
localNote = await activitypub.mocks.notes.public(postData);
|
||||
});
|
||||
|
||||
it('should increment announces counter when a remote user shares', async () => {
|
||||
const { id } = helpers.mocks.person();
|
||||
const { activity } = helpers.mocks.announce({
|
||||
actor: id,
|
||||
object: localNote,
|
||||
cc: [`${nconf.get('url')}/uid/${topicData.uid}`],
|
||||
});
|
||||
|
||||
await activitypub.inbox.announce({ body: activity });
|
||||
announces += 1;
|
||||
|
||||
const count = await posts.getPostField(topicData.mainPid, 'announces');
|
||||
assert.strictEqual(count, announces);
|
||||
});
|
||||
|
||||
it('should contain the remote user announcer id in the post announces zset', async () => {
|
||||
const { id } = helpers.mocks.person();
|
||||
const { activity } = helpers.mocks.announce({
|
||||
actor: id,
|
||||
object: localNote,
|
||||
cc: [`${nconf.get('url')}/uid/${topicData.uid}`],
|
||||
});
|
||||
|
||||
await activitypub.inbox.announce({ body: activity });
|
||||
announces += 1;
|
||||
|
||||
const exists = await db.isSortedSetMember(`pid:${topicData.mainPid}:announces`, id);
|
||||
assert(exists);
|
||||
});
|
||||
|
||||
it('should NOT increment announces counter when a remote category shares', async () => {
|
||||
const { id } = helpers.mocks.group();
|
||||
const { activity } = helpers.mocks.announce({
|
||||
actor: id,
|
||||
object: localNote,
|
||||
cc: [`${nconf.get('url')}/uid/${topicData.uid}`],
|
||||
});
|
||||
|
||||
await activitypub.inbox.announce({ body: activity });
|
||||
|
||||
const count = await posts.getPostField(topicData.mainPid, 'announces');
|
||||
assert.strictEqual(count, announces);
|
||||
});
|
||||
|
||||
it('should NOT contain the remote category announcer id in the post announces zset', async () => {
|
||||
const { id } = helpers.mocks.group();
|
||||
const { activity } = helpers.mocks.announce({
|
||||
actor: id,
|
||||
object: localNote,
|
||||
cc: [`${nconf.get('url')}/uid/${topicData.uid}`],
|
||||
});
|
||||
|
||||
await activitypub.inbox.announce({ body: activity });
|
||||
|
||||
const exists = await db.isSortedSetMember(`pid:${topicData.mainPid}:announces`, id);
|
||||
assert(!exists);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(Note)', () => {
|
||||
it('should create a new topic in cid -1 if category not addressed', async () => {
|
||||
const { note } = helpers.mocks.note();
|
||||
|
||||
@@ -476,9 +476,7 @@ describe('Categories', () => {
|
||||
|
||||
it('should get privilege settings', async () => {
|
||||
const data = await apiCategories.getPrivileges({ uid: adminUid }, categoryObj.cid);
|
||||
assert(data.labels);
|
||||
assert(data.labels.users);
|
||||
assert(data.labels.groups);
|
||||
assert(data.labelData);
|
||||
assert(data.keys.users);
|
||||
assert(data.keys.groups);
|
||||
assert(data.users);
|
||||
|
||||
@@ -1442,44 +1442,44 @@ describe('Controllers', () => {
|
||||
});
|
||||
|
||||
it('should handle CSRF error', async () => {
|
||||
plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || [];
|
||||
plugins.loadedHooks['filter:router.page'].push({
|
||||
method: function (req, res, next) {
|
||||
plugins.loadedHooks['response:router.page'] = plugins.loadedHooks['response:router.page'] || [];
|
||||
plugins.loadedHooks['response:router.page'].push({
|
||||
method: function () {
|
||||
const err = new Error('csrf-error');
|
||||
err.code = 'EBADCSRFTOKEN';
|
||||
next(err);
|
||||
throw err;
|
||||
},
|
||||
});
|
||||
|
||||
const { response } = await request.get(`${nconf.get('url')}/users`);
|
||||
plugins.loadedHooks['filter:router.page'] = [];
|
||||
plugins.loadedHooks['response:router.page'] = [];
|
||||
assert.equal(response.statusCode, 403);
|
||||
});
|
||||
|
||||
it('should handle black-list error', async () => {
|
||||
plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || [];
|
||||
plugins.loadedHooks['filter:router.page'].push({
|
||||
method: function (req, res, next) {
|
||||
plugins.loadedHooks['response:router.page'] = plugins.loadedHooks['response:router.page'] || [];
|
||||
plugins.loadedHooks['response:router.page'].push({
|
||||
method: function () {
|
||||
const err = new Error('blacklist error message');
|
||||
err.code = 'blacklisted-ip';
|
||||
next(err);
|
||||
throw err;
|
||||
},
|
||||
});
|
||||
const { response, body } = await request.get(`${nconf.get('url')}/users`);
|
||||
plugins.loadedHooks['filter:router.page'] = [];
|
||||
plugins.loadedHooks['response:router.page'] = [];
|
||||
assert.equal(response.statusCode, 403);
|
||||
assert.equal(body, 'blacklist error message');
|
||||
});
|
||||
|
||||
it('should handle page redirect through error', async () => {
|
||||
plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || [];
|
||||
plugins.loadedHooks['filter:router.page'].push({
|
||||
method: function (req, res, next) {
|
||||
plugins.loadedHooks['response:router.page'] = plugins.loadedHooks['response:router.page'] || [];
|
||||
plugins.loadedHooks['response:router.page'].push({
|
||||
method: function () {
|
||||
const err = new Error('redirect');
|
||||
err.status = 302;
|
||||
err.path = '/popular';
|
||||
plugins.loadedHooks['filter:router.page'] = [];
|
||||
next(err);
|
||||
plugins.loadedHooks['response:router.page'] = [];
|
||||
throw err;
|
||||
},
|
||||
});
|
||||
const { response, body } = await request.get(`${nconf.get('url')}/users`);
|
||||
@@ -1488,14 +1488,14 @@ describe('Controllers', () => {
|
||||
});
|
||||
|
||||
it('should handle api page redirect through error', async () => {
|
||||
plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || [];
|
||||
plugins.loadedHooks['filter:router.page'].push({
|
||||
method: function (req, res, next) {
|
||||
plugins.loadedHooks['response:router.page'] = plugins.loadedHooks['response:router.page'] || [];
|
||||
plugins.loadedHooks['response:router.page'].push({
|
||||
method: function () {
|
||||
const err = new Error('redirect');
|
||||
err.status = 308;
|
||||
err.path = '/api/popular';
|
||||
plugins.loadedHooks['filter:router.page'] = [];
|
||||
next(err);
|
||||
plugins.loadedHooks['response:router.page'] = [];
|
||||
throw err;
|
||||
},
|
||||
});
|
||||
const { response, body } = await request.get(`${nconf.get('url')}/api/users`);
|
||||
@@ -1505,15 +1505,15 @@ describe('Controllers', () => {
|
||||
});
|
||||
|
||||
it('should handle error page', async () => {
|
||||
plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || [];
|
||||
plugins.loadedHooks['filter:router.page'].push({
|
||||
method: function (req, res, next) {
|
||||
plugins.loadedHooks['response:router.page'] = plugins.loadedHooks['response:router.page'] || [];
|
||||
plugins.loadedHooks['response:router.page'].push({
|
||||
method: function () {
|
||||
const err = new Error('regular error');
|
||||
next(err);
|
||||
throw err;
|
||||
},
|
||||
});
|
||||
const { response, body } = await request.get(`${nconf.get('url')}/users`);
|
||||
plugins.loadedHooks['filter:router.page'] = [];
|
||||
plugins.loadedHooks['response:router.page'] = [];
|
||||
assert.equal(response.statusCode, 500);
|
||||
assert(body);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user