Merge branch 'badges' into modularize

This commit is contained in:
yflory 2025-04-18 10:09:38 +02:00
commit 900ab02404
25 changed files with 371 additions and 45 deletions

View File

@ -134,7 +134,12 @@ define(req, function(AppConfig, Default, Language) {
}
};
// XXX
Messages.badges_admin = "Instance administrator";
Messages.badges_moderator = "Instance moderator";
Messages.badges_premium = "Premium user";
Messages.badges_error = "Error while validating this user's data";
Messages.profile_badges = "Badges";
return Messages;

View File

@ -0,0 +1,28 @@
@import (reference) "./colortheme-all.less";
@import (reference) "./variables.less";
.badges_vars(
@width: 30px
) {
@badges-width: @width;
}
.badges_main(@width: 30px) {
--LessLoader_require: LessLoader_currentFile();
.badges_vars(@width);
--badges-width: @badges-width;
--badges-font: ceil(@badges-width*2/3);
}
& {
.badges_vars();
i.cp-badge {
width: var(--badges-width);
height: var(--badges-width);
box-sizing: border-box;
border: 1px solid @cryptpad_text_col;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: var(--badges-font);
}
}

View File

@ -263,6 +263,14 @@
.cp-toolbar-userlist-friend {
padding: 0;
}
.cp-userlist-badge {
i {
color: @cryptpad_color_brand;
}
i.cp-badge-error {
color: @cryptpad_color_warn_red;
}
}
}
}
}

View File

@ -309,3 +309,9 @@ Pinning.getDeletedPads = function (Env, channels, cb) {
Pinning.isChannelPinned = function (Env, channel, cb) {
return void cb(void 0, true);
};
Pinning.isPremium = function (Env, userKey, cb) {
const limit = Env.limits[userKey];
return void cb(void 0, !!limit?.plan);
//return void cb(void 0, (limit?.plan && limit.plan !== "custom"));
};

View File

@ -24,6 +24,7 @@ const UNAUTHENTICATED_CALLS = {
WRITE_PRIVATE_MESSAGE: Channel.writePrivateMessage,
DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage,
GET_METADATA: Metadata.getMetadata,
IS_PREMIUM: Pinning.isPremium,
ADD_FIRST_ADMIN: Admin.addFirstAdmin
};

View File

@ -8,12 +8,13 @@ const factory = (Sortify, UserObject, ProxyManager,
SF, AccountTS, DriveTS, Cursor,
Support, Integration, OnlyOffice,
Mailbox, Profile, Team, Messenger, History,
Calendar, Block, NetConfig,
Calendar, BadgeTS, Block, NetConfig,
Crypto, ChainPad, CpNetflux, Listmap,
Netflux, nThen) => {
const Account = AccountTS.Account;
const Drive = DriveTS.Drive;
const Badge = BadgeTS.Badge;
const window = globalThis;
globalThis.nacl = globalThis.nacl || Crypto.Nacl;
@ -661,11 +662,15 @@ const factory = (Sortify, UserObject, ProxyManager,
color: Store.getUserColor(),
notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']),
curvePublic: proxy.curvePublic,
edPublic: proxy.edPublic,
netfluxId: store?.network?.webChannels?.[0]?.myID,
badge: Util.find(proxy, ['profile', 'badge'])
},
// "priv" is not shared with other users but is needed by the apps
priv: {
clientId: clientId,
edPublic: proxy.edPublic,
edPrivate: proxy.edPrivate,
friends: proxy.friends || {},
settings: proxy.settings || NEW_USER_SETTINGS,
thumbnails: disableThumbnails === false,
@ -2809,6 +2814,7 @@ const factory = (Sortify, UserObject, ProxyManager,
loadUniversal(Integration, 'integration', waitFor);
loadUniversal(Messenger, 'messenger', waitFor);
loadUniversal(History, 'history', waitFor);
loadUniversal(Badge, 'badge', waitFor);
loadOnlyOffice();
if (store) {
store.messenger = store.modules['messenger'];
@ -3464,7 +3470,7 @@ module.exports = factory(
require('../common/common-constants'),
require('../common/common-feedback'),
require('../common/common-realtime'),
require('../common/common-messaging'),
require('./components/messaging'),
require('../common/pinpad'),
require('../common/rpc'),
require('./components/merge-drive'),
@ -3482,6 +3488,7 @@ module.exports = factory(
require('./modules/messenger'),
require('./modules/history'),
require('./modules/calendar'),
require('./modules/badge'), // .ts
require('../common/outer/login-block'),
require('../common/network-config'),
require('chainpad-crypto'),

View File

@ -983,7 +983,7 @@ const factory = (Messaging, Hash, Util, Crypto, Block) => {
};
module.exports = factory(
require('../../common/common-messaging'),
require('./messaging'),
require('../../common/common-hash'),
require('../../common/common-util'),
require('chainpad-crypto'),

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Crypto, Hash, Util, Constants, Realtime) => {
var Msg = {};
@ -15,6 +14,7 @@ const factory = (Crypto, Hash, Util, Constants, Realtime) => {
curvePublic: proxy.curvePublic,
notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']),
avatar: proxy.profile && proxy.profile.avatar,
badge: proxy.profile && proxy.profile.badge,
uid: proxy.uid,
};
if (hash === false) { delete data.channel; }
@ -139,23 +139,10 @@ const factory = (Crypto, Hash, Util, Constants, Realtime) => {
return Msg;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require('chainpad-crypto'),
require('./common-hash'),
require('./common-util'),
require('./common-constants'),
require('./common-realtime')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/components/chainpad-crypto/crypto.js',
'/common/common-hash.js',
'/common/common-util.js',
'/common/common-constants.js',
'/common/common-realtime.js',
], factory);
} else {
// unsupported initialization
}
})();
module.exports = factory(
require('chainpad-crypto'),
require('../../common/common-hash'),
require('../../common/common-util'),
require('../../common/common-constants'),
require('../../common/common-realtime')
);

View File

@ -511,7 +511,7 @@ module.exports = factory(
require('../../common/common-feedback'),
require('../../common/common-hash'),
require('../../common/common-util'),
require('../../common/common-messaging'),
require('./messaging'),
require('../../common/cryptget'),
require('../modules/mailbox'),
require('../../common/common-realtime'),

114
src/worker/modules/badge.ts Normal file
View File

@ -0,0 +1,114 @@
import nacl from 'tweetnacl/nacl-fast';
import nThen from 'nthen';
import { Module, ModuleObject, Callback } from '../types'
import * as Util from '../../common/common-util.js';
export interface BadgeModule<T> extends Module<T> {
setCustomize: (data: any) => void
}
let ApiConfig:any = {};
const checkApi = (ctx, ed, cb) => {
const badges = [];
const origin:string = ApiConfig?.httpUnsafeOrigin;
Util.fetchApi(origin, 'config', true, Config => {
const isAdmin = Config?.adminKeys?.includes(ed);
if (isAdmin) { badges.push('admin'); }
const isModerator = Config?.moderatorKeys?.includes(ed);
if (isModerator) { badges.push('moderator'); }
cb(badges);
});
};
const checkPremium = (ctx, ed, cb) => {
ctx.Store.anonRpcMsg('', {
msg: 'IS_PREMIUM',
data: ed
}, obj => {
let premium = Array.isArray(obj) && obj[0];
cb(premium ? ['premium'] : []);
});
};
const listBadges:Callback = (ctx, data, clientId, cb) => {
const badges = [];
const origin:string = ApiConfig?.httpUnsafeOrigin;
const ed = data.edPublic || ctx.store?.proxy?.edPublic;
nThen(waitFor => {
checkApi(ctx, ed, waitFor(list => {
Array.prototype.push.apply(badges, list);
}));
}).nThen(waitFor => {
checkPremium(ctx, ed, waitFor(list => {
Array.prototype.push.apply(badges, list);
}));
}).nThen(() => {
cb(badges);
});
};
const allBadges = {
'admin': checkApi,
'moderator': checkApi,
'premium': checkPremium
};
const checkBadge:Callback = (ctx, data, clientId, cb) => {
const { badge, ed, sig, nid/*, channel*/ } = data;
const publicKey = Util.decodeBase64(ed); // Public key uint8
const signature = Util.decodeBase64(sig); // badge uint8
// Check signature
const msg = nacl.sign.open(signature, publicKey);
if (!msg) { return void cb({verified: false}); }
// Check channel (replay attack)
const msgStr:string = Util.encodeUTF8(msg);
const expected:string = nid; // + channel;
if (msgStr !== expected) { return void cb({verified: false}); }
let f = allBadges[badge];
if (!f) { return void cb({verified: false, error: 'EINVAL'}); }
f(ctx, ed, list => {
cb({
verified: list.includes(badge),
badge: badge
});
});
};
const Badge: BadgeModule<ModuleObject> = {
init: (config, cb, emit) => {
const ctx:any = {
store: config.store,
Store: config.Store,
updateMetadata: config.updateMetadata
};
return {
removeClient: () => {},
execCommand: (clientId, obj, cb) => {
console.log('Exex command', clientId, obj);
const cmd = obj.cmd;
const data = obj.data;
if (cmd === 'LIST_BADGES') {
return void listBadges(ctx, data, clientId, cb);
}
if (cmd === 'CHECK_BADGE') {
return void checkBadge(ctx, data, clientId, cb);
}
cb();
},
}
},
setCustomize: data => {
ApiConfig = data?.ApiConfig;
}
};
export { Badge }

View File

@ -677,7 +677,7 @@ module.exports = factory(
require('../../common/common-util'),
require('../../common/common-hash'),
require('../../common/common-realtime'),
require('../../common/common-messaging'),
require('../components/messaging'),
require('../../common/notify'),
require('../components/mailbox-handlers'),
require('chainpad-netflux'),

View File

@ -1124,7 +1124,7 @@ module.exports = factory(
require('../../common/common-hash'),
require('../../common/common-util'),
require('../../common/common-realtime'),
require('../../common/common-messaging'),
require('../components/messaging'),
require('../../common/common-constants'),
require('../../common/pad-types'),
require('nthen')

View File

@ -98,6 +98,12 @@ const factory = (Util, Hash, Constants, Realtime,
ctx.emit('UPDATE', ctx.listmap.proxy, ctx.clients.filter(function (clientId) {
return clientId !== cId;
}));
if (key === 'badge') {
ctx.Store.set(null, {
key: ['profile', 'badge'],
value: value || undefined
}, () => {});
}
cb(ctx.listmap.proxy);
});
};
@ -112,6 +118,7 @@ const factory = (Util, Hash, Constants, Realtime,
var store = cfg.store;
if (!store.loggedIn || !store.proxy.edPublic) { return; }
var ctx = {
Store: cfg.Store,
store: store,
pinPads: cfg.pinPads,
updateMetadata: cfg.updateMetadata,

View File

@ -2250,7 +2250,7 @@ module.exports = factory(
require('../../common/user-object'),
require('../components/sharedfolder'),
require('../components/roster'),
require('../../common/common-messaging'),
require('../components/messaging'),
require('../../common/common-feedback'),
require('../components/invitation'),
require('../../common/cryptget'),

View File

@ -12,7 +12,7 @@ export interface TestModuleObject extends ModuleObject {
const TestModule: Module<TestModuleObject> = {
init: (config, cb) => {
init: (config, cb, emit) => {
let myTest = {};
cb();

View File

@ -15,7 +15,6 @@ import * as Util from '../common/common-util.js';
import * as Hash from '../common/common-hash.js';
import * as Feedback from '../common/common-feedback.js';
import * as Realtime from '../common/common-realtime.js';
import * as Messaging from '../common/common-messaging.js';
import * as Constants from '../common/common-constants.js';
import * as Credential from '../common/common-credential.js';
import * as ProxyManager from '../common/proxy-manager.js';
@ -36,6 +35,7 @@ import * as AsyncConnector from './core/async-connector.js';
// Components
import * as Migrate from './components/migrate-user-object.js';
import * as Messaging from './components/messaging.js';
// Modules
import * as Mailbox from './modules/mailbox.js';
@ -48,6 +48,7 @@ import * as Team from './modules/team.js';
import * as Messenger from './modules/messenger.js';
import * as History from './modules/history.js';
import * as Calendar from './modules/calendar.js';
import { Badge } from './modules/badge';
declare var WorkerGlobalScope: any
declare var SharedWorkerGlobalScope: any
@ -76,7 +77,8 @@ let start = (cfg: StoreConfig):void => {
Calendar,
Store,
Account,
Mailbox
Mailbox,
Badge
].forEach(dep => {
if (typeof(dep.setCustomize) === "function") {
dep.setCustomize(cfg);

View File

@ -7,7 +7,11 @@
export type Callback = (...args: any[]) => void
export type ModuleConfig = {
emit: Function
store: any,
Store: any,
updateMetadata: Callback,
pinPads: Callback,
unpinPads: Callback
}
export type CommandData = {
@ -21,7 +25,7 @@ export interface ModuleObject {
}
export interface Module<T> {
init: (config: ModuleConfig, cb: Callback) => T
init: (config: ModuleConfig, cb: Callback, emit: Callback) => T
}

View File

@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
define([
'/common/hyperscript.js',
'/customize/messages.js',
], function (h, Messages) {
var Badges = {};
const badges = {
admin: 'fa-star',
moderator: 'fa-life-ring',
premium: 'fa-check-circle',
error: 'fa-exclamation-circle'
};
Badges.render = id => {
let icon = badges[id];
if (!icon) { return; }
let cls = icon.indexOf('cptools') === 0 ? 'cptools '+icon : 'fa '+icon;
if (id === 'error') { cls += ' cp-badge-error'; }
return h('i', {
'class': `cp-badge ${cls}`,
'title': Messages[`badges_${id}`] || id
});
};
return Badges;
});

View File

@ -186,7 +186,7 @@ define([
if (avatars[href]) {
var nodes = $.parseHTML(avatars[href]);
var $el = $(nodes[0]);
$container.append($el);
$container.empty().append($el);
return void cb($el);
}
@ -222,7 +222,7 @@ define([
if (typeof data !== "number") { return void displayDefault(); }
if (Util.bytesToMegabytes(data) > 0.5) { return void displayDefault(); }
var mt = UI.mediaTag(src, cryptKey);
var $img = $(mt).appendTo($container);
var $img = $(mt).appendTo($container.empty());
MT.displayMediatagImage(common, $img, function (err, $image) {
if (err) { return void console.error(err); }
centerImage($img, $image);

View File

@ -751,6 +751,7 @@ define([
if (!parsed.type) { throw new Error(); }
var defaultTitle = Utils.UserObject.getDefaultName(parsed);
var edPublic, curvePublic, notifications, isTemplate;
var edPrivate;
var settings = {};
var isSafe = ['debug', 'profile', 'drive', 'teams', 'calendar', 'file'].indexOf(currentPad.app) !== -1;
var isOO = ['sheet', 'doc', 'presentation'].indexOf(parsed.type) !== -1;
@ -760,6 +761,7 @@ define([
if (isDeleted) {
Utils.Cache.clearChannel(secret.channel);
}
let signature;
var updateMeta = function () {
//console.log('EV_METADATA_UPDATE');
@ -775,6 +777,9 @@ define([
curvePublic = metaObj.user.curvePublic;
notifications = metaObj.user.notifications;
settings = metaObj.priv.settings;
edPrivate = metaObj.priv.edPrivate;
delete metaObj.priv.edPrivate; // don't send to inner
}));
if (typeof(isTemplate) === "undefined") {
Cryptpad.isTemplate(currentPad.href, waitFor(function (err, t) {
@ -856,6 +861,18 @@ define([
}
}
if (metaObj?.user?.edPublic) {
let str = metaObj?.user?.netfluxId;// + secret.channel;
if (!signature && str) {
let myIDu8 = Utils.Util.decodeUTF8(str);
let k = Utils.Util.decodeBase64(edPrivate);
let nacl = Utils.Crypto.Nacl;
let s = nacl.sign(myIDu8, k);
signature = Utils.Util.encodeBase64(s);
}
metaObj.user.signature = signature;
}
// Early access
var priv = metaObj.priv;
var _plan = typeof(priv.plan) === "undefined" ? Utils.LocalStore.getPremium() : priv.plan;

View File

@ -4,7 +4,7 @@
(() => {
const factory = function () {
let USE_MIN = false;
let USE_MIN = true;
let path = '/common/worker.bundle.js?';
if (USE_MIN) { path = '/common/worker.bundle.min.js?'; }

View File

@ -13,12 +13,13 @@ define([
'/common/common-util.js',
'/common/common-feedback.js',
'/common/inner/common-mediatag.js',
'/common/inner/badges.js',
'/common/hyperscript.js',
'/common/messenger-ui.js',
'/customize/messages.js',
'/customize/pages.js',
'/common/pad-types.js',
], function ($, Config, ApiConfig, Broadcast, UIElements, UI, Hash, Util, Feedback, MT, h,
], function ($, Config, ApiConfig, Broadcast, UIElements, UI, Hash, Util, Feedback, MT, Badges, h,
MessengerUI, Messages, Pages, PadTypes) {
var Common;
@ -193,16 +194,14 @@ MessengerUI, Messages, Pages, PadTypes) {
// Display only one time each user (if he is connected in multiple tabs)
var uids = [];
Object.keys(userData).forEach(function(user) {
//if (user !== userNetfluxId) {
var data = userData[user] || {};
var userId = data.uid;
if (!userId) { return; }
//data.netfluxId = user;
if (uids.indexOf(userId) === -1) {// && (!myUid || userId !== myUid)) {
if (user !== data.netfluxId) { return; }
if (uids.indexOf(userId) === -1) {
uids.push(userId);
list.push(data);
} else { i++; }
//}
});
return {
list: list,
@ -223,6 +222,7 @@ MessengerUI, Messages, Pages, PadTypes) {
});
};
var showColors = false;
const validatedBadges = {};
var updateUserList = function (toolbar, config, forceOffline) {
if (!config.displayed || config.displayed.indexOf('userlist') === -1) { return; }
if (toolbar.isAlone) { return; }
@ -427,6 +427,38 @@ MessengerUI, Messages, Pages, PadTypes) {
$span.append($rightCol);
}, data.uid);
$span.data('uid', data.uid);
if (data.badge && data.edPublic) {
const addBadge = (badge) => {
let i = Badges.render(badge);
if (!i) { return; }
$rightCol.append(h('div.cp-userlist-badge', i));
};
const key = data.signature + '-' + data.badge;
const v = validatedBadges[key];
if (typeof (v) === "string") {
addBadge(v);
} else if (v === false) {
addBadge('error');
} else {
let ev = validatedBadges[key] ||= Util.mkEvent(true);
ev.reg(badge => { addBadge(badge); });
toolbar.badges.execCommand('CHECK_BADGE', {
badge: data.badge,
channel: priv.channel,
ed: data.edPublic,
sig: data.signature,
nid: data.netfluxId
}, res => {
if (!res?.verified) {
validatedBadges[key] = false;
return void addBadge('error');
}
validatedBadges[key] = res.badge;
ev.fire(res.badge);
});
}
}
$editUsersList.append($span);
});
@ -1401,6 +1433,8 @@ MessengerUI, Messages, Pages, PadTypes) {
toolbar.connected = false;
toolbar.firstConnection = true;
toolbar.badges = Common.makeUniversal('badge');
if (Array.isArray(cfg.displayed) && cfg.displayed.includes('pad')) {
cfg.addFileMenu = true;
}

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,7 @@
@import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
@import (reference) '../../customize/src/less2/include/variables.less';
@import (reference) '../../customize/src/less2/include/badges.less';
&.cp-app-profile {
@ -26,6 +27,29 @@
margin-top: 1rem;
}
.cp-profile-badges {
display: flex;
align-items: center;
& > span {
margin-right: 1em;
}
.cp-profile-badges-list {
.badges_main(40px);
i {
cursor: pointer;
&:focus-visible {
outline: @variables_focus_style;
}
&:not(:last-child) {
margin-right: 0.5em;
}
&.cp-selected {
border-color: @cryptpad_color_brand;
}
}
}
}
#cp-app-profile-header {
display: flex;
flex-flow: row wrap;

View File

@ -17,6 +17,7 @@ define([
'/common/common-realtime.js',
'/common/clipboard.js',
'/common/inner/common-mediatag.js',
'/common/inner/badges.js',
'/common/hyperscript.js',
'/customize/messages.js',
'/customize/application_config.js',
@ -48,6 +49,7 @@ define([
Realtime,
Clipboard,
MT,
Badges,
h,
Messages,
AppConfig,
@ -92,6 +94,7 @@ define([
var LINK_ID = "cp-app-profile-link";
var AVATAR_ID = "cp-app-profile-avatar";
var DESCRIPTION_ID = "cp-app-profile-description";
var BADGES_ID = "cp-app-profile-badges";
var CREATE_ID = "cp-app-profile-create";
var HEADER_ID = "cp-app-profile-header";
var HEADER_RIGHT_ID = "cp-app-profile-rightside";
@ -375,7 +378,7 @@ define([
var displayAvatar = function (val) {
var sframeChan = common.getSframeChannel();
var $span = APP.$avatar;
$span.html('');
$span.empty();
if (!val) {
$('<img>', {
src: '/customize/images/avatar.png',
@ -447,6 +450,48 @@ define([
displayAvatar(data.avatar);
};
const addBadges = $container => {
var $block = $('<div>', {id: BADGES_ID, class:'cp-sidebarlayout-element'}).appendTo($container);
APP.$badges = $(h('span')).appendTo($block);
};
const refreshBadges = (obj) => {
if (!APP.$badges) { return; }
const metadataMgr = APP.common.getMetadataMgr();
const privateData = metadataMgr.getPrivateData();
let args = {};
if (!privateData.isOwnProfile) { args.edPublic = obj.edPublic; }
APP.badge.execCommand('LIST_BADGES', args, data => {
APP.$badges.empty();
let spinner;
APP.$badges.toggle(!!data.length);
let all = data.map(str => {
const i = Badges.render(str);
const $i = $(i).attr('tabindex', 0);
if (APP.readOnly) { return i; }
const selected = obj?.badge === str;
if (selected) { $i.addClass('cp-selected'); }
Util.onClickEnter($i, () => {
let value = selected ? '' : str;
spinner.spin();
APP.module.execCommand('SET', {
key: 'badge',
value
}, function (data) {
spinner.hide();
APP.updateValues(data);
});
});
return i;
});
let content = h('div.cp-profile-badges', [
h('span', Messages.profile_badges),
h('div.cp-profile-badges-list', all)
]);
APP.$badges.append(content);
spinner = UI.makeSpinner(APP.$badges.find('> div'));
});
};
var addDescription = function ($container) {
var $block = $('<div>', {id: DESCRIPTION_ID, class: PROFILE_SECTION}).appendTo($container);
@ -462,7 +507,7 @@ define([
}, [
h('i.fa.fa-pencil', {'aria-hidden': 'true' }),
h('span#cp-profile-add-description-button', Messages.profile_addDescription)
]);
]);
APP.$descriptionEdit = $(button);
var save = h('button.btn.btn-primary', Messages.settings_save);
var text = h('textarea');
@ -609,6 +654,7 @@ define([
addLink($rightside);
addFriendRequest($rightside);
addMuteButton($rightside);
addBadges($rightside);
addPublicKey($rightside);
addCopyData($rightside);
addViewButton($rightside);
@ -627,6 +673,7 @@ define([
refreshName(data);
refreshLink(data);
refreshDescription(data);
refreshBadges(data);
refreshFriendRequest(data);
refreshMute(data);
setPublicKeyButton(data);
@ -700,6 +747,9 @@ define([
return;
}
APP.badge = common.makeUniversal('badge', {
onEvent: onEvent
});
if (privateData.isOwnProfile) {
APP.module = common.makeUniversal('profile', {