create-message-kit 1.0.15 → 1.0.16
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/index.js +12 -3
- package/package.json +2 -2
- package/templates/agent/.env.example +2 -0
- package/{examples/one-to-one → templates/agent}/package.json +2 -4
- package/templates/agent/src/commands.ts +69 -0
- package/templates/agent/src/handler/ens.ts +247 -0
- package/templates/agent/src/index.ts +6 -0
- package/templates/agent/src/lib/openai.ts +125 -0
- package/templates/agent/src/lib/resolver.ts +82 -0
- package/templates/agent/src/lib/types.ts +33 -0
- package/templates/agent/src/prompt.ts +93 -0
- package/templates/gm/.env.example +1 -0
- package/{examples → templates}/gm/package.json +1 -0
- package/templates/gm/src/commands.ts +18 -0
- package/templates/gm/src/handler.ts +6 -0
- package/templates/gm/src/index.ts +16 -0
- package/templates/group/.env.example +3 -0
- package/{examples → templates}/group/package.json +1 -0
- package/{examples → templates}/group/src/commands.ts +13 -16
- package/{examples → templates}/group/src/handler/agent.ts +2 -1
- package/{examples → templates}/group/src/handler/game.ts +2 -2
- package/{examples → templates}/group/src/handler/splitpayment.ts +1 -1
- package/{examples → templates}/group/src/handler/transaction.ts +1 -1
- package/{examples → templates}/group/src/index.ts +2 -17
- package/{examples → templates}/group/src/lib/openai.ts +0 -1
- package/templates/group/src/lib/resolver.ts +0 -0
- package/examples/gm/.env.example +0 -1
- package/examples/gm/src/index.ts +0 -9
- package/examples/group/.env.example +0 -3
- package/examples/one-to-one/.env.example +0 -2
- package/examples/one-to-one/src/index.ts +0 -72
- package/examples/one-to-one/src/lib/cron.ts +0 -34
- package/examples/one-to-one/src/lib/redis.ts +0 -15
- /package/{examples → templates}/group/src/handler/loyalty.ts +0 -0
- /package/{examples → templates}/group/src/handler/tipping.ts +0 -0
- /package/{examples → templates}/group/src/lib/stack.ts +0 -0
package/index.js
CHANGED
@@ -19,7 +19,16 @@ program
|
|
19
19
|
.name("byob")
|
20
20
|
.description("CLI to initialize projects")
|
21
21
|
.action(async () => {
|
22
|
-
|
22
|
+
log.message(`\x1b[38;2;250;105;119m\
|
23
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
24
|
+
|
25
|
+
███╗ ███╗███████╗███████╗███████╗ █████╗ ██████╗ ███████╗██╗ ██╗██╗████████╗
|
26
|
+
████╗ ████║██╔════╝██╔════╝██╔════╝██╔══██╗██╔════╝ ██╔════╝██║ ██╔╝██║╚══██╔══╝
|
27
|
+
██╔████╔██║█████╗ ███████╗███████╗███████║██║ ███╗█████╗ █████╔╝ ██║ ██║
|
28
|
+
██║╚██╔╝██║██╔══╝ ╚════██║╚════██║██╔══██║██║ ██║██╔══╝ ██╔═██╗ ██║ ██║
|
29
|
+
██║ ╚═╝ ██║███████╗███████║███████║██║ ██║╚██████╔╝███████╗██║ ██╗██║ ██║
|
30
|
+
╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
31
|
+
Powered by XMTP \x1b[0m`);
|
23
32
|
|
24
33
|
const { templateType, displayName, destDir } = await gatherProjectInfo();
|
25
34
|
|
@@ -57,7 +66,7 @@ program.parse(process.argv);
|
|
57
66
|
async function gatherProjectInfo() {
|
58
67
|
const templateOptions = [
|
59
68
|
{ value: "gm", label: "GM" },
|
60
|
-
{ value: "
|
69
|
+
{ value: "agent", label: "Agent" },
|
61
70
|
{ value: "group", label: "Group" },
|
62
71
|
];
|
63
72
|
|
@@ -70,7 +79,7 @@ async function gatherProjectInfo() {
|
|
70
79
|
process.exit(0);
|
71
80
|
}
|
72
81
|
|
73
|
-
const templateDir = resolve(__dirname, `./
|
82
|
+
const templateDir = resolve(__dirname, `./templates/${templateType}`);
|
74
83
|
|
75
84
|
// Ensure the template directory exists
|
76
85
|
if (!fs.existsSync(templateDir)) {
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "create-message-kit",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.16",
|
4
4
|
"license": "MIT",
|
5
5
|
"type": "module",
|
6
6
|
"main": "index.js",
|
@@ -8,7 +8,7 @@
|
|
8
8
|
"bin": "index.js",
|
9
9
|
"files": [
|
10
10
|
"index.js",
|
11
|
-
"
|
11
|
+
"templates/**/*"
|
12
12
|
],
|
13
13
|
"scripts": {
|
14
14
|
"clean": "rm -rf .turbo && rm -rf node_modules",
|
@@ -1,5 +1,5 @@
|
|
1
1
|
{
|
2
|
-
"name": "
|
2
|
+
"name": "agent",
|
3
3
|
"private": true,
|
4
4
|
"type": "module",
|
5
5
|
"scripts": {
|
@@ -8,13 +8,11 @@
|
|
8
8
|
"start": "node dist/index.js"
|
9
9
|
},
|
10
10
|
"dependencies": {
|
11
|
-
"@redis/client": "^1.5.16",
|
12
11
|
"@xmtp/message-kit": "workspace:*",
|
13
|
-
"
|
12
|
+
"openai": "^4.65.0"
|
14
13
|
},
|
15
14
|
"devDependencies": {
|
16
15
|
"@types/node": "^20.14.2",
|
17
|
-
"@types/node-cron": "^3.0.11",
|
18
16
|
"nodemon": "^3.1.3",
|
19
17
|
"typescript": "^5.4.5"
|
20
18
|
},
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import type { CommandGroup } from "@xmtp/message-kit";
|
2
|
+
import { ensAgent, handleEns } from "./handler/ens.js";
|
3
|
+
|
4
|
+
export const commands: CommandGroup[] = [
|
5
|
+
{
|
6
|
+
name: "Ens Domain Bot",
|
7
|
+
description: "Register ENS domains.",
|
8
|
+
commands: [
|
9
|
+
{
|
10
|
+
command: "/register [domain]",
|
11
|
+
triggers: ["/register", "@ensbot"],
|
12
|
+
handler: handleEns,
|
13
|
+
description: "Register a domain.",
|
14
|
+
params: {
|
15
|
+
domain: {
|
16
|
+
type: "string",
|
17
|
+
},
|
18
|
+
},
|
19
|
+
},
|
20
|
+
{
|
21
|
+
command: "/info [domain]",
|
22
|
+
triggers: ["/info", "@ensbot"],
|
23
|
+
handler: handleEns,
|
24
|
+
description: "Get information about a domain.",
|
25
|
+
params: {
|
26
|
+
domain: {
|
27
|
+
type: "string",
|
28
|
+
},
|
29
|
+
},
|
30
|
+
},
|
31
|
+
{
|
32
|
+
command: "/renew [domain]",
|
33
|
+
triggers: ["/renew", "@ensbot"],
|
34
|
+
handler: handleEns,
|
35
|
+
description: "Renew a domain.",
|
36
|
+
params: {
|
37
|
+
domain: {
|
38
|
+
type: "string",
|
39
|
+
},
|
40
|
+
},
|
41
|
+
},
|
42
|
+
{
|
43
|
+
command: "/check [domain] [cool_alternatives]",
|
44
|
+
triggers: ["/check"],
|
45
|
+
handler: handleEns,
|
46
|
+
description: "Check if a domain is available.",
|
47
|
+
params: {
|
48
|
+
domain: {
|
49
|
+
type: "string",
|
50
|
+
},
|
51
|
+
cool_alternatives: {
|
52
|
+
type: "quoted",
|
53
|
+
},
|
54
|
+
},
|
55
|
+
},
|
56
|
+
{
|
57
|
+
command: "/tip [address]",
|
58
|
+
description: "Show a URL for tipping a domain owner.",
|
59
|
+
triggers: ["/tip"],
|
60
|
+
handler: ensAgent,
|
61
|
+
params: {
|
62
|
+
address: {
|
63
|
+
type: "address",
|
64
|
+
},
|
65
|
+
},
|
66
|
+
},
|
67
|
+
],
|
68
|
+
},
|
69
|
+
];
|
@@ -0,0 +1,247 @@
|
|
1
|
+
import { HandlerContext } from "@xmtp/message-kit";
|
2
|
+
import { Client } from "@xmtp/xmtp-js";
|
3
|
+
import { getUserInfo, getInfoCache, isOnXMTP } from "../lib/resolver.js";
|
4
|
+
import { textGeneration, responseParser } from "../lib/openai.js";
|
5
|
+
import { ens_agent_prompt } from "../prompt.js";
|
6
|
+
import type {
|
7
|
+
ensDomain,
|
8
|
+
converseUsername,
|
9
|
+
tipAddress,
|
10
|
+
chatHistories,
|
11
|
+
tipDomain,
|
12
|
+
} from "../lib/types.js";
|
13
|
+
import { frameUrl, ensUrl, baseTxUrl, InfoCache } from "../lib/types.js";
|
14
|
+
|
15
|
+
let tipAddress: tipAddress = {};
|
16
|
+
let tipDomain: tipDomain = {};
|
17
|
+
let ensDomain: ensDomain = {};
|
18
|
+
let infoCache: InfoCache = {};
|
19
|
+
let converseUsername: converseUsername = {};
|
20
|
+
let chatHistories: chatHistories = {};
|
21
|
+
|
22
|
+
// URL for the send transaction
|
23
|
+
export async function handleEns(context: HandlerContext) {
|
24
|
+
const {
|
25
|
+
message: {
|
26
|
+
content: { command, params, sender },
|
27
|
+
},
|
28
|
+
} = context;
|
29
|
+
|
30
|
+
if (command == "renew") {
|
31
|
+
// Destructure and validate parameters for the ens command
|
32
|
+
const { domain } = params;
|
33
|
+
// Check if the user holds the domain
|
34
|
+
if (!domain) {
|
35
|
+
context.reply("Missing required parameters. Please provide domain.");
|
36
|
+
return;
|
37
|
+
}
|
38
|
+
|
39
|
+
const { infoCache: retrievedInfoCache } = await getInfoCache(
|
40
|
+
domain,
|
41
|
+
infoCache,
|
42
|
+
);
|
43
|
+
infoCache = retrievedInfoCache;
|
44
|
+
let data = infoCache[domain].info;
|
45
|
+
|
46
|
+
if (data?.address !== sender?.address) {
|
47
|
+
return {
|
48
|
+
code: 403,
|
49
|
+
message: "You do not hold this domain. Only the owner can renew it.",
|
50
|
+
};
|
51
|
+
}
|
52
|
+
|
53
|
+
// Generate URL for the ens
|
54
|
+
let url_ens = frameUrl + "frames/manage?name=" + domain;
|
55
|
+
return { code: 200, message: `${url_ens}` };
|
56
|
+
} else if (command == "register") {
|
57
|
+
// Destructure and validate parameters for the ens command
|
58
|
+
const { domain } = params;
|
59
|
+
|
60
|
+
if (!domain) {
|
61
|
+
return {
|
62
|
+
code: 400,
|
63
|
+
message: "Missing required parameters. Please provide domain.",
|
64
|
+
};
|
65
|
+
}
|
66
|
+
// Generate URL for the ens
|
67
|
+
let url_ens = ensUrl + domain;
|
68
|
+
return { code: 200, message: `${url_ens}` };
|
69
|
+
} else if (command == "info") {
|
70
|
+
const { domain } = params;
|
71
|
+
|
72
|
+
const { infoCache: retrievedInfoCache } = await getInfoCache(
|
73
|
+
domain,
|
74
|
+
infoCache,
|
75
|
+
);
|
76
|
+
infoCache = retrievedInfoCache;
|
77
|
+
let data = infoCache[domain].info;
|
78
|
+
|
79
|
+
const formattedData = {
|
80
|
+
Address: data?.address,
|
81
|
+
"Avatar URL": data?.avatar_url,
|
82
|
+
Description: data?.description,
|
83
|
+
ENS: data?.ens,
|
84
|
+
"Primary ENS": data?.ens_primary,
|
85
|
+
GitHub: data?.github,
|
86
|
+
Resolver: data?.resolverAddress,
|
87
|
+
Twitter: data?.twitter,
|
88
|
+
URL: `${ensUrl}${domain}`,
|
89
|
+
};
|
90
|
+
|
91
|
+
let message = "Domain information:\n\n";
|
92
|
+
for (const [key, value] of Object.entries(formattedData)) {
|
93
|
+
if (value) {
|
94
|
+
message += `${key}: ${value}\n`;
|
95
|
+
}
|
96
|
+
}
|
97
|
+
message += `\n\nWould you like to tip the domain owner for getting there first 🤣?`;
|
98
|
+
message = message.trim();
|
99
|
+
if (await isOnXMTP(context.v2client, data?.ens, data?.address)) {
|
100
|
+
context.send(
|
101
|
+
`Ah, this domains is in XMTP, you can message it directly: https://converse.xyz/dm/${domain}`,
|
102
|
+
);
|
103
|
+
}
|
104
|
+
return { code: 200, message };
|
105
|
+
} else if (command == "check") {
|
106
|
+
console.log(params);
|
107
|
+
const { domain, cool_alternatives } = params;
|
108
|
+
|
109
|
+
const cool_alternativesFormat = cool_alternatives
|
110
|
+
?.split(",")
|
111
|
+
.map(
|
112
|
+
(alternative: string, index: number) =>
|
113
|
+
`${index + 1}. ${alternative} ✨`,
|
114
|
+
)
|
115
|
+
.join("\n");
|
116
|
+
|
117
|
+
if (!domain) {
|
118
|
+
return {
|
119
|
+
code: 400,
|
120
|
+
message: "Please provide a domain name to check.",
|
121
|
+
};
|
122
|
+
}
|
123
|
+
const { infoCache: retrievedInfoCache } = await getInfoCache(
|
124
|
+
domain,
|
125
|
+
infoCache,
|
126
|
+
);
|
127
|
+
infoCache = retrievedInfoCache;
|
128
|
+
let data = infoCache?.[domain]?.info;
|
129
|
+
if (!data?.address) {
|
130
|
+
let message = `Looks like ${domain} is available! Do you want to register it? ${ensUrl}${domain}`;
|
131
|
+
return {
|
132
|
+
code: 200,
|
133
|
+
message,
|
134
|
+
};
|
135
|
+
} else {
|
136
|
+
let message = `Looks like ${domain} is already registered! What about these cool alternatives?\n\n${cool_alternativesFormat}`;
|
137
|
+
return {
|
138
|
+
code: 404,
|
139
|
+
message,
|
140
|
+
};
|
141
|
+
}
|
142
|
+
} else if (command == "tip") {
|
143
|
+
// Destructure and validate parameters for the send command
|
144
|
+
const { address } = params;
|
145
|
+
|
146
|
+
const { infoCache: retrievedInfoCache } = await getInfoCache(
|
147
|
+
address,
|
148
|
+
infoCache,
|
149
|
+
);
|
150
|
+
infoCache = retrievedInfoCache;
|
151
|
+
let data = infoCache[address].info;
|
152
|
+
|
153
|
+
tipAddress[sender.address] = data?.address;
|
154
|
+
tipDomain[sender.address] = data?.ens;
|
155
|
+
|
156
|
+
if (!address || !tipAddress[sender.address]) {
|
157
|
+
context.reply("Missing required parameters. Please provide address.");
|
158
|
+
return;
|
159
|
+
}
|
160
|
+
let txUrl = `${baseTxUrl}/transaction/?transaction_type=send&buttonName=Tip%20${tipDomain[sender.address]}&amount=1&token=USDC&receiver=${tipAddress[sender.address]}`;
|
161
|
+
// Generate URL for the send transaction
|
162
|
+
context.send(`Here is the url to send the tip:\n${txUrl}`);
|
163
|
+
} else if (command == "cool") {
|
164
|
+
return;
|
165
|
+
}
|
166
|
+
}
|
167
|
+
|
168
|
+
export async function clearChatHistory() {
|
169
|
+
chatHistories = {};
|
170
|
+
}
|
171
|
+
|
172
|
+
async function processResponseWithIntent(
|
173
|
+
reply: string,
|
174
|
+
context: any,
|
175
|
+
senderAddress: string,
|
176
|
+
) {
|
177
|
+
let messages = reply
|
178
|
+
.split("\n")
|
179
|
+
.map((message: string) => responseParser(message))
|
180
|
+
.filter((message): message is string => message.length > 0);
|
181
|
+
|
182
|
+
console.log(messages);
|
183
|
+
for (const message of messages) {
|
184
|
+
if (message.startsWith("/")) {
|
185
|
+
const response = await context.intent(message);
|
186
|
+
if (response && response.message) {
|
187
|
+
let msg = responseParser(response.message);
|
188
|
+
|
189
|
+
chatHistories[senderAddress].push({
|
190
|
+
role: "system",
|
191
|
+
content: msg,
|
192
|
+
});
|
193
|
+
|
194
|
+
await context.send(response.message);
|
195
|
+
}
|
196
|
+
} else {
|
197
|
+
await context.send(message);
|
198
|
+
}
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
export async function ensAgent(context: HandlerContext) {
|
203
|
+
if (!process?.env?.OPEN_AI_API_KEY) {
|
204
|
+
console.warn("No OPEN_AI_API_KEY found in .env");
|
205
|
+
return;
|
206
|
+
}
|
207
|
+
|
208
|
+
const {
|
209
|
+
message: {
|
210
|
+
content: { content, params },
|
211
|
+
sender,
|
212
|
+
},
|
213
|
+
group,
|
214
|
+
} = context;
|
215
|
+
|
216
|
+
try {
|
217
|
+
let userPrompt = params?.prompt ?? content;
|
218
|
+
const { converseUsername: newConverseUsername, ensDomain: newEnsDomain } =
|
219
|
+
await getUserInfo(
|
220
|
+
sender.address,
|
221
|
+
ensDomain[sender.address],
|
222
|
+
converseUsername[sender.address],
|
223
|
+
);
|
224
|
+
|
225
|
+
ensDomain[sender.address] = newEnsDomain;
|
226
|
+
converseUsername[sender.address] = newConverseUsername;
|
227
|
+
let txUrl = `${baseTxUrl}/transaction/?transaction_type=send&buttonName=Tip%20${tipDomain[sender.address]}&amount=1&token=USDC&receiver=${tipAddress[sender.address]}`;
|
228
|
+
|
229
|
+
const { reply, history } = await textGeneration(
|
230
|
+
userPrompt,
|
231
|
+
await ens_agent_prompt(
|
232
|
+
sender.address,
|
233
|
+
ensDomain[sender.address],
|
234
|
+
converseUsername[sender.address],
|
235
|
+
tipAddress[sender.address],
|
236
|
+
txUrl,
|
237
|
+
),
|
238
|
+
chatHistories[sender.address],
|
239
|
+
);
|
240
|
+
if (!group) chatHistories[sender.address] = history; // Update chat history for the user
|
241
|
+
|
242
|
+
await processResponseWithIntent(reply, context, sender.address);
|
243
|
+
} catch (error) {
|
244
|
+
console.error("Error during OpenAI call:", error);
|
245
|
+
await context.send("An error occurred while processing your request.");
|
246
|
+
}
|
247
|
+
}
|
@@ -0,0 +1,125 @@
|
|
1
|
+
import dotenv from "dotenv";
|
2
|
+
dotenv.config();
|
3
|
+
|
4
|
+
import OpenAI from "openai";
|
5
|
+
const openai = new OpenAI({
|
6
|
+
apiKey: process.env.OPEN_AI_API_KEY,
|
7
|
+
});
|
8
|
+
|
9
|
+
export async function textGeneration(
|
10
|
+
userPrompt: string,
|
11
|
+
systemPrompt: string,
|
12
|
+
chatHistory?: any[],
|
13
|
+
) {
|
14
|
+
let messages = chatHistory ? [...chatHistory] : []; // Start with existing chat history
|
15
|
+
if (messages.length === 0) {
|
16
|
+
messages.push({
|
17
|
+
role: "system",
|
18
|
+
content: systemPrompt,
|
19
|
+
});
|
20
|
+
}
|
21
|
+
messages.push({
|
22
|
+
role: "user",
|
23
|
+
content: userPrompt,
|
24
|
+
});
|
25
|
+
try {
|
26
|
+
const response = await openai.chat.completions.create({
|
27
|
+
model: "gpt-4o",
|
28
|
+
messages: messages as any,
|
29
|
+
});
|
30
|
+
const reply = response.choices[0].message.content;
|
31
|
+
messages.push({
|
32
|
+
role: "assistant",
|
33
|
+
content: reply || "No response from OpenAI.",
|
34
|
+
});
|
35
|
+
const cleanedReply = responseParser(reply as string);
|
36
|
+
|
37
|
+
return { reply: cleanedReply, history: messages };
|
38
|
+
} catch (error) {
|
39
|
+
console.error("Failed to fetch from OpenAI:", error);
|
40
|
+
throw error;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
// New method to interpret an image
|
45
|
+
export async function vision(imageData: Uint8Array, systemPrompt: string) {
|
46
|
+
const base64Image = Buffer.from(imageData).toString("base64");
|
47
|
+
const dataUrl = `data:image/jpeg;base64,${base64Image}`;
|
48
|
+
|
49
|
+
// Create a new thread for each vision request
|
50
|
+
const visionMessages = [
|
51
|
+
{
|
52
|
+
role: "system",
|
53
|
+
content: systemPrompt,
|
54
|
+
},
|
55
|
+
{
|
56
|
+
role: "user",
|
57
|
+
content: [
|
58
|
+
{ type: "text", text: systemPrompt },
|
59
|
+
{
|
60
|
+
type: "image_url",
|
61
|
+
image_url: {
|
62
|
+
url: dataUrl,
|
63
|
+
},
|
64
|
+
},
|
65
|
+
],
|
66
|
+
},
|
67
|
+
];
|
68
|
+
|
69
|
+
try {
|
70
|
+
const response = await openai.chat.completions.create({
|
71
|
+
model: "gpt-4o",
|
72
|
+
messages: visionMessages as any,
|
73
|
+
});
|
74
|
+
return response.choices[0].message.content;
|
75
|
+
} catch (error) {
|
76
|
+
console.error("Failed to interpret image with OpenAI:", error);
|
77
|
+
throw error;
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
export function responseParser(message: string) {
|
82
|
+
let trimmedMessage = message;
|
83
|
+
// Remove bold and underline markdown
|
84
|
+
trimmedMessage = trimmedMessage?.replace(/(\*\*|__)(.*?)\1/g, "$2");
|
85
|
+
// Remove markdown links, keeping only the URL
|
86
|
+
trimmedMessage = trimmedMessage?.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$2");
|
87
|
+
// Remove markdown headers
|
88
|
+
trimmedMessage = trimmedMessage?.replace(/^#+\s*(.*)$/gm, "$1");
|
89
|
+
// Remove inline code formatting
|
90
|
+
trimmedMessage = trimmedMessage?.replace(/`([^`]+)`/g, "$1");
|
91
|
+
// Remove single backticks at the start or end of the message
|
92
|
+
trimmedMessage = trimmedMessage?.replace(/^`|`$/g, "");
|
93
|
+
// Remove leading and trailing whitespace
|
94
|
+
trimmedMessage = trimmedMessage?.replace(/^\s+|\s+$/g, "");
|
95
|
+
// Remove any remaining leading or trailing whitespace
|
96
|
+
trimmedMessage = trimmedMessage.trim();
|
97
|
+
return trimmedMessage;
|
98
|
+
}
|
99
|
+
|
100
|
+
// UNTESTED, recursive response parser
|
101
|
+
export function responseParser2(message: string | string[]): string | string[] {
|
102
|
+
// If message is an array, process each item individually
|
103
|
+
if (Array.isArray(message)) {
|
104
|
+
return message
|
105
|
+
.map((item) => responseParser(item))
|
106
|
+
.flat() // Flatten nested arrays
|
107
|
+
.filter((item: string) => item.length > 0)
|
108
|
+
.filter((item: string) => item !== "`");
|
109
|
+
}
|
110
|
+
let trimmedMessage = message;
|
111
|
+
// Remove bold and underline markdown
|
112
|
+
trimmedMessage = trimmedMessage?.replace(/(\*\*|__)(.*?)\1/g, "$2");
|
113
|
+
// Remove markdown links, keeping only the URL
|
114
|
+
trimmedMessage = trimmedMessage?.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$2");
|
115
|
+
// Remove markdown headers
|
116
|
+
trimmedMessage = trimmedMessage?.replace(/^#+\s*(.*)$/gm, "$1");
|
117
|
+
// Remove inline code formatting
|
118
|
+
trimmedMessage = trimmedMessage?.replace(/(`{1,3})(.*?)\1/g, "$2");
|
119
|
+
// Remove leading and trailing whitespace
|
120
|
+
trimmedMessage = trimmedMessage?.replace(/`/g, ""); // Remove single backticks
|
121
|
+
// Remove any remaining leading or trailing whitespace
|
122
|
+
trimmedMessage = trimmedMessage?.trim();
|
123
|
+
|
124
|
+
return trimmedMessage;
|
125
|
+
}
|
@@ -0,0 +1,82 @@
|
|
1
|
+
import type { EnsData } from "./types.js";
|
2
|
+
import { endpointURL } from "./types.js";
|
3
|
+
import { Client } from "@xmtp/xmtp-js";
|
4
|
+
import { InfoCache } from "./types.js";
|
5
|
+
|
6
|
+
export async function getUserInfo(
|
7
|
+
address: string,
|
8
|
+
ensDomain: string | undefined,
|
9
|
+
converseUsername: string | undefined,
|
10
|
+
) {
|
11
|
+
if (!ensDomain) {
|
12
|
+
const response = await fetch(`https://ensdata.net/${address}`);
|
13
|
+
const data: EnsData = (await response.json()) as EnsData;
|
14
|
+
ensDomain = data?.ens;
|
15
|
+
}
|
16
|
+
if (!converseUsername) {
|
17
|
+
const response = await fetch(`${endpointURL}/profile/${address}`, {
|
18
|
+
method: "POST",
|
19
|
+
headers: {
|
20
|
+
"Content-Type": "application/json",
|
21
|
+
Accept: "application/json",
|
22
|
+
},
|
23
|
+
body: JSON.stringify({ address: address }),
|
24
|
+
});
|
25
|
+
const data = (await response.json()) as { name: string };
|
26
|
+
converseUsername = data?.name;
|
27
|
+
converseUsername = converseUsername.replace(".converse.xyz", "");
|
28
|
+
}
|
29
|
+
console.log("User info fetched for", {
|
30
|
+
address,
|
31
|
+
converseUsername,
|
32
|
+
ensDomain,
|
33
|
+
});
|
34
|
+
return { converseUsername: converseUsername, ensDomain: ensDomain };
|
35
|
+
}
|
36
|
+
|
37
|
+
export const getInfoCache = async (
|
38
|
+
key: string, // This can be either domain or address
|
39
|
+
infoCache: InfoCache,
|
40
|
+
): Promise<{ domain: string; info: EnsData; infoCache: InfoCache }> => {
|
41
|
+
if (infoCache[key]) {
|
42
|
+
let data = {
|
43
|
+
domain: key,
|
44
|
+
info: infoCache[key].info,
|
45
|
+
infoCache: infoCache,
|
46
|
+
};
|
47
|
+
return data;
|
48
|
+
}
|
49
|
+
try {
|
50
|
+
const response = await fetch(`https://ensdata.net/${key}`);
|
51
|
+
const data: EnsData = (await response.json()) as EnsData;
|
52
|
+
|
53
|
+
// Assuming the data contains both domain and address
|
54
|
+
const domain = data?.ens;
|
55
|
+
const address = data?.address;
|
56
|
+
|
57
|
+
// Store data in cache by both domain and address
|
58
|
+
if (domain) infoCache[domain as string] = { info: data };
|
59
|
+
if (address) infoCache[address as string] = { info: data };
|
60
|
+
|
61
|
+
return { info: data, infoCache: infoCache, domain: domain as string };
|
62
|
+
} catch (error) {
|
63
|
+
console.error(error);
|
64
|
+
return { info: {}, infoCache: infoCache, domain: "" };
|
65
|
+
}
|
66
|
+
};
|
67
|
+
export const generateCoolAlternatives = (domain: string) => {
|
68
|
+
const suffixes = ["lfg", "cool", "degen", "moon", "base", "gm"];
|
69
|
+
const alternatives = suffixes.map((suffix) => {
|
70
|
+
const randomPosition = Math.random() < 0.5;
|
71
|
+
return randomPosition ? `${suffix}${domain}.eth` : `${domain}${suffix}.eth`;
|
72
|
+
});
|
73
|
+
return alternatives.join(",");
|
74
|
+
};
|
75
|
+
export const isOnXMTP = async (
|
76
|
+
client: Client,
|
77
|
+
domain: string | undefined,
|
78
|
+
address: string | undefined,
|
79
|
+
) => {
|
80
|
+
if (domain == "fabri.eth") return false;
|
81
|
+
if (address) return (await client.canMessage([address])).length > 0;
|
82
|
+
};
|
@@ -0,0 +1,33 @@
|
|
1
|
+
export type chatHistories = Record<string, any[]>;
|
2
|
+
export type ensDomain = Record<string, string | undefined>;
|
3
|
+
export type converseUsername = Record<string, string | undefined>;
|
4
|
+
export type tipAddress = Record<string, string | undefined>;
|
5
|
+
export type tipDomain = Record<string, string | undefined>;
|
6
|
+
|
7
|
+
export type InfoCache = {
|
8
|
+
[key: string]: { info: EnsData };
|
9
|
+
};
|
10
|
+
export const frameUrl = "https://ens.steer.fun/";
|
11
|
+
export const ensUrl = "https://app.ens.domains/";
|
12
|
+
export const baseTxUrl = "https://base-tx-frame.vercel.app";
|
13
|
+
export const endpointURL =
|
14
|
+
"https://converse-website-git-endpoit-ephemerahq.vercel.app";
|
15
|
+
|
16
|
+
export interface EnsData {
|
17
|
+
address?: string;
|
18
|
+
avatar?: string;
|
19
|
+
avatar_small?: string;
|
20
|
+
converse?: string;
|
21
|
+
avatar_url?: string;
|
22
|
+
contentHash?: string;
|
23
|
+
description?: string;
|
24
|
+
ens?: string;
|
25
|
+
ens_primary?: string;
|
26
|
+
github?: string;
|
27
|
+
resolverAddress?: string;
|
28
|
+
twitter?: string;
|
29
|
+
url?: string;
|
30
|
+
wallets?: {
|
31
|
+
eth?: string;
|
32
|
+
};
|
33
|
+
}
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import { generateCoolAlternatives } from "./lib/resolver.js";
|
2
|
+
export async function ens_agent_prompt(
|
3
|
+
address: string,
|
4
|
+
domain?: string,
|
5
|
+
name?: string,
|
6
|
+
converseUsername?: string,
|
7
|
+
tipAddress?: string,
|
8
|
+
txUrl?: string,
|
9
|
+
) {
|
10
|
+
const userName = domain ?? name ?? "";
|
11
|
+
const commonAlternatives = generateCoolAlternatives(userName);
|
12
|
+
const systemPrompt = `You are a helpful and playful agent that lives inside a web3 messaging app.
|
13
|
+
- You can respond with multiple messages if needed. Each message should be separated by a newline character.
|
14
|
+
- You can trigger commands by only sending the command in a newline message.
|
15
|
+
- Never announce actions without using a command separated by a newline character.
|
16
|
+
- Only provide answers based on verified information.
|
17
|
+
- Do not make guesses or assumptions
|
18
|
+
- CHECK that you are not missing a command
|
19
|
+
|
20
|
+
User context:
|
21
|
+
- Users address is: ${address}
|
22
|
+
${domain != undefined ? `- User ENS domain is: ${domain}` : "- User ENS domain: None"}
|
23
|
+
${name != undefined ? `- User name is: ${name}` : "- User name: None"}
|
24
|
+
|
25
|
+
## Task
|
26
|
+
- Start by fetch their domain from or Convese username
|
27
|
+
- Call the user by their name or domain, in case they have one
|
28
|
+
- Ask for a name (if they don't have one) so you can suggest domains.
|
29
|
+
- Use "/check [domain] [cool_alternatives]" to see if a domain is available and offer cool alternatives
|
30
|
+
- To check the information about the domain by using the command "/info [domain]".
|
31
|
+
- To register a domain use the command "/register [domain]".
|
32
|
+
- To trigger renewal: "/renew [domain]".
|
33
|
+
- To tip the domain owner: "/tip [address]".
|
34
|
+
|
35
|
+
Commands:
|
36
|
+
- /info [domain]: Get information about a domain
|
37
|
+
- /check [domain] [cool_alternatives]: Check if a domain is available and send cool alternatives
|
38
|
+
- /register [domain]: Register a domain
|
39
|
+
- /renew [domain]: Renew a domain
|
40
|
+
- /tip [address]: Tip the domain owner
|
41
|
+
|
42
|
+
Examples:
|
43
|
+
- /check ${userName} "${commonAlternatives}"
|
44
|
+
- /info nick.eth
|
45
|
+
- /register vitalik.eth
|
46
|
+
- /renew fabri.base.eth
|
47
|
+
- /tip 0xf0EA7663233F99D0c12370671abBb6Cca980a490
|
48
|
+
|
49
|
+
## Example response:
|
50
|
+
|
51
|
+
1. Check if the user does not have a ENS domain
|
52
|
+
Hey ${name}! it looks like you don't have a ENS domain yet! \n\nCan you give me another name so I can suggest some cool domain alternatives for you or i can use your ${converseUsername} username? 🤔
|
53
|
+
|
54
|
+
2. If the user has a ENS domain
|
55
|
+
Hello ${domain} ! I'll help you get your ENS domain.\n Let's start by checking your ENS domain ${domain}. Give me a moment.\n/check ${domain} "${commonAlternatives}"
|
56
|
+
|
57
|
+
3. Check if the ENS domain is available
|
58
|
+
Hello! I'll help you get your domain.\n Let's start by checking your ENS domain ${domain}. Give me a moment.\n/check ${domain} "${commonAlternatives}"
|
59
|
+
|
60
|
+
4. If the ENS domain is available,
|
61
|
+
Looks like ${domain} is available! Would you like to register it?\n/register ${domain}\n or I can suggest some cool alternatives? Le me know 🤔
|
62
|
+
|
63
|
+
5. If the ENS domain is already registered, let me suggest 5 cool alternatives
|
64
|
+
Looks like ${domain} is already registered!\n What about these cool alternatives\n/check ${domain} "${commonAlternatives}"
|
65
|
+
|
66
|
+
6. If the user wants to register a ENS domain, use the command "/register [domain]"
|
67
|
+
Looks like ${domain} is available! Let me help you register it\n/register ${domain}
|
68
|
+
|
69
|
+
7. If the user wants to tip the ENS domain owner, use the command "/tip [address]", this will return a url to send the tip
|
70
|
+
Looks like ${domain} is already registered!\n Would you like to tip the owner for getting there first 🤣?\n/tip ${tipAddress}
|
71
|
+
|
72
|
+
8. When sending a tip, the url will be returned in the message.
|
73
|
+
Here is the url to send the tip:\n${txUrl}
|
74
|
+
|
75
|
+
9. If the user wants to get information about the ENS domain, use the command "/info [domain]"
|
76
|
+
Hello! I'll help you get info about ${domain}.\n Give me a moment.\n/info ${domain}
|
77
|
+
|
78
|
+
10. If the user wants to renew their domain, use the command "/renew [domain]"
|
79
|
+
Hello! I'll help you get your ENS domain.\n Let's start by checking your ENS domain ${domain}. Give me a moment.\n/renew ${domain}
|
80
|
+
|
81
|
+
11. If the user wants to directly to tip the ENS domain owner, use the command "/tip [address]", this will return a url to send the tip
|
82
|
+
Here is the url to send the tip:\n${txUrl}
|
83
|
+
|
84
|
+
12. If the user wants cool suggestions about a domain, use the command "/cool [domain]"
|
85
|
+
Here are some cool suggestions for ${domain}: "${commonAlternatives}"
|
86
|
+
|
87
|
+
## Most common bug
|
88
|
+
Some times you will say something like: "Looks like vitalik.eth is registered! What about these cool alternatives?"
|
89
|
+
But you forgot to add the command at the end of the message.
|
90
|
+
You should have said something like: "Looks like vitalik.eth is registered! What about these cool alternatives?\n/check vitalik.eth "${commonAlternatives}"
|
91
|
+
`;
|
92
|
+
return systemPrompt;
|
93
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
KEY= # the private key of the bot wallet
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { handler as handlerAll } from "./handler.js";
|
2
|
+
import type { CommandGroup } from "@xmtp/message-kit";
|
3
|
+
|
4
|
+
export const commands: CommandGroup[] = [
|
5
|
+
{
|
6
|
+
name: "Gm Commands",
|
7
|
+
description: "Commands to send a gm.",
|
8
|
+
commands: [
|
9
|
+
{
|
10
|
+
command: "/gm",
|
11
|
+
triggers: ["/gm"],
|
12
|
+
description: "Send a gm.",
|
13
|
+
handler: handlerAll,
|
14
|
+
params: {},
|
15
|
+
},
|
16
|
+
],
|
17
|
+
},
|
18
|
+
];
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import { run, HandlerContext } from "@xmtp/message-kit";
|
2
|
+
|
3
|
+
run(
|
4
|
+
async (context: HandlerContext) => {
|
5
|
+
// Get the message and the address from the sender
|
6
|
+
const { content, sender } = context.message;
|
7
|
+
|
8
|
+
// To reply, just call `reply` on the HandlerContext
|
9
|
+
await context.send(`gm`);
|
10
|
+
},
|
11
|
+
{
|
12
|
+
memberChange: true,
|
13
|
+
attachments: true,
|
14
|
+
experimental: true,
|
15
|
+
},
|
16
|
+
);
|
@@ -4,15 +4,16 @@ import { handler as agent } from "./handler/agent.js";
|
|
4
4
|
import { handler as transaction } from "./handler/transaction.js";
|
5
5
|
import { handler as games } from "./handler/game.js";
|
6
6
|
import { handler as loyalty } from "./handler/loyalty.js";
|
7
|
+
import { helpHandler } from "./index.js";
|
7
8
|
|
8
9
|
export const commands: CommandGroup[] = [
|
9
10
|
{
|
10
11
|
name: "Tipping",
|
11
12
|
description: "Tip tokens via emoji, replies or command.",
|
12
|
-
triggers: ["/tip", "🎩", "@tip"],
|
13
13
|
commands: [
|
14
14
|
{
|
15
15
|
command: "/tip [@users] [amount] [token]",
|
16
|
+
triggers: ["/tip", "🎩", "@tip"],
|
16
17
|
description: "Tip users in a specified token.",
|
17
18
|
handler: tipping,
|
18
19
|
params: {
|
@@ -30,11 +31,11 @@ export const commands: CommandGroup[] = [
|
|
30
31
|
},
|
31
32
|
{
|
32
33
|
name: "Transactions",
|
33
|
-
triggers: ["@send", "/send", "@swap", "/swap", "/show"],
|
34
34
|
description: "Multipurpose transaction frame built onbase.",
|
35
35
|
commands: [
|
36
36
|
{
|
37
37
|
command: "/send [amount] [token] [@username]",
|
38
|
+
triggers: ["@send", "/send"],
|
38
39
|
description:
|
39
40
|
"Send a specified amount of a cryptocurrency to a destination address.",
|
40
41
|
handler: transaction,
|
@@ -56,6 +57,7 @@ export const commands: CommandGroup[] = [
|
|
56
57
|
},
|
57
58
|
{
|
58
59
|
command: "/swap [amount] [token_from] [token_to]",
|
60
|
+
triggers: ["@swap", "/swap"],
|
59
61
|
description: "Exchange one type of cryptocurrency for another.",
|
60
62
|
handler: transaction,
|
61
63
|
params: {
|
@@ -77,6 +79,7 @@ export const commands: CommandGroup[] = [
|
|
77
79
|
},
|
78
80
|
{
|
79
81
|
command: "/show",
|
82
|
+
triggers: ["/show"],
|
80
83
|
handler: transaction,
|
81
84
|
description: "Show the whole frame.",
|
82
85
|
params: {},
|
@@ -85,11 +88,11 @@ export const commands: CommandGroup[] = [
|
|
85
88
|
},
|
86
89
|
{
|
87
90
|
name: "Games",
|
88
|
-
triggers: ["/game", "@game", "🔎", "🔍"],
|
89
91
|
description: "Provides various gaming experiences.",
|
90
92
|
commands: [
|
91
93
|
{
|
92
94
|
command: "/game [game]",
|
95
|
+
triggers: ["/game", "@game", "🔎", "🔍"],
|
93
96
|
handler: games,
|
94
97
|
description: "Play a game.",
|
95
98
|
params: {
|
@@ -104,17 +107,19 @@ export const commands: CommandGroup[] = [
|
|
104
107
|
},
|
105
108
|
{
|
106
109
|
name: "Loyalty",
|
107
|
-
triggers: ["/points", "@points", "/leaderboard", "@leaderboard"],
|
108
110
|
description: "Manage group members and metadata.",
|
109
111
|
commands: [
|
110
112
|
{
|
111
113
|
command: "/points",
|
114
|
+
triggers: ["/points", "@points"],
|
112
115
|
handler: loyalty,
|
113
116
|
description: "Check your points.",
|
114
117
|
params: {},
|
115
118
|
},
|
116
119
|
{
|
117
120
|
command: "/leaderboard",
|
121
|
+
triggers: ["/leaderboard", "@leaderboard"],
|
122
|
+
adminOnly: true,
|
118
123
|
handler: loyalty,
|
119
124
|
description: "Check the points of a user.",
|
120
125
|
params: {},
|
@@ -123,11 +128,11 @@ export const commands: CommandGroup[] = [
|
|
123
128
|
},
|
124
129
|
{
|
125
130
|
name: "Agent",
|
126
|
-
triggers: ["/agent", "@agent", "@bot"],
|
127
131
|
description: "Manage agent commands.",
|
128
132
|
commands: [
|
129
133
|
{
|
130
134
|
command: "/agent [prompt]",
|
135
|
+
triggers: ["/agent", "@agent", "@bot"],
|
131
136
|
handler: agent,
|
132
137
|
description: "Manage agent commands.",
|
133
138
|
params: {
|
@@ -139,22 +144,14 @@ export const commands: CommandGroup[] = [
|
|
139
144
|
},
|
140
145
|
],
|
141
146
|
},
|
142
|
-
{
|
143
|
-
name: "Split Payments",
|
144
|
-
image: true,
|
145
|
-
triggers: [],
|
146
|
-
description: "Split payments between users.",
|
147
|
-
commands: [],
|
148
|
-
},
|
149
147
|
{
|
150
148
|
name: "Help",
|
151
|
-
|
152
|
-
|
153
|
-
description: "Get help with the bot.",
|
149
|
+
description: "Get help with the bot.",
|
154
150
|
commands: [
|
155
151
|
{
|
156
152
|
command: "/help",
|
157
|
-
|
153
|
+
triggers: ["/help"],
|
154
|
+
handler: helpHandler,
|
158
155
|
description: "Get help with the bot.",
|
159
156
|
params: {},
|
160
157
|
},
|
@@ -3,7 +3,7 @@ import { textGeneration } from "../lib/openai.js";
|
|
3
3
|
|
4
4
|
export async function handler(context: HandlerContext) {
|
5
5
|
if (!process?.env?.OPEN_AI_API_KEY) {
|
6
|
-
console.
|
6
|
+
console.warn("No OPEN_AI_API_KEY found in .env");
|
7
7
|
return;
|
8
8
|
}
|
9
9
|
|
@@ -14,6 +14,7 @@ export async function handler(context: HandlerContext) {
|
|
14
14
|
} = context;
|
15
15
|
|
16
16
|
const systemPrompt = generateSystemPrompt(context);
|
17
|
+
console.log("systemPrompt", systemPrompt);
|
17
18
|
try {
|
18
19
|
let userPrompt = params?.prompt ?? content;
|
19
20
|
console.log("userPrompt", userPrompt);
|
@@ -17,8 +17,8 @@ export async function handler(context: HandlerContext) {
|
|
17
17
|
}
|
18
18
|
// URLs for each game type
|
19
19
|
const gameUrls: { [key: string]: string } = {
|
20
|
-
wordle: "https://framedl.xyz
|
21
|
-
slot: "https://slot-machine-frame.vercel.app
|
20
|
+
wordle: "https://framedl.xyz",
|
21
|
+
slot: "https://slot-machine-frame.vercel.app",
|
22
22
|
};
|
23
23
|
// Respond with the appropriate game URL or an error message
|
24
24
|
switch (params.game) {
|
@@ -3,7 +3,7 @@ import { vision, textGeneration } from "../lib/openai.js";
|
|
3
3
|
|
4
4
|
export async function handler(context: HandlerContext) {
|
5
5
|
if (!process?.env?.OPEN_AI_API_KEY) {
|
6
|
-
console.
|
6
|
+
console.warn("No OPEN_AI_API_KEY found in .env");
|
7
7
|
return;
|
8
8
|
}
|
9
9
|
const {
|
@@ -7,7 +7,7 @@ export async function handler(context: HandlerContext) {
|
|
7
7
|
content: { command, params },
|
8
8
|
},
|
9
9
|
} = context;
|
10
|
-
const baseUrl = "https://base-frame
|
10
|
+
const baseUrl = "https://base-tx-frame.vercel.app/transaction";
|
11
11
|
|
12
12
|
switch (command) {
|
13
13
|
case "send":
|
@@ -15,9 +15,6 @@ run(async (context: HandlerContext) => {
|
|
15
15
|
case "remoteStaticAttachment":
|
16
16
|
handleAttachment(context);
|
17
17
|
break;
|
18
|
-
case "text":
|
19
|
-
handleTextMessage(context);
|
20
|
-
break;
|
21
18
|
}
|
22
19
|
});
|
23
20
|
async function handleReply(context: HandlerContext) {
|
@@ -35,8 +32,7 @@ async function handleReply(context: HandlerContext) {
|
|
35
32
|
version,
|
36
33
|
v2client.address,
|
37
34
|
);
|
38
|
-
|
39
|
-
handleTextMessage(context);
|
35
|
+
//await context.intent(chain);
|
40
36
|
}
|
41
37
|
|
42
38
|
// Handle attachment messages
|
@@ -44,18 +40,7 @@ async function handleAttachment(context: HandlerContext) {
|
|
44
40
|
await splitpayment(context);
|
45
41
|
}
|
46
42
|
|
47
|
-
|
48
|
-
async function handleTextMessage(context: HandlerContext) {
|
49
|
-
const {
|
50
|
-
content: { content: text },
|
51
|
-
} = context.message;
|
52
|
-
if (text.includes("/help")) {
|
53
|
-
await helpHandler(context);
|
54
|
-
} else if (text.startsWith("@agent")) {
|
55
|
-
await agent(context);
|
56
|
-
} else await context.intent(text);
|
57
|
-
}
|
58
|
-
async function helpHandler(context: HandlerContext) {
|
43
|
+
export async function helpHandler(context: HandlerContext) {
|
59
44
|
const { commands = [] } = context;
|
60
45
|
const intro =
|
61
46
|
"Available experiences:\n" +
|
File without changes
|
package/examples/gm/.env.example
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
KEY= # 0x... the private key of the bot wallet (with the 0x prefix)
|
package/examples/gm/src/index.ts
DELETED
@@ -1,9 +0,0 @@
|
|
1
|
-
import { run, HandlerContext } from "@xmtp/message-kit";
|
2
|
-
|
3
|
-
run(async (context: HandlerContext) => {
|
4
|
-
// Get the message and the address from the sender
|
5
|
-
const { content, sender } = context.message;
|
6
|
-
|
7
|
-
// To reply, just call `reply` on the HandlerContext
|
8
|
-
await context.send(`gm`);
|
9
|
-
});
|
@@ -1,72 +0,0 @@
|
|
1
|
-
import { getRedisClient } from "./lib/redis.js";
|
2
|
-
import { run, HandlerContext } from "@xmtp/message-kit";
|
3
|
-
import { startCron } from "./lib/cron.js";
|
4
|
-
import { RedisClientType } from "@redis/client";
|
5
|
-
|
6
|
-
//Tracks conversation steps
|
7
|
-
const inMemoryCacheStep = new Map<string, number>();
|
8
|
-
|
9
|
-
//List of words to stop or unsubscribe.
|
10
|
-
const stopWords = ["stop", "unsubscribe", "cancel", "list"];
|
11
|
-
|
12
|
-
const redisClient: RedisClientType = await getRedisClient();
|
13
|
-
|
14
|
-
let clientInitialized = false;
|
15
|
-
run(async (context: HandlerContext) => {
|
16
|
-
const {
|
17
|
-
v2client,
|
18
|
-
message: {
|
19
|
-
content: { content: text },
|
20
|
-
typeId,
|
21
|
-
sender,
|
22
|
-
},
|
23
|
-
} = context;
|
24
|
-
|
25
|
-
if (!clientInitialized) {
|
26
|
-
startCron(redisClient, v2client);
|
27
|
-
clientInitialized = true;
|
28
|
-
}
|
29
|
-
if (typeId !== "text") {
|
30
|
-
/* If the input is not text do nothing */
|
31
|
-
return;
|
32
|
-
}
|
33
|
-
|
34
|
-
const lowerContent = text?.toLowerCase();
|
35
|
-
|
36
|
-
//Handles unsubscribe and resets step
|
37
|
-
if (stopWords.some((word) => lowerContent.includes(word))) {
|
38
|
-
inMemoryCacheStep.set(sender.address, 0);
|
39
|
-
await redisClient.del(sender.address);
|
40
|
-
await context.reply(
|
41
|
-
"You are now unsubscribed. You will no longer receive updates!.",
|
42
|
-
);
|
43
|
-
return;
|
44
|
-
}
|
45
|
-
|
46
|
-
const cacheStep = inMemoryCacheStep.get(sender.address) || 0;
|
47
|
-
let message = "";
|
48
|
-
if (cacheStep === 0) {
|
49
|
-
message = "Welcome! Choose an option:\n1. Info\n2. Subscribe";
|
50
|
-
// Move to the next step
|
51
|
-
inMemoryCacheStep.set(sender.address, cacheStep + 1);
|
52
|
-
} else if (cacheStep === 1) {
|
53
|
-
if (text === "1") {
|
54
|
-
message = "Here is the info.";
|
55
|
-
} else if (text === "2") {
|
56
|
-
await redisClient.set(sender.address, "subscribed"); //test
|
57
|
-
message =
|
58
|
-
"You are now subscribed. You will receive updates.\n\ntype 'stop' to unsubscribe";
|
59
|
-
//reset the app to the initial step
|
60
|
-
inMemoryCacheStep.set(sender.address, 0);
|
61
|
-
} else {
|
62
|
-
message = "Invalid option. Please choose 1 for Info or 2 to Subscribe.";
|
63
|
-
// Keep the same step to allow for re-entry
|
64
|
-
}
|
65
|
-
} else {
|
66
|
-
message = "Invalid option. Please start again.";
|
67
|
-
inMemoryCacheStep.set(sender.address, 0);
|
68
|
-
}
|
69
|
-
|
70
|
-
//Send the message
|
71
|
-
await context.reply(message);
|
72
|
-
});
|
@@ -1,34 +0,0 @@
|
|
1
|
-
import cron from "node-cron";
|
2
|
-
import { Client } from "@xmtp/xmtp-js";
|
3
|
-
import { RedisClientType } from "@redis/client";
|
4
|
-
|
5
|
-
export async function startCron(
|
6
|
-
redisClient: RedisClientType,
|
7
|
-
v2client: Client,
|
8
|
-
) {
|
9
|
-
console.log("Starting daily cron");
|
10
|
-
const conversations = await v2client.conversations.list();
|
11
|
-
cron.schedule(
|
12
|
-
"0 0 * * *", // Daily or every 5 seconds in debug mode
|
13
|
-
async () => {
|
14
|
-
const keys = await redisClient.keys("*");
|
15
|
-
console.log(`Running daily task. ${keys.length} subscribers.`);
|
16
|
-
for (const address of keys) {
|
17
|
-
const subscriptionStatus = await redisClient.get(address);
|
18
|
-
if (subscriptionStatus === "subscribed") {
|
19
|
-
console.log(`Sending daily update to ${address}`);
|
20
|
-
// Logic to send daily updates to each subscriber
|
21
|
-
const targetConversation = conversations.find(
|
22
|
-
(conv) => conv.peerAddress === address,
|
23
|
-
);
|
24
|
-
if (targetConversation)
|
25
|
-
await targetConversation.send("Here is your daily update!");
|
26
|
-
}
|
27
|
-
}
|
28
|
-
},
|
29
|
-
{
|
30
|
-
scheduled: true,
|
31
|
-
timezone: "UTC",
|
32
|
-
},
|
33
|
-
);
|
34
|
-
}
|
@@ -1,15 +0,0 @@
|
|
1
|
-
import { createClient } from "@redis/client";
|
2
|
-
import { RedisClientType } from "@redis/client";
|
3
|
-
|
4
|
-
export const getRedisClient = async () => {
|
5
|
-
const client = createClient({
|
6
|
-
url: process.env.REDIS_CONNECTION_STRING,
|
7
|
-
});
|
8
|
-
|
9
|
-
client.on("error", (err) => {
|
10
|
-
console.error("Redis client error:", err);
|
11
|
-
});
|
12
|
-
|
13
|
-
await client.connect();
|
14
|
-
return client as RedisClientType;
|
15
|
-
};
|
File without changes
|
File without changes
|
File without changes
|