create-message-kit 1.0.15 → 1.0.16
Sign up to get free protection for your applications and to get access to all the features.
- 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
|