clay-server 2.27.0-beta.8 → 2.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/lib/daemon-projects.js +164 -0
- package/lib/daemon.js +13 -126
- package/lib/mates-identity.js +132 -0
- package/lib/mates-knowledge.js +113 -0
- package/lib/mates-prompts.js +398 -0
- package/lib/mates.js +40 -599
- package/lib/project-connection.js +2 -0
- package/lib/project-debate.js +19 -12
- package/lib/project-http.js +4 -2
- package/lib/project-loop.js +110 -48
- package/lib/project-mate-interaction.js +4 -0
- package/lib/project-notifications.js +210 -0
- package/lib/project-sessions.js +5 -2
- package/lib/project-user-message.js +2 -1
- package/lib/project.js +26 -2
- package/lib/public/app.js +1193 -8521
- package/lib/public/css/command-palette.css +14 -0
- package/lib/public/css/loop.css +301 -0
- package/lib/public/css/notifications-center.css +190 -0
- package/lib/public/css/rewind.css +6 -0
- package/lib/public/index.html +89 -35
- package/lib/public/modules/app-connection.js +160 -0
- package/lib/public/modules/app-cursors.js +473 -0
- package/lib/public/modules/app-debate-ui.js +389 -0
- package/lib/public/modules/app-dm.js +627 -0
- package/lib/public/modules/app-favicon.js +212 -0
- package/lib/public/modules/app-header.js +229 -0
- package/lib/public/modules/app-home-hub.js +600 -0
- package/lib/public/modules/app-loop-ui.js +589 -0
- package/lib/public/modules/app-loop-wizard.js +439 -0
- package/lib/public/modules/app-messages.js +1560 -0
- package/lib/public/modules/app-misc.js +299 -0
- package/lib/public/modules/app-notifications.js +372 -0
- package/lib/public/modules/app-panels.js +888 -0
- package/lib/public/modules/app-projects.js +798 -0
- package/lib/public/modules/app-rate-limit.js +451 -0
- package/lib/public/modules/app-rendering.js +597 -0
- package/lib/public/modules/app-skills-install.js +234 -0
- package/lib/public/modules/command-palette.js +27 -4
- package/lib/public/modules/input.js +31 -20
- package/lib/public/modules/scheduler-config.js +1532 -0
- package/lib/public/modules/scheduler-history.js +79 -0
- package/lib/public/modules/scheduler.js +33 -1554
- package/lib/public/modules/session-search.js +13 -1
- package/lib/public/modules/sidebar-mates.js +812 -0
- package/lib/public/modules/sidebar-mobile.js +1269 -0
- package/lib/public/modules/sidebar-projects.js +1449 -0
- package/lib/public/modules/sidebar-sessions.js +986 -0
- package/lib/public/modules/sidebar.js +232 -4591
- package/lib/public/modules/store.js +27 -0
- package/lib/public/modules/ws-ref.js +7 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +96 -717
- package/lib/sdk-message-processor.js +587 -0
- package/lib/sdk-message-queue.js +42 -0
- package/lib/sdk-skill-discovery.js +131 -0
- package/lib/server-admin.js +712 -0
- package/lib/server-auth.js +737 -0
- package/lib/server-dm.js +221 -0
- package/lib/server-mates.js +281 -0
- package/lib/server-palette.js +110 -0
- package/lib/server-settings.js +479 -0
- package/lib/server-skills.js +280 -0
- package/lib/server.js +246 -2755
- package/lib/sessions.js +11 -4
- package/lib/users-auth.js +146 -0
- package/lib/users-permissions.js +118 -0
- package/lib/users-preferences.js +210 -0
- package/lib/users.js +48 -398
- package/lib/ws-schema.js +498 -0
- package/package.json +1 -1
package/lib/sessions.js
CHANGED
|
@@ -19,6 +19,7 @@ function createSessionManager(opts) {
|
|
|
19
19
|
var slashCommands = null; // shared across sessions
|
|
20
20
|
var skillNames = null; // Claude-only skills to filter from slash menu
|
|
21
21
|
var singleUserUnread = {}; // sessionLocalId -> unread count (single-user mode)
|
|
22
|
+
var permissionRequestIndex = {}; // requestId -> sessionLocalId (O(1) lookup)
|
|
22
23
|
|
|
23
24
|
// --- Session persistence (centralized in ~/.clay/sessions/{encoded-cwd}/) ---
|
|
24
25
|
var sessionsBase = path.join(config.CONFIG_DIR, "sessions");
|
|
@@ -521,11 +522,15 @@ function createSessionManager(opts) {
|
|
|
521
522
|
if (ws.readyState === 1) ws.send(ioData);
|
|
522
523
|
}
|
|
523
524
|
// Track unread: increment on "done" for clients not viewing this session
|
|
525
|
+
// Only count if session has no owner (my session) or owner matches this client
|
|
524
526
|
if (obj.type === "done" && ws._clayActiveSession !== session.localId) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
ws.
|
|
527
|
+
var _isMySession = !session.ownerId || (ws._clayUser && ws._clayUser.id === session.ownerId);
|
|
528
|
+
if (_isMySession) {
|
|
529
|
+
if (!ws._clayUnread) ws._clayUnread = {};
|
|
530
|
+
ws._clayUnread[session.localId] = (ws._clayUnread[session.localId] || 0) + 1;
|
|
531
|
+
if (ws.readyState === 1) {
|
|
532
|
+
ws.send(JSON.stringify({ type: "session_unread", id: session.localId, count: ws._clayUnread[session.localId] }));
|
|
533
|
+
}
|
|
529
534
|
}
|
|
530
535
|
}
|
|
531
536
|
});
|
|
@@ -664,6 +669,7 @@ function createSessionManager(opts) {
|
|
|
664
669
|
historyIndex: i,
|
|
665
670
|
snippet: snippet,
|
|
666
671
|
role: entry.type === "user_message" ? "user" : "assistant",
|
|
672
|
+
ts: entry._ts || null,
|
|
667
673
|
});
|
|
668
674
|
}
|
|
669
675
|
}
|
|
@@ -773,6 +779,7 @@ function createSessionManager(opts) {
|
|
|
773
779
|
saveSessionFile(session);
|
|
774
780
|
return { ok: true };
|
|
775
781
|
},
|
|
782
|
+
permissionRequestIndex: permissionRequestIndex,
|
|
776
783
|
};
|
|
777
784
|
}
|
|
778
785
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
function attachAuth(deps) {
|
|
4
|
+
var loadUsers = deps.loadUsers;
|
|
5
|
+
var saveUsers = deps.saveUsers;
|
|
6
|
+
var findAdmin = deps.findAdmin;
|
|
7
|
+
|
|
8
|
+
// --- Multi-user mode ---
|
|
9
|
+
|
|
10
|
+
function isMultiUser() {
|
|
11
|
+
var data = loadUsers();
|
|
12
|
+
return !!data.multiUser;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function enableMultiUser() {
|
|
16
|
+
var data = loadUsers();
|
|
17
|
+
if (data.multiUser) {
|
|
18
|
+
// Already enabled -- check if admin exists
|
|
19
|
+
var admin = findAdmin(data);
|
|
20
|
+
if (admin) {
|
|
21
|
+
return { alreadyEnabled: true, hasAdmin: true, setupCode: null };
|
|
22
|
+
}
|
|
23
|
+
// Multi-user enabled but no admin -- regenerate setup code
|
|
24
|
+
var code = generateSetupCode();
|
|
25
|
+
data.setupCode = code;
|
|
26
|
+
saveUsers(data);
|
|
27
|
+
return { alreadyEnabled: true, hasAdmin: false, setupCode: code };
|
|
28
|
+
}
|
|
29
|
+
var code = generateSetupCode();
|
|
30
|
+
data.multiUser = true;
|
|
31
|
+
data.setupCode = code;
|
|
32
|
+
saveUsers(data);
|
|
33
|
+
return { alreadyEnabled: false, hasAdmin: false, setupCode: code };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function disableMultiUser() {
|
|
37
|
+
var data = loadUsers();
|
|
38
|
+
data.multiUser = false;
|
|
39
|
+
data.setupCode = null;
|
|
40
|
+
saveUsers(data);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Setup code ---
|
|
44
|
+
|
|
45
|
+
function generateSetupCode() {
|
|
46
|
+
var chars = "abcdefghijkmnpqrstuvwxyz23456789"; // no ambiguous chars
|
|
47
|
+
var code = "";
|
|
48
|
+
var bytes = crypto.randomBytes(6);
|
|
49
|
+
for (var i = 0; i < 6; i++) {
|
|
50
|
+
code += chars[bytes[i] % chars.length];
|
|
51
|
+
}
|
|
52
|
+
return code;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getSetupCode() {
|
|
56
|
+
var data = loadUsers();
|
|
57
|
+
if (data.setupCode) return data.setupCode;
|
|
58
|
+
// Defensive: if multi-user is on, no admin, and no code, auto-generate one
|
|
59
|
+
if (data.multiUser && !findAdmin(data)) {
|
|
60
|
+
var code = generateSetupCode();
|
|
61
|
+
data.setupCode = code;
|
|
62
|
+
saveUsers(data);
|
|
63
|
+
return code;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function clearSetupCode() {
|
|
69
|
+
var data = loadUsers();
|
|
70
|
+
data.setupCode = null;
|
|
71
|
+
saveUsers(data);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validateSetupCode(code) {
|
|
75
|
+
var data = loadUsers();
|
|
76
|
+
if (!data.setupCode) return false;
|
|
77
|
+
return data.setupCode === code;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Pin hashing ---
|
|
81
|
+
|
|
82
|
+
function hashPin(pin) {
|
|
83
|
+
return crypto.createHash("sha256").update("clay-user:" + pin).digest("hex");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Generate a random 6-digit PIN
|
|
87
|
+
function generatePin() {
|
|
88
|
+
var digits = "";
|
|
89
|
+
var bytes = crypto.randomBytes(6);
|
|
90
|
+
for (var i = 0; i < 6; i++) {
|
|
91
|
+
digits += (bytes[i] % 10).toString();
|
|
92
|
+
}
|
|
93
|
+
return digits;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Authentication ---
|
|
97
|
+
|
|
98
|
+
function authenticateUser(username, pin) {
|
|
99
|
+
var data = loadUsers();
|
|
100
|
+
var user = null;
|
|
101
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
102
|
+
if (data.users[i].username.toLowerCase() === username.toLowerCase()) {
|
|
103
|
+
user = data.users[i];
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!user) return null;
|
|
108
|
+
var pinH = hashPin(pin);
|
|
109
|
+
if (user.pinHash !== pinH) return null;
|
|
110
|
+
return user;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Auth tokens ---
|
|
114
|
+
|
|
115
|
+
function generateUserAuthToken(userId) {
|
|
116
|
+
var token = crypto.randomBytes(32).toString("hex");
|
|
117
|
+
return userId + ":" + token;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseAuthCookie(cookieValue) {
|
|
121
|
+
if (!cookieValue) return null;
|
|
122
|
+
var idx = cookieValue.indexOf(":");
|
|
123
|
+
if (idx < 0) return null;
|
|
124
|
+
return {
|
|
125
|
+
userId: cookieValue.substring(0, idx),
|
|
126
|
+
token: cookieValue.substring(idx + 1),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
isMultiUser: isMultiUser,
|
|
132
|
+
enableMultiUser: enableMultiUser,
|
|
133
|
+
disableMultiUser: disableMultiUser,
|
|
134
|
+
generateSetupCode: generateSetupCode,
|
|
135
|
+
getSetupCode: getSetupCode,
|
|
136
|
+
clearSetupCode: clearSetupCode,
|
|
137
|
+
validateSetupCode: validateSetupCode,
|
|
138
|
+
hashPin: hashPin,
|
|
139
|
+
generatePin: generatePin,
|
|
140
|
+
authenticateUser: authenticateUser,
|
|
141
|
+
generateUserAuthToken: generateUserAuthToken,
|
|
142
|
+
parseAuthCookie: parseAuthCookie,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { attachAuth: attachAuth };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// --- Per-user RBAC permissions (default values for regular users) ---
|
|
2
|
+
var DEFAULT_PERMISSIONS = {
|
|
3
|
+
terminal: false,
|
|
4
|
+
fileBrowser: true,
|
|
5
|
+
createProject: true,
|
|
6
|
+
deleteProject: false,
|
|
7
|
+
skills: true,
|
|
8
|
+
sessionDelete: false,
|
|
9
|
+
scheduledTasks: false,
|
|
10
|
+
projectSettings: false,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
var ALL_PERMISSIONS = {
|
|
14
|
+
terminal: true,
|
|
15
|
+
fileBrowser: true,
|
|
16
|
+
createProject: true,
|
|
17
|
+
deleteProject: true,
|
|
18
|
+
skills: true,
|
|
19
|
+
sessionDelete: true,
|
|
20
|
+
scheduledTasks: true,
|
|
21
|
+
projectSettings: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function attachPermissions(deps) {
|
|
25
|
+
var loadUsers = deps.loadUsers;
|
|
26
|
+
var saveUsers = deps.saveUsers;
|
|
27
|
+
var findUserById = deps.findUserById;
|
|
28
|
+
|
|
29
|
+
function getEffectivePermissions(user, osUsersMode) {
|
|
30
|
+
// OS-mode users with linuxUser are exempt from RBAC (OS handles isolation)
|
|
31
|
+
if (osUsersMode && user && user.linuxUser) return ALL_PERMISSIONS;
|
|
32
|
+
// Admin always has full permissions
|
|
33
|
+
if (user && user.role === "admin") return ALL_PERMISSIONS;
|
|
34
|
+
// Merge stored permissions with defaults (handles missing keys for forward-compat)
|
|
35
|
+
var stored = (user && user.permissions) || {};
|
|
36
|
+
var result = {};
|
|
37
|
+
var keys = Object.keys(DEFAULT_PERMISSIONS);
|
|
38
|
+
for (var i = 0; i < keys.length; i++) {
|
|
39
|
+
var k = keys[i];
|
|
40
|
+
result[k] = stored[k] !== undefined ? stored[k] : DEFAULT_PERMISSIONS[k];
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function updateUserPermissions(userId, permissions) {
|
|
46
|
+
var data = loadUsers();
|
|
47
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
48
|
+
if (data.users[i].id === userId) {
|
|
49
|
+
// Validate: only allow known permission keys with boolean values
|
|
50
|
+
var clean = {};
|
|
51
|
+
var keys = Object.keys(DEFAULT_PERMISSIONS);
|
|
52
|
+
for (var j = 0; j < keys.length; j++) {
|
|
53
|
+
var k = keys[j];
|
|
54
|
+
clean[k] = permissions[k] === true;
|
|
55
|
+
}
|
|
56
|
+
data.users[i].permissions = clean;
|
|
57
|
+
saveUsers(data);
|
|
58
|
+
return { ok: true, permissions: clean };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { error: "User not found" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- Project access helpers ---
|
|
65
|
+
|
|
66
|
+
function canAccessProject(userId, project) {
|
|
67
|
+
if (!project) return false;
|
|
68
|
+
// Public projects are accessible to all authenticated users
|
|
69
|
+
if (!project.visibility || project.visibility === "public") return true;
|
|
70
|
+
// Admin always has access
|
|
71
|
+
var user = findUserById(userId);
|
|
72
|
+
if (user && user.role === "admin") return true;
|
|
73
|
+
// Owner always has access to their own project
|
|
74
|
+
if (project.ownerId && project.ownerId === userId) return true;
|
|
75
|
+
// Private project -- check allowedUsers
|
|
76
|
+
var allowed = project.allowedUsers || [];
|
|
77
|
+
return allowed.indexOf(userId) >= 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getAccessibleProjects(userId, projects) {
|
|
81
|
+
if (!projects) return [];
|
|
82
|
+
return projects.filter(function (p) {
|
|
83
|
+
return canAccessProject(userId, p);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Session visibility helpers ---
|
|
88
|
+
|
|
89
|
+
function canAccessSession(userId, session, project) {
|
|
90
|
+
// Must have project access first
|
|
91
|
+
if (!canAccessProject(userId, project)) return false;
|
|
92
|
+
// Sessions without ownerId are legacy -- only admin can see them
|
|
93
|
+
if (!session.ownerId) {
|
|
94
|
+
var user = findUserById(userId);
|
|
95
|
+
return !!(user && user.role === "admin");
|
|
96
|
+
}
|
|
97
|
+
// Owner can always see their own sessions
|
|
98
|
+
if (session.ownerId === userId) return true;
|
|
99
|
+
// Shared sessions are visible to all project members (default)
|
|
100
|
+
if (!session.sessionVisibility || session.sessionVisibility === "shared") return true;
|
|
101
|
+
// Private sessions are only visible to the owner
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
getEffectivePermissions: getEffectivePermissions,
|
|
107
|
+
updateUserPermissions: updateUserPermissions,
|
|
108
|
+
canAccessProject: canAccessProject,
|
|
109
|
+
getAccessibleProjects: getAccessibleProjects,
|
|
110
|
+
canAccessSession: canAccessSession,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
DEFAULT_PERMISSIONS: DEFAULT_PERMISSIONS,
|
|
116
|
+
ALL_PERMISSIONS: ALL_PERMISSIONS,
|
|
117
|
+
attachPermissions: attachPermissions,
|
|
118
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
function attachPreferences(deps) {
|
|
2
|
+
var loadUsers = deps.loadUsers;
|
|
3
|
+
var saveUsers = deps.saveUsers;
|
|
4
|
+
|
|
5
|
+
// --- DM Favorites ---
|
|
6
|
+
|
|
7
|
+
function getDmFavorites(userId) {
|
|
8
|
+
var data = loadUsers();
|
|
9
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
10
|
+
if (data.users[i].id === userId) {
|
|
11
|
+
return data.users[i].dmFavorites || [];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function addDmFavorite(userId, targetUserId) {
|
|
18
|
+
var data = loadUsers();
|
|
19
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
20
|
+
if (data.users[i].id === userId) {
|
|
21
|
+
if (!data.users[i].dmFavorites) data.users[i].dmFavorites = [];
|
|
22
|
+
if (data.users[i].dmFavorites.indexOf(targetUserId) === -1) {
|
|
23
|
+
data.users[i].dmFavorites.push(targetUserId);
|
|
24
|
+
saveUsers(data);
|
|
25
|
+
}
|
|
26
|
+
return data.users[i].dmFavorites;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function removeDmFavorite(userId, targetUserId) {
|
|
33
|
+
var data = loadUsers();
|
|
34
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
35
|
+
if (data.users[i].id === userId) {
|
|
36
|
+
if (!data.users[i].dmFavorites) data.users[i].dmFavorites = [];
|
|
37
|
+
data.users[i].dmFavorites = data.users[i].dmFavorites.filter(function (id) {
|
|
38
|
+
return id !== targetUserId;
|
|
39
|
+
});
|
|
40
|
+
saveUsers(data);
|
|
41
|
+
return data.users[i].dmFavorites;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- DM Hidden (dismissed from strip) ---
|
|
48
|
+
|
|
49
|
+
function getDmHidden(userId) {
|
|
50
|
+
var data = loadUsers();
|
|
51
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
52
|
+
if (data.users[i].id === userId) {
|
|
53
|
+
return data.users[i].dmHidden || [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function addDmHidden(userId, targetUserId) {
|
|
60
|
+
var data = loadUsers();
|
|
61
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
62
|
+
if (data.users[i].id === userId) {
|
|
63
|
+
if (!data.users[i].dmHidden) data.users[i].dmHidden = [];
|
|
64
|
+
if (data.users[i].dmHidden.indexOf(targetUserId) === -1) {
|
|
65
|
+
data.users[i].dmHidden.push(targetUserId);
|
|
66
|
+
saveUsers(data);
|
|
67
|
+
}
|
|
68
|
+
return data.users[i].dmHidden;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function removeDmHidden(userId, targetUserId) {
|
|
75
|
+
var data = loadUsers();
|
|
76
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
77
|
+
if (data.users[i].id === userId) {
|
|
78
|
+
if (!data.users[i].dmHidden) data.users[i].dmHidden = [];
|
|
79
|
+
data.users[i].dmHidden = data.users[i].dmHidden.filter(function (id) {
|
|
80
|
+
return id !== targetUserId;
|
|
81
|
+
});
|
|
82
|
+
saveUsers(data);
|
|
83
|
+
return data.users[i].dmHidden;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- Deleted built-in mate keys tracking ---
|
|
90
|
+
|
|
91
|
+
function getDeletedBuiltinKeys(userId) {
|
|
92
|
+
var data = loadUsers();
|
|
93
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
94
|
+
if (data.users[i].id === userId) {
|
|
95
|
+
return data.users[i].deletedBuiltinKeys || [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function addDeletedBuiltinKey(userId, key) {
|
|
102
|
+
var data = loadUsers();
|
|
103
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
104
|
+
if (data.users[i].id === userId) {
|
|
105
|
+
if (!data.users[i].deletedBuiltinKeys) data.users[i].deletedBuiltinKeys = [];
|
|
106
|
+
if (data.users[i].deletedBuiltinKeys.indexOf(key) === -1) {
|
|
107
|
+
data.users[i].deletedBuiltinKeys.push(key);
|
|
108
|
+
saveUsers(data);
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function removeDeletedBuiltinKey(userId, key) {
|
|
116
|
+
var data = loadUsers();
|
|
117
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
118
|
+
if (data.users[i].id === userId) {
|
|
119
|
+
if (!data.users[i].deletedBuiltinKeys) return;
|
|
120
|
+
data.users[i].deletedBuiltinKeys = data.users[i].deletedBuiltinKeys.filter(function (k) {
|
|
121
|
+
return k !== key;
|
|
122
|
+
});
|
|
123
|
+
saveUsers(data);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- Per-user chat layout setting ---
|
|
130
|
+
|
|
131
|
+
function getChatLayout(userId) {
|
|
132
|
+
var data = loadUsers();
|
|
133
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
134
|
+
if (data.users[i].id === userId) {
|
|
135
|
+
return data.users[i].chatLayout || "channel";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return "channel";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function setChatLayout(userId, layout) {
|
|
142
|
+
var val = (layout === "bubble") ? "bubble" : "channel";
|
|
143
|
+
var data = loadUsers();
|
|
144
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
145
|
+
if (data.users[i].id === userId) {
|
|
146
|
+
data.users[i].chatLayout = val;
|
|
147
|
+
saveUsers(data);
|
|
148
|
+
return { ok: true, chatLayout: val };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { error: "User not found" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- Per-user auto-continue setting ---
|
|
155
|
+
|
|
156
|
+
function getAutoContinue(userId) {
|
|
157
|
+
var data = loadUsers();
|
|
158
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
159
|
+
if (data.users[i].id === userId) {
|
|
160
|
+
return !!data.users[i].autoContinueOnRateLimit;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function setAutoContinue(userId, enabled) {
|
|
167
|
+
var data = loadUsers();
|
|
168
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
169
|
+
if (data.users[i].id === userId) {
|
|
170
|
+
data.users[i].autoContinueOnRateLimit = !!enabled;
|
|
171
|
+
saveUsers(data);
|
|
172
|
+
return { ok: true, autoContinueOnRateLimit: !!enabled };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { error: "User not found" };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- Mate onboarding ---
|
|
179
|
+
|
|
180
|
+
function setMateOnboarded(userId) {
|
|
181
|
+
var data = loadUsers();
|
|
182
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
183
|
+
if (data.users[i].id === userId) {
|
|
184
|
+
data.users[i].mateOnboardingShown = true;
|
|
185
|
+
saveUsers(data);
|
|
186
|
+
return { ok: true };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { error: "User not found" };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
getDmFavorites: getDmFavorites,
|
|
194
|
+
addDmFavorite: addDmFavorite,
|
|
195
|
+
removeDmFavorite: removeDmFavorite,
|
|
196
|
+
getDmHidden: getDmHidden,
|
|
197
|
+
addDmHidden: addDmHidden,
|
|
198
|
+
removeDmHidden: removeDmHidden,
|
|
199
|
+
getDeletedBuiltinKeys: getDeletedBuiltinKeys,
|
|
200
|
+
addDeletedBuiltinKey: addDeletedBuiltinKey,
|
|
201
|
+
removeDeletedBuiltinKey: removeDeletedBuiltinKey,
|
|
202
|
+
getChatLayout: getChatLayout,
|
|
203
|
+
setChatLayout: setChatLayout,
|
|
204
|
+
getAutoContinue: getAutoContinue,
|
|
205
|
+
setAutoContinue: setAutoContinue,
|
|
206
|
+
setMateOnboarded: setMateOnboarded,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { attachPreferences: attachPreferences };
|