@stage-labs/metro 0.1.0-beta.152 → 0.1.0-beta.153
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/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/core/package.json +2 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +58 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +3 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-tool.ts +92 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +5 -1
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +2 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +48 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +2 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +2 -0
- package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +36 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +2 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +2 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +33 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +2 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +13 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +12 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +2 -0
- package/runtime/runtime.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.153",
|
|
4
4
|
"description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.153",
|
|
4
4
|
"description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"./stations/station-runtime": "./src/stations/station-runtime.ts",
|
|
24
24
|
"./stations/account-store": "./src/stations/account-store.ts",
|
|
25
25
|
"./stations/attachments": "./src/stations/attachments.ts",
|
|
26
|
-
"./stations/messaging-normalize": "./src/stations/messaging-normalize.ts"
|
|
26
|
+
"./stations/messaging-normalize": "./src/stations/messaging-normalize.ts",
|
|
27
|
+
"./stations/profile": "./src/stations/profile.ts"
|
|
27
28
|
},
|
|
28
29
|
"scripts": {
|
|
29
30
|
"build": "tsc",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { TrainError } from '../train-error.js';
|
|
2
|
+
|
|
3
|
+
export const PROFILE_FIELDS = ['name', 'bio', 'avatar'] as const;
|
|
4
|
+
export type ProfileField = (typeof PROFILE_FIELDS)[number];
|
|
5
|
+
|
|
6
|
+
export const NAME_MAX = 64;
|
|
7
|
+
export const BIO_MAX = 512;
|
|
8
|
+
|
|
9
|
+
export interface ProfileAvatar {
|
|
10
|
+
path: string;
|
|
11
|
+
mime: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProfileChange {
|
|
16
|
+
name?: string;
|
|
17
|
+
bio?: string;
|
|
18
|
+
avatar?: ProfileAvatar;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ProfileApplied {
|
|
22
|
+
account: string;
|
|
23
|
+
applied: ProfileField[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const text = (value: unknown, field: string, max: number): string | undefined => {
|
|
27
|
+
if (value === undefined) return undefined;
|
|
28
|
+
if (typeof value !== 'string') throw new TrainError('bad_request', `${field} must be a string`);
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
if (trimmed === '') throw new TrainError('bad_request', `${field} cannot be empty`);
|
|
31
|
+
if (trimmed.length > max) throw new TrainError('bad_request', `${field} is over ${String(max)} characters`);
|
|
32
|
+
return trimmed;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function avatarOf(value: unknown): ProfileAvatar | undefined {
|
|
36
|
+
if (value === undefined) return undefined;
|
|
37
|
+
const raw = (value ?? {}) as Record<string, unknown>;
|
|
38
|
+
if (typeof raw.path !== 'string' || raw.path === '') throw new TrainError('bad_request', 'avatar needs a local file');
|
|
39
|
+
return { path: raw.path, mime: typeof raw.mime === 'string' ? raw.mime : 'application/octet-stream', name: typeof raw.name === 'string' ? raw.name : 'avatar' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseProfileChange(args: Record<string, unknown>): ProfileChange {
|
|
43
|
+
const change: ProfileChange = {};
|
|
44
|
+
const name = text(args.name, 'name', NAME_MAX);
|
|
45
|
+
const bio = text(args.bio, 'bio', BIO_MAX);
|
|
46
|
+
const avatar = avatarOf(args.avatar);
|
|
47
|
+
if (name !== undefined) change.name = name;
|
|
48
|
+
if (bio !== undefined) change.bio = bio;
|
|
49
|
+
if (avatar !== undefined) change.avatar = avatar;
|
|
50
|
+
if (Object.keys(change).length === 0) throw new TrainError('bad_request', 'set_profile needs at least one of name, bio, avatar');
|
|
51
|
+
return change;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const fieldsOf = (change: ProfileChange): ProfileField[] => PROFILE_FIELDS.filter((f) => change[f] !== undefined);
|
|
55
|
+
|
|
56
|
+
export const assertImage = (avatar: ProfileAvatar): void => {
|
|
57
|
+
if (!avatar.mime.startsWith('image/')) throw new TrainError('bad_request', `avatar must be an image, not ${avatar.mime}`);
|
|
58
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ProfileField } from './profile.js';
|
|
2
|
+
|
|
1
3
|
export type Verb =
|
|
2
4
|
| 'send'
|
|
3
5
|
| 'reply'
|
|
@@ -98,6 +100,7 @@ export interface Station {
|
|
|
98
100
|
messageVerbs: ReadonlySet<Verb>;
|
|
99
101
|
groupOps?: ReadonlySet<GroupOp>;
|
|
100
102
|
resolvesSenders?: boolean;
|
|
103
|
+
profileFields?: ReadonlySet<ProfileField>;
|
|
101
104
|
attachmentMode: AttachmentMode;
|
|
102
105
|
sendAttachments?(
|
|
103
106
|
line: string,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { CanonicalAttachment, Station, ToolResult } from '@metro-labs/core/stations/types';
|
|
2
|
+
import { PROFILE_FIELDS, type ProfileField } from '@metro-labs/core/stations/profile';
|
|
3
|
+
import { str } from '@metro-labs/core/str';
|
|
4
|
+
import { STATIONS, stationByName } from '../stations/registry.js';
|
|
5
|
+
import { cleanupAttachments, resolveAttachments, type ResolvedAttachment } from '../stations/attach-resolve.js';
|
|
6
|
+
import { errResult, makeCtx, okJson, toErr } from './ctx.js';
|
|
7
|
+
import { allowedAgents, currentIdentity } from './request-identity.js';
|
|
8
|
+
|
|
9
|
+
const SOURCE = { type: 'string' };
|
|
10
|
+
|
|
11
|
+
export const SET_PROFILE_TOOL = {
|
|
12
|
+
name: 'set_profile',
|
|
13
|
+
description:
|
|
14
|
+
'Change how one of your accounts presents itself on its network: the display name, the bio ' +
|
|
15
|
+
'(about line) and the avatar. Args: station (required), account? (which account of that station; ' +
|
|
16
|
+
'needed only when you have several), name?, bio?, avatar? (an image given like an attachment: ' +
|
|
17
|
+
'exactly one of upload, url, path, data, plus mime and name). At least one of name, bio, avatar. ' +
|
|
18
|
+
'Each station takes what its network allows (`list_accounts` reports the fields under ' +
|
|
19
|
+
'`profiles`); a field the station cannot set is refused by name, never dropped. Discord changes ' +
|
|
20
|
+
'the bot username and app description (Discord allows two username changes an hour); Telegram ' +
|
|
21
|
+
'bots set their name, description and photo; a Telegram user account sets its first name, bio ' +
|
|
22
|
+
'(70 chars) and photo; WhatsApp sets the profile name, about and picture. Visible to everyone ' +
|
|
23
|
+
'who talks to the account, so do it when asked, not on your own. Returns {account, applied}.',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
station: { type: 'string', description: 'discord-bot | telegram-bot | telegram | whatsapp' },
|
|
28
|
+
account: { type: 'string', description: 'The account id on that station, from list_accounts.' },
|
|
29
|
+
name: { type: 'string', description: 'The display name.' },
|
|
30
|
+
bio: { type: 'string', description: 'The bio or about line.' },
|
|
31
|
+
avatar: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
description: 'The new picture, as an attachment source.',
|
|
34
|
+
properties: { upload: SOURCE, url: SOURCE, path: SOURCE, data: SOURCE, mime: SOURCE, name: SOURCE },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['station'],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const profileCapabilities = (): Record<string, ProfileField[]> => {
|
|
42
|
+
const out: Record<string, ProfileField[]> = {};
|
|
43
|
+
for (const s of STATIONS) if (s.profileFields !== undefined) out[s.name] = [...s.profileFields];
|
|
44
|
+
return out;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const asked = (a: Record<string, unknown>): ProfileField[] => PROFILE_FIELDS.filter((f) => a[f] !== undefined);
|
|
48
|
+
|
|
49
|
+
type Target = { station: Station; fields: ProfileField[] } | { refused: string };
|
|
50
|
+
|
|
51
|
+
function targetOf(a: Record<string, unknown>): Target {
|
|
52
|
+
const name = str(a.station);
|
|
53
|
+
if (!name) return { refused: 'set_profile requires `station`' };
|
|
54
|
+
const station = stationByName(name);
|
|
55
|
+
if (!station) return { refused: `no station named ${name}` };
|
|
56
|
+
const fields = station.profileFields;
|
|
57
|
+
if (fields === undefined) return { refused: `${name} accounts have no profile metro can set` };
|
|
58
|
+
const wanted = asked(a);
|
|
59
|
+
if (wanted.length === 0) return { refused: 'set_profile needs at least one of name, bio, avatar' };
|
|
60
|
+
const refused = wanted.filter((f) => !fields.has(f));
|
|
61
|
+
if (refused.length > 0) return { refused: `${name} cannot set ${refused.join(', ')}; it takes ${[...fields].join(', ')}` };
|
|
62
|
+
return { station, fields: wanted };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function trainArgs(a: Record<string, unknown>, picture: ResolvedAttachment | undefined): Record<string, unknown> {
|
|
66
|
+
const args: Record<string, unknown> = {};
|
|
67
|
+
if (typeof a.account === 'string') args.account = a.account;
|
|
68
|
+
if (typeof a.name === 'string') args.name = a.name;
|
|
69
|
+
if (typeof a.bio === 'string') args.bio = a.bio;
|
|
70
|
+
if (picture !== undefined) args.avatar = { path: picture.path, mime: picture.mime, name: picture.name };
|
|
71
|
+
return args;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function dispatchSetProfile(a: Record<string, unknown>): Promise<ToolResult> {
|
|
75
|
+
const target = targetOf(a);
|
|
76
|
+
if ('refused' in target) return errResult(target.refused);
|
|
77
|
+
const avatar = a.avatar === undefined ? [] : [a.avatar as CanonicalAttachment];
|
|
78
|
+
let resolved: ResolvedAttachment[];
|
|
79
|
+
try {
|
|
80
|
+
resolved = await resolveAttachments(avatar, { allowed: allowedAgents(currentIdentity()) });
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return toErr('set_profile', e);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const { result } = await makeCtx(target.station.name).call('set_profile', trainArgs(a, resolved[0]));
|
|
86
|
+
return okJson(result);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return toErr('set_profile', e);
|
|
89
|
+
} finally {
|
|
90
|
+
await cleanupAttachments(resolved);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
dispatchRemoveMembers,
|
|
22
22
|
} from './group-tools.js';
|
|
23
23
|
import { dispatchCreateUpload } from './upload-tool.js';
|
|
24
|
+
import { dispatchSetProfile, profileCapabilities, SET_PROFILE_TOOL } from './profile-tool.js';
|
|
24
25
|
import { callTargetDenied, lineTargetDenied } from '../agents/scope.js';
|
|
25
26
|
import { SOURCE_KEYS } from '../stations/attach-resolve.js';
|
|
26
27
|
import { decodedLengthOf } from '../stations/attach-inline.js';
|
|
@@ -49,6 +50,7 @@ const CORE_DISPATCH: Record<
|
|
|
49
50
|
remove_members: dispatchRemoveMembers,
|
|
50
51
|
export_invite: dispatchInviteLink,
|
|
51
52
|
create_upload: dispatchCreateUpload,
|
|
53
|
+
set_profile: dispatchSetProfile,
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
const toolList = (): { tools: unknown[] } => ({
|
|
@@ -62,6 +64,7 @@ const toolList = (): { tools: unknown[] } => ({
|
|
|
62
64
|
})),
|
|
63
65
|
),
|
|
64
66
|
LIST_ACCOUNTS_TOOL,
|
|
67
|
+
SET_PROFILE_TOOL,
|
|
65
68
|
],
|
|
66
69
|
});
|
|
67
70
|
|
|
@@ -82,6 +85,7 @@ async function handleListAccounts(
|
|
|
82
85
|
return okJson({
|
|
83
86
|
accounts: await gatherAccounts(allowedAgents(identity)),
|
|
84
87
|
capabilities: accountStationCapabilities(),
|
|
88
|
+
profiles: profileCapabilities(),
|
|
85
89
|
});
|
|
86
90
|
} catch (e) {
|
|
87
91
|
return errResult(`metro list_accounts failed: ${String(e)}`);
|
|
@@ -92,7 +96,7 @@ function stationForTool(
|
|
|
92
96
|
name: string,
|
|
93
97
|
args: Record<string, unknown>,
|
|
94
98
|
): string | undefined {
|
|
95
|
-
if (name === 'create_group') return str(args.station) || undefined;
|
|
99
|
+
if (name === 'create_group' || name === 'set_profile') return str(args.station) || undefined;
|
|
96
100
|
return STATION_TOOLS.get(name)?.station.name;
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from '@metro-labs/core/stations/station-runtime';
|
|
28
28
|
import { joinVoice, leaveVoice, voiceDebug, voiceTranscribe } from './voice.js';
|
|
29
29
|
import { speak } from './voice-speak.js';
|
|
30
|
+
import { setProfile } from './profile.js';
|
|
30
31
|
import { discordMembers } from './members.js';
|
|
31
32
|
import {
|
|
32
33
|
groupAddHandler,
|
|
@@ -375,6 +376,7 @@ const HANDLERS: Record<string, StationHandler> = {
|
|
|
375
376
|
channel,
|
|
376
377
|
listMembers,
|
|
377
378
|
set_presence: presence,
|
|
379
|
+
set_profile: setProfile,
|
|
378
380
|
joinVoice,
|
|
379
381
|
leaveVoice,
|
|
380
382
|
speak,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import type { Client } from 'discord.js';
|
|
3
|
+
import { TrainError } from '@metro-labs/core/train-error';
|
|
4
|
+
import {
|
|
5
|
+
assertImage,
|
|
6
|
+
fieldsOf,
|
|
7
|
+
parseProfileChange,
|
|
8
|
+
type ProfileApplied,
|
|
9
|
+
type ProfileChange,
|
|
10
|
+
} from '@metro-labs/core/stations/profile';
|
|
11
|
+
import { accountFor, accounts } from './accounts.js';
|
|
12
|
+
import { respond } from './wire.js';
|
|
13
|
+
|
|
14
|
+
export async function applyProfile(
|
|
15
|
+
client: Client,
|
|
16
|
+
change: ProfileChange,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const user = client.user;
|
|
19
|
+
if (!user) throw new TrainError('discord_not_ready', 'the gateway is not ready yet');
|
|
20
|
+
if (change.avatar !== undefined) assertImage(change.avatar);
|
|
21
|
+
const avatar = change.avatar === undefined ? undefined : await readFile(change.avatar.path);
|
|
22
|
+
if (change.name !== undefined || avatar !== undefined)
|
|
23
|
+
await user.edit({
|
|
24
|
+
...(change.name === undefined ? {} : { username: change.name }),
|
|
25
|
+
...(avatar === undefined ? {} : { avatar }),
|
|
26
|
+
});
|
|
27
|
+
if (change.bio !== undefined) {
|
|
28
|
+
const app = client.application;
|
|
29
|
+
if (!app) throw new TrainError('discord_not_ready', 'the application is not loaded yet');
|
|
30
|
+
await app.edit({ description: change.bio });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function setProfile(
|
|
35
|
+
id: string,
|
|
36
|
+
args: Record<string, unknown>,
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
const accountId = accountFor({ account: args.account as string | undefined });
|
|
39
|
+
const acct = accounts.get(accountId);
|
|
40
|
+
if (!acct) {
|
|
41
|
+
respond(id, { error: `unknown account '${accountId}'` });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const change = parseProfileChange(args);
|
|
45
|
+
await applyProfile(acct.client, change);
|
|
46
|
+
const result: ProfileApplied = { account: accountId, applied: fieldsOf(change) };
|
|
47
|
+
respond(id, { result });
|
|
48
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GroupOp, Station, Verb } from '@metro-labs/core/stations/types';
|
|
2
|
+
import { PROFILE_FIELDS, type ProfileField } from '@metro-labs/core/stations/profile';
|
|
2
3
|
|
|
3
4
|
export const discordBotStation: Station = {
|
|
4
5
|
name: 'discord-bot',
|
|
@@ -15,5 +16,6 @@ export const discordBotStation: Station = {
|
|
|
15
16
|
]),
|
|
16
17
|
groupOps: new Set<GroupOp>(['create_group', 'add_members', 'remove_members']),
|
|
17
18
|
attachmentMode: 'canonical',
|
|
19
|
+
profileFields: new Set<ProfileField>(PROFILE_FIELDS),
|
|
18
20
|
tools: [],
|
|
19
21
|
};
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type CanonicalAttachment,
|
|
15
15
|
} from './media-actions.js';
|
|
16
16
|
import { clampLimit, shapeHistory } from './history.js';
|
|
17
|
+
import { makeSetProfile } from './profile.js';
|
|
17
18
|
import { fetchMembers, isRestricted, restrictedMemberList } from './members.js';
|
|
18
19
|
import {
|
|
19
20
|
groupAddMembers,
|
|
@@ -248,6 +249,7 @@ export function makeHandleCall(
|
|
|
248
249
|
groupAddMembers: makeGroupAdd(clientFor),
|
|
249
250
|
groupRemoveMembers: makeGroupRemove(clientFor),
|
|
250
251
|
groupInviteLink: makeGroupInvite(clientFor),
|
|
252
|
+
set_profile: makeSetProfile(clientFor),
|
|
251
253
|
},
|
|
252
254
|
normalize: normalizeTelegramUser,
|
|
253
255
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TelegramClient } from '@mtcute/bun';
|
|
2
|
+
import {
|
|
3
|
+
assertImage,
|
|
4
|
+
fieldsOf,
|
|
5
|
+
parseProfileChange,
|
|
6
|
+
type ProfileApplied,
|
|
7
|
+
type ProfileChange,
|
|
8
|
+
} from '@metro-labs/core/stations/profile';
|
|
9
|
+
import { respond, type StationHandler } from '@metro-labs/core/stations/station-runtime';
|
|
10
|
+
import { str } from '@metro-labs/core/str';
|
|
11
|
+
import { accountFor } from './accounts.js';
|
|
12
|
+
import type { UserClient } from './client.js';
|
|
13
|
+
|
|
14
|
+
const BIO_MAX = 70;
|
|
15
|
+
|
|
16
|
+
export async function applyProfile(tg: TelegramClient, change: ProfileChange): Promise<void> {
|
|
17
|
+
if (change.name !== undefined || change.bio !== undefined)
|
|
18
|
+
await tg.updateProfile({
|
|
19
|
+
...(change.name === undefined ? {} : { firstName: change.name }),
|
|
20
|
+
...(change.bio === undefined ? {} : { bio: change.bio.slice(0, BIO_MAX) }),
|
|
21
|
+
});
|
|
22
|
+
if (change.avatar !== undefined) {
|
|
23
|
+
assertImage(change.avatar);
|
|
24
|
+
await tg.setMyProfilePhoto({ type: 'photo', media: `file:${change.avatar.path}` });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function makeSetProfile(clientFor: (accountId: string) => UserClient): StationHandler {
|
|
29
|
+
return async (id, args) => {
|
|
30
|
+
const accountId = accountFor({ account: str(args.account) });
|
|
31
|
+
const change = parseProfileChange(args);
|
|
32
|
+
await applyProfile(clientFor(accountId).tg, change);
|
|
33
|
+
const result: ProfileApplied = { account: accountId, applied: fieldsOf(change) };
|
|
34
|
+
respond(id, { result });
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GroupOp, Station, Verb } from '@metro-labs/core/stations/types';
|
|
2
|
+
import { PROFILE_FIELDS, type ProfileField } from '@metro-labs/core/stations/profile';
|
|
2
3
|
|
|
3
4
|
export const telegramStation: Station = {
|
|
4
5
|
name: 'telegram',
|
|
@@ -20,5 +21,6 @@ export const telegramStation: Station = {
|
|
|
20
21
|
'invite_link',
|
|
21
22
|
]),
|
|
22
23
|
attachmentMode: 'canonical',
|
|
24
|
+
profileFields: new Set<ProfileField>(PROFILE_FIELDS),
|
|
23
25
|
tools: [],
|
|
24
26
|
};
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
sendLocation,
|
|
24
24
|
sendMedia,
|
|
25
25
|
} from './media-actions.js';
|
|
26
|
+
import { setProfile } from './profile.js';
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
|
|
@@ -271,6 +272,7 @@ const HANDLERS: Record<string, StationHandler> = {
|
|
|
271
272
|
edit,
|
|
272
273
|
delete: remove,
|
|
273
274
|
listMembers,
|
|
275
|
+
set_profile: setProfile,
|
|
274
276
|
send_photo: (id, args) =>
|
|
275
277
|
media(id, 'sendPhoto', 'photo', ((args.caption as string) ?? '') + ' [image]', args),
|
|
276
278
|
send_document: (id, args) =>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { appendFile } from '@metro-labs/core/stations/attachments';
|
|
2
|
+
import {
|
|
3
|
+
assertImage,
|
|
4
|
+
fieldsOf,
|
|
5
|
+
parseProfileChange,
|
|
6
|
+
type ProfileApplied,
|
|
7
|
+
type ProfileAvatar,
|
|
8
|
+
type ProfileChange,
|
|
9
|
+
} from '@metro-labs/core/stations/profile';
|
|
10
|
+
import { accountFor, tg, tgForm } from './accounts.js';
|
|
11
|
+
import { respond } from './wire.js';
|
|
12
|
+
|
|
13
|
+
async function setPhoto(accountId: string, avatar: ProfileAvatar): Promise<void> {
|
|
14
|
+
assertImage(avatar);
|
|
15
|
+
const form = new FormData();
|
|
16
|
+
form.append('photo', JSON.stringify({ type: 'static', photo: 'attach://avatar' }));
|
|
17
|
+
await appendFile(form, 'avatar', avatar.path, avatar.name);
|
|
18
|
+
await tgForm(accountId, 'setMyProfilePhoto', form);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function applyProfile(accountId: string, change: ProfileChange): Promise<void> {
|
|
22
|
+
if (change.name !== undefined) await tg(accountId, 'setMyName', { name: change.name });
|
|
23
|
+
if (change.bio !== undefined) await tg(accountId, 'setMyDescription', { description: change.bio });
|
|
24
|
+
if (change.avatar !== undefined) await setPhoto(accountId, change.avatar);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function setProfile(id: string, args: Record<string, unknown>): Promise<void> {
|
|
28
|
+
const accountId = accountFor({ account: args.account as string | undefined });
|
|
29
|
+
const change = parseProfileChange(args);
|
|
30
|
+
await applyProfile(accountId, change);
|
|
31
|
+
const result: ProfileApplied = { account: accountId, applied: fieldsOf(change) };
|
|
32
|
+
respond(id, { result });
|
|
33
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Station, Verb } from '@metro-labs/core/stations/types';
|
|
2
|
+
import { PROFILE_FIELDS, type ProfileField } from '@metro-labs/core/stations/profile';
|
|
2
3
|
|
|
3
4
|
export const telegramBotStation: Station = {
|
|
4
5
|
name: 'telegram-bot',
|
|
@@ -13,5 +14,6 @@ export const telegramBotStation: Station = {
|
|
|
13
14
|
'delete',
|
|
14
15
|
]),
|
|
15
16
|
attachmentMode: 'canonical',
|
|
17
|
+
profileFields: new Set<ProfileField>(PROFILE_FIELDS),
|
|
16
18
|
tools: [],
|
|
17
19
|
};
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { accountFor, accounts, targetOf } from './accounts.js';
|
|
11
11
|
import { normalizeWhatsApp } from './normalize.js';
|
|
12
12
|
import { phoneNumberOf, senderLookup } from './resolve.js';
|
|
13
|
+
import { assertImage, fieldsOf, parseProfileChange, type ProfileApplied } from '@metro-labs/core/stations/profile';
|
|
13
14
|
import type { WAClient } from './client.js';
|
|
14
15
|
|
|
15
16
|
type Args = Record<string, unknown>;
|
|
@@ -173,6 +174,17 @@ function makeResolveSender(clientFor: ClientFor): StationHandler {
|
|
|
173
174
|
};
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
function makeSetProfile(clientFor: ClientFor): StationHandler {
|
|
178
|
+
return async (id, args) => {
|
|
179
|
+
const accountId = accountFor({ account: str(args.account) });
|
|
180
|
+
const change = parseProfileChange(args);
|
|
181
|
+
if (change.avatar !== undefined) assertImage(change.avatar);
|
|
182
|
+
await guard(() => clientFor(accountId).setProfile(change));
|
|
183
|
+
const result: ProfileApplied = { account: accountId, applied: fieldsOf(change) };
|
|
184
|
+
respond(id, { result });
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
176
188
|
export function makeHandleCall(
|
|
177
189
|
clientFor: ClientFor,
|
|
178
190
|
): (msg: CallMsg) => Promise<void> {
|
|
@@ -184,6 +196,7 @@ export function makeHandleCall(
|
|
|
184
196
|
edit: makeEdit(clientFor),
|
|
185
197
|
delete: makeDelete(clientFor),
|
|
186
198
|
resolve_sender: makeResolveSender(clientFor),
|
|
199
|
+
set_profile: makeSetProfile(clientFor),
|
|
187
200
|
},
|
|
188
201
|
normalize: normalizeWhatsApp,
|
|
189
202
|
});
|
|
@@ -13,6 +13,7 @@ import { errMsg } from '@metro-labs/core/log';
|
|
|
13
13
|
import type { WhatsAppAccount } from './types.js';
|
|
14
14
|
import type { InboundMessage, ReactionInput } from './format.js';
|
|
15
15
|
import type { SenderFound } from './resolve.js';
|
|
16
|
+
import type { ProfileChange } from '@metro-labs/core/stations/profile';
|
|
16
17
|
import { toInbound, toReaction, type ReactionEvent, type SelfRef } from './parse.js';
|
|
17
18
|
import { baileysLogger } from './logger.js';
|
|
18
19
|
import { useAccountAuthState } from './auth-state.js';
|
|
@@ -51,6 +52,7 @@ export interface WAClient {
|
|
|
51
52
|
deleteMessage(jid: string, messageId: string): Promise<void>;
|
|
52
53
|
reuploadMedia(m: WAMessage): Promise<WAMessage>;
|
|
53
54
|
lookupSender(number: string): Promise<SenderFound>;
|
|
55
|
+
setProfile(change: ProfileChange): Promise<void>;
|
|
54
56
|
disconnect(): Promise<void>;
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -335,6 +337,16 @@ export function createClient(account: WhatsAppAccount): WAClient {
|
|
|
335
337
|
const jid = jidNormalizedUser(found.jid);
|
|
336
338
|
return { exists: true, jid, lid: await lidFor(st, sock, jid) };
|
|
337
339
|
},
|
|
340
|
+
async setProfile(change) {
|
|
341
|
+
const sock = await ready(st);
|
|
342
|
+
if (change.name !== undefined) await sock.updateProfileName(change.name);
|
|
343
|
+
if (change.bio !== undefined) await sock.updateProfileStatus(change.bio);
|
|
344
|
+
if (change.avatar !== undefined) {
|
|
345
|
+
const me = sock.user?.id;
|
|
346
|
+
if (me === undefined) throw new TrainError('whatsapp_call', 'own jid unknown');
|
|
347
|
+
await sock.updateProfilePicture(me, { url: change.avatar.path });
|
|
348
|
+
}
|
|
349
|
+
},
|
|
338
350
|
async disconnect() {
|
|
339
351
|
st.closed = true;
|
|
340
352
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Station, Verb } from '@metro-labs/core/stations/types';
|
|
2
|
+
import { PROFILE_FIELDS, type ProfileField } from '@metro-labs/core/stations/profile';
|
|
2
3
|
|
|
3
4
|
export const whatsappStation: Station = {
|
|
4
5
|
name: 'whatsapp',
|
|
@@ -13,6 +14,7 @@ export const whatsappStation: Station = {
|
|
|
13
14
|
'delete',
|
|
14
15
|
]),
|
|
15
16
|
attachmentMode: 'canonical',
|
|
17
|
+
profileFields: new Set<ProfileField>(PROFILE_FIELDS),
|
|
16
18
|
resolvesSenders: true,
|
|
17
19
|
tools: [],
|
|
18
20
|
};
|
package/runtime/runtime.json
CHANGED