Avatar

vlad

19 public vals
Joined September 11, 2022
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import { fetch } from "https://esm.town/v/std/fetch";
export const sendMatrixChatRoomMessage = async (
accessToken: string,
roomId: string,
transactionId: string,
message: MessageEventContent,
serverUrl: string = "https://matrix.org",
) => {
const url = `${serverUrl}/_matrix/client/v3/rooms/${
encodeURIComponent(roomId)
}/send/m.room.message/${encodeURIComponent(transactionId)}`;
//
const config = {
method: "PUT",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(message),
};
return await fetch(url, config);
};
//
// Types
export interface MessageEventContent {
msgtype?: string;
body?: string;
format?: string;
formatted_body?: string;
info?: MessageEventContentInfo_;
url?: string;
membership?: RoomPhase;
"m.relates_to"?: {
event_id?: string;
is_falling_back?: boolean;
rel_type?: string;
"m.in_reply_to"?: {
[event_id: string]: string;
};
};
name?: string;
alias?: string;
join_rule?: string;
topic?: string;
display_name?: string;
displayname?: string;
avatar_url?: string;
is_direct?: boolean;
third_party_signed?: string;
last_active_ago?: number;
users?: {
[id: string]: number;
};
"m.new_content"?: MessageEventContent;
// todo
"org.matrix.msc1767.message"?: any[];
"org.matrix.msc1767.text"?: string;
// todo a big mess of fields that should be separated by event type
join_authorised_via_users_server?: string;
[other: string]: any;
}
export interface MessageEventContentInfo_ {
mimetype?: string;
size?: number;
h?: number;
w?: number;
thumbnail_url?: string;
thumbnail_info?: ThumbnailInfo_;
}
export interface ThumbnailInfo_ {
mimetype: string;
size: number;
h: number;
w: number;
}
export type RoomPhase = "join" | "invite" | "leave";
export interface LinkPreview_ {
url: string;
text?: string;
image_url?: string;
image_width?: number;
image_height?: number;
title?: string;
site_name?: string;
}
1
2
3
4
5
6
7
8
9
10
11
export function matrixMediaToHttpUrl(
mxcUrl: string,
homeserverDomain: string = "matrix.org",
) {
const mxcPrefix = "mxc://";
if (!mxcUrl.startsWith(mxcPrefix)) {
throw new Error("Invalid mxc URL");
}
const mxcUrlWithoutScheme = mxcUrl.slice(mxcPrefix.length);
return `https://${homeserverDomain}/_matrix/media/v3/download/${mxcUrlWithoutScheme}`;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { fetch } from "https://esm.town/v/std/fetch";
export const joinMatrixRoom = async (
accessToken: string,
roomId: string,
serverUrl: string = "https://matrix.org",
) => {
const url = `${serverUrl}/_matrix/client/v3/rooms/${
encodeURIComponent(roomId)
}/join`;
//
const config = {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
};
return await fetch(url, config);
};

A example Serverless Bot for Matrix chat. See https://vlad.roam.garden/How-to-create-a-serverless-Matrix-Chat-bot for a more detailed write-up on how to set one up!

To test this bot:

  • invite serverless-echo@matrix.org to your unencrypted room
  • Send a message starting with !echo and the bot will repeat content after.

Use https://matrix-serverless.netlify.app/ to configure Matrix to call the endpoint on newly received messages

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { sendMatrixChatRoomTextMessage } from "https://esm.town/v/vlad/sendMatrixChatRoomTextMessage";
import { joinMatrixRoom } from "https://esm.town/v/vlad/joinMatrixRoom";
import process from "node:process";
export async function serverlessMatrixEchoBot(
req: express.Request,
res: express.Response,
) {
// Todo: explicitly ignore encrypted messages, or matrix will keep try sending them
// as rn this will just error out
const event = req.body.notification;
const matrixToken = process.env.matrixEchoBot;
/**
* Accept all invites sent to the bot, generally this is optional
* (e.g. I have a personal bot that I only talk to in a specific room and so I just joined that room from bot account)
* but you'd want this if you want to make a bot anyone can interact with
*/
if (event.membership === "invite") {
await joinMatrixRoom(matrixToken, event.room_id);
return res.json({ rejected: [] });
}
//
const text = event.content.body;
const roomId = event.room_id;
//
const echoText = "!echo ";
if (text.startsWith(echoText)) {
await sendMatrixChatRoomTextMessage(
matrixToken,
roomId,
text.slice(echoText.length),
);
}
//
// This is important - if you don't return this JSON -
// Matrix won't consider notification as processed and will keep sending you the same one
return res.json({ rejected: [] });
}
1
2
3
4
export const formatDateToRoam = async (date: Date) => {
const { format } = await import("npm:date-fns");
return format(date, "MMMM do, yyyy");
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { sendMatrixChatRoomMessage } from "https://esm.town/v/vlad/sendMatrixChatRoomMessage";
export const sendMatrixChatRoomTextMessage = (
accessToken: string,
roomId: string,
message: string,
) => {
sendMatrixChatRoomMessage(
accessToken,
roomId,
new Date().toISOString(),
{ msgtype: "m.text", body: message },
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { GptApi } from "https://esm.town/v/xkonti/gptApiFramework";
import { Hono } from "npm:hono@3";
import { z } from "npm:zod";
/**
* INITIALIZE API
*/
const api = new GptApi({
url: "https://vlad-tempsimplegptexample.web.val.run",
title: "Some API",
description: "API for something",
version: "1.0.0",
});
// Example of action that doesn't accept any input, but returns something
const ResponseCommandSchema = z.object({
feedback: z.string().describe("Feedback regarding submitted action"),
data: z.string().optional().describe("Additional data related to the given command"),
}).describe("Contains feedback regarding submitted action");
export type ResponseCommand = z.infer<typeof ResponseCommandSchema>;
api.nothingToJson<ResponseCommand>({
verb: "POST",
path: "/some/endpoint",
operationId: "doSomething",
desc: "Does something to do something",
requestSchema: null,
requestDesc: null,
responseSchema: ResponseCommandSchema,
responseDesc: "Summary of executed action",
}, async (ctx) => {
// TODO: Do something / call something
return {
feedback: "Did something.",
data: "124 miles",
};
});
// Example of action that accept some data and returns something
const ThingsRequestSchema = z.object({
things: z.array(z.string().describe("A thing to do something with")).describe("A list of things "),
}).describe("Input for the action containing some things");
type ThingsRequest = z.infer<typeof ThingsRequestSchema>;
api.jsonToJson<ThingsRequest, ResponseCommand>({
verb: "POST",
path: "/submitthings",
operationId: "submit-things",
desc: "Endpoint for submitting some things",
requestSchema: ThingsRequestSchema,
requestDesc: "Things to do things with",
responseSchema: ResponseCommandSchema,
responseDesc: "Feedback on things",
}, async (ctx, dto) => {
// TODO: Do things to things
return {
feedback: "Things have been done to things.",
data: "15 things processed",
};
});
// Serve the API
export default api.serve();

Allows for automatic generation of Hono API comatible with GPTs. Endpoints' inputs and outputs need to be specified via types from which the Open API spec is generated automatically and available via /gpt/schema endpoint.

Usage example:

Create valimport { GptApi } from "https://esm.town/v/xkonti/gptApiFramework"; import { z } from "npm:zod"; /** * COMMON TYPES */ const ResponseCommandSchema = z.object({ feedback: z.string().describe("Feedback regarding submitted action"), command: z.string().describe("The command for the Mediator AI to follow strictly"), data: z.string().optional().describe("Additional data related to the given command"), }).describe("Contains feedback and further instructions to follow"); export type ResponseCommand = z.infer<typeof ResponseCommandSchema>; /** * INITIALIZE API */ const api = new GptApi({ url: "https://xkonti-planoverseerai.web.val.run", title: "Overseer AI API", description: "The API for interacting with the Overseer AI", version: "1.0.0", }); /** * REQUIREMENTS GATHERING ENDPOINTS */ api.nothingToJson<ResponseCommand>({ verb: "POST", path: "/newproblem", operationId: "new-problem", desc: "Endpoint for informing Overseer AI about a new problem presented by the User", requestSchema: null, requestDesc: null, responseSchema: ResponseCommandSchema, responseDesc: "Instruction on how to proceed with the new problem", }, async (ctx) => { return { feedback: "User input downloaded. Problem analysis is required.", command: await getPrompt("analyze-problem"), data: "", }; }); export default api.serve();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { EndpointDefinition, getOpenApiSpec } from "https://esm.town/v/xkonti/gptApiSchemaBuilder";
import { Hono } from "npm:hono@3";
import { z } from "npm:zod";
export interface ApiInfo {
url: string;
title: string;
description: string;
version: string;
}
export class GptApi {
app = new Hono();
info: ApiInfo;
endpoints: EndpointDefinition[] = [];
constructor(info: ApiInfo) {
this.info = info;
this.app.get("/gpt/schema", async (ctx) => {
const spec = getOpenApiSpec(
this.info.url,
this.info.title,
this.info.description,
this.info.version,
this.endpoints,
);
return ctx.text(JSON.stringify(spec, null, 2));
});
}
/**
* Register endpoint that ignores all inputs and returns a specific JSON response.
*/
nothingToJson<TResponse>(
endpointDef: EndpointDefinition,
handler: (ctx) => Promise<TResponse>,
) {
endpointDef.requestDesc = null;
endpointDef.requestSchema = null;
const handlerWrapper = async (ctx) => {
const response = await handler(ctx);
return ctx.json(response);
};
// TODO: Verify response stuff is in place
this.registerHandler(endpointDef, handlerWrapper);
}
/**
* Register endpoint that accepts a JSON DTO and returns a specific JSON response.
*/
jsonToJson<TRequest, TResponse>(
endpointDef: EndpointDefinition,
handler: (ctx, dto: TRequest) => Promise<TResponse>,
) {
// TODO: Verify request and response is in place
const handlerWrapper = async (ctx) => {
const data = await ctx.req.json() as TRequest;
// TODO: Handle invalid data format
const response = await handler(ctx, data);
return ctx.json(response);
};
this.registerHandler(endpointDef, handlerWrapper);
}
// Registers a handler for a verb + path combo.
registerHandler(
endpointDef: EndpointDefinition,
handler: (ctx) => any,
) {
this.endpoints.push(endpointDef);
const verb = endpointDef.verb;
const path = endpointDef.path;
switch (verb) {
case "GET":
this.app.get(path, handler);
break;
case "POST":
this.app.post(path, handler);
break;
case "PUT":
this.app.put(path, handler);
break;
case "DELETE":
this.app.delete(path, handler);
break;
case "PATCH":
this.app.patch(path, handler);
break;
default:
throw new Error(`HTTP verb ${verb} not supported`);
}
}
/**
* Usage: `export default gptApi.serve();`
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { fetch } from "https://esm.town/v/std/fetch";
export let blocks_inner = async (request: Request) => {
const [_, user, id, ...file] = new URL(request.url).pathname.split("/");
const gist = await fetch(`https://api.github.com/gists/${id}`).then((r) => r.json());
// res.set("Cache-Control", "public,max-age=64800");
// if (file.filter(Boolean).length) {
// if (gist.files[file.filter(Boolean).join("/")].raw_url.endsWith(".js")) {
// res.set("Content-Type", "application/javascript");
// return res.send(
// await fetch(gist.files[file.filter(Boolean).join("/")].raw_url).then((
// r,
// ) => r.text()),
// );
// }
// return res.redirect(gist.files[file.filter(Boolean).join("/")].raw_url);
// }
// console.log(gist);
const index = gist.files["index.html"];
if (!index) {
// return res.end({ message: "Gist did not contain index.html" });
}
return new Response(
`
${index.content}
`,
{
headers: {
"Content-Type": "text/html",
"Cache-Control": "public,max-age=64800",
},
},
);
};
// Forked from @tmcw.blocks_inner
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { fetch } from "https://esm.town/v/std/fetch";
export const saveToReadwiseReader = async (
params: ReaderSaveParams,
token: string,
) => {
return fetch("https://readwise.io/api/v3/save/", {
method: "POST",
headers: {
"Authorization": `Token ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(params),
});
};
interface ReaderSaveParams {
url: string;
html?: string;
}