Add ai functionality
This commit is contained in:
parent
da66cf9001
commit
dd4da06753
19 changed files with 1118 additions and 52 deletions
|
|
@ -9,11 +9,17 @@
|
|||
},
|
||||
"app": {
|
||||
"triggerPrefix": "!",
|
||||
"bowlingRoomId": "",
|
||||
"experience": {
|
||||
"gain": 5,
|
||||
"startingRequirement": 50,
|
||||
"multiplier": 1.25,
|
||||
"timeout": 60000
|
||||
},
|
||||
"ai": {
|
||||
"api": {
|
||||
"key": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { config } from "./config.js";
|
||||
import { state } from "./store/store.js";
|
||||
import { type IUser, type TRole } from "./store/types.js";
|
||||
import type { IRank } from "./types.js";
|
||||
import type { ILevel } from "./types.js";
|
||||
|
||||
const getUserById = (userId: string): IUser => {
|
||||
return (
|
||||
|
|
@ -19,22 +19,22 @@ const checkRoles = (roles: TRole[], userId: string) => {
|
|||
return roles.includes(user.role);
|
||||
};
|
||||
|
||||
const getRank = (experience: number): IRank => {
|
||||
const getLevel = (experience: number): ILevel => {
|
||||
let tmpExperience = experience;
|
||||
let expToNextRank = config.app.experience.startingRequirement;
|
||||
let rank = 0;
|
||||
let expToNextLevel = config.app.experience.startingRequirement;
|
||||
let level = 0;
|
||||
|
||||
while (tmpExperience >= expToNextRank) {
|
||||
rank++;
|
||||
tmpExperience -= expToNextRank;
|
||||
expToNextRank = expToNextRank *= config.app.experience.multiplier;
|
||||
while (tmpExperience >= expToNextLevel) {
|
||||
level++;
|
||||
tmpExperience -= expToNextLevel;
|
||||
expToNextLevel = expToNextLevel *= config.app.experience.multiplier;
|
||||
}
|
||||
|
||||
return {
|
||||
rank: rank,
|
||||
level: level,
|
||||
experience: Math.floor(experience),
|
||||
experienceInRank: Math.floor(tmpExperience),
|
||||
expToNextRank: Math.floor(expToNextRank),
|
||||
experienceInLevel: Math.floor(tmpExperience),
|
||||
expToNextLevel: Math.floor(expToNextLevel),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -48,4 +48,4 @@ const getUserName = (user: IUser): string => {
|
|||
return match.replaceAll("@", "");
|
||||
};
|
||||
|
||||
export { getUserById, checkRoles, getRank, getUserName };
|
||||
export { getUserById, checkRoles, getLevel, getUserName };
|
||||
|
|
|
|||
|
|
@ -12,35 +12,41 @@ const registerModuleAdmin = (
|
|||
client = matrixClient;
|
||||
|
||||
callbackStore.messageCallbacks.push({
|
||||
startCondition: `${config.app.triggerPrefix}shutdown`,
|
||||
startConditions: [`${config.app.triggerPrefix}shutdown`],
|
||||
allowedRoles: ["ADMIN"],
|
||||
callbackFunc: onShutdown,
|
||||
});
|
||||
callbackStore.messageCallbacks.push({
|
||||
startCondition: `${config.app.triggerPrefix}loaddata`,
|
||||
startConditions: [`${config.app.triggerPrefix}loaddata`],
|
||||
allowedRoles: ["MODERATOR", "ADMIN"],
|
||||
callbackFunc: onLoadData,
|
||||
});
|
||||
callbackStore.messageCallbacks.push({
|
||||
startCondition: `${config.app.triggerPrefix}savedata`,
|
||||
startConditions: [`${config.app.triggerPrefix}savedata`],
|
||||
allowedRoles: ["MODERATOR", "ADMIN"],
|
||||
callbackFunc: onSaveData,
|
||||
});
|
||||
};
|
||||
|
||||
const onShutdown = (text: string) => {
|
||||
const onShutdown = (text: string, roomId: string) => {
|
||||
if (!text.includes("nosave")) {
|
||||
client.sendTextMessage(roomId, "Saving data...");
|
||||
save();
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
const sendPromise = client.sendTextMessage(roomId, "Shutting down...");
|
||||
sendPromise.finally(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
const onLoadData = () => {
|
||||
const onLoadData = (_text: string, roomId: string) => {
|
||||
client.sendTextMessage(roomId, "Loading data...");
|
||||
load();
|
||||
};
|
||||
|
||||
const onSaveData = () => {
|
||||
const onSaveData = (_text: string, roomId: string) => {
|
||||
client.sendTextMessage(roomId, "Saving data...");
|
||||
save();
|
||||
};
|
||||
|
||||
|
|
|
|||
140
src/modules/ai/ai.ts
Normal file
140
src/modules/ai/ai.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { MatrixClient, MsgType } from "matrix-js-sdk";
|
||||
import type { ICallbackStore } from "../types.js";
|
||||
import { config } from "../../config.js";
|
||||
import { getImageNanoBanana, getTextGemini } from "../../services/ai/ai.js";
|
||||
import type { IAnimal } from "./types.js";
|
||||
|
||||
let client: MatrixClient;
|
||||
|
||||
const registerModuleAI = (
|
||||
matrixClient: MatrixClient,
|
||||
callbackStore: ICallbackStore,
|
||||
) => {
|
||||
client = matrixClient;
|
||||
|
||||
callbackStore.messageCallbacks.push({
|
||||
startConditions: [`${config.app.triggerPrefix}ai `],
|
||||
callbackFunc: onAI,
|
||||
});
|
||||
callbackStore.messageCallbacks.push({
|
||||
startConditions: [
|
||||
`satek when `,
|
||||
`satek ked `,
|
||||
`gabor when `,
|
||||
`gabor ked `,
|
||||
`martin when `,
|
||||
`martin ked `,
|
||||
`madys when `,
|
||||
`madys ked `,
|
||||
`mandak when `,
|
||||
`mandak ked `,
|
||||
`mando when `,
|
||||
`mando ked `,
|
||||
`mandik when `,
|
||||
`mandik ked `,
|
||||
`madik when `,
|
||||
`madik ked `,
|
||||
`janys when `,
|
||||
`janys ked `,
|
||||
`jano when `,
|
||||
`jano ked `,
|
||||
],
|
||||
callbackFunc: onImageGen,
|
||||
});
|
||||
};
|
||||
|
||||
const getAnimal = (name: string): IAnimal | undefined => {
|
||||
const animals: IAnimal[] = [
|
||||
{
|
||||
name: "satek",
|
||||
animal: "black cat",
|
||||
},
|
||||
{
|
||||
name: "gabor",
|
||||
animal: "hedgehog",
|
||||
},
|
||||
{
|
||||
name: "martin",
|
||||
animal: "hedgehog",
|
||||
},
|
||||
{
|
||||
name: "madys",
|
||||
animal: "beaver",
|
||||
},
|
||||
{
|
||||
name: "mandak",
|
||||
animal: "beaver",
|
||||
},
|
||||
{
|
||||
name: "mando",
|
||||
animal: "beaver",
|
||||
},
|
||||
{
|
||||
name: "mandik",
|
||||
animal: "beaver",
|
||||
},
|
||||
{
|
||||
name: "madik",
|
||||
animal: "beaver",
|
||||
},
|
||||
{
|
||||
name: "janys",
|
||||
animal: "lion",
|
||||
},
|
||||
{
|
||||
name: "jano",
|
||||
animal: "lion",
|
||||
},
|
||||
];
|
||||
|
||||
const foundAnimals = animals
|
||||
.map((animal) => {
|
||||
if (name.includes(animal.name)) {
|
||||
return animal;
|
||||
}
|
||||
})
|
||||
.filter((animal) => animal);
|
||||
|
||||
return foundAnimals.at(0);
|
||||
};
|
||||
|
||||
const onAI = async (text: string, roomId: string) => {
|
||||
if (text.trim().length < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
const responseAI = await getTextGemini(text.replace("!ai ", ""));
|
||||
client.sendTextMessage(roomId, responseAI);
|
||||
};
|
||||
|
||||
const onImageGen = async (text: string, roomId: string) => {
|
||||
const animal = getAnimal(text.trim().split(/\s+/)[0] ?? "");
|
||||
if (!animal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textMod = text.replace(animal.name, animal.animal);
|
||||
const buffer = await getImageNanoBanana(textMod.trim());
|
||||
if (!buffer || buffer.length < 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageName = `photo-${animal.name}.png`;
|
||||
|
||||
const uploadResult = await client.uploadContent(buffer, {
|
||||
type: "image/png",
|
||||
name: imageName,
|
||||
});
|
||||
|
||||
await client.sendMessage(roomId, {
|
||||
msgtype: MsgType.Image,
|
||||
body: imageName,
|
||||
url: uploadResult.content_uri,
|
||||
info: {
|
||||
mimetype: "image/png",
|
||||
size: buffer.length,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { getAnimal, registerModuleAI, onImageGen };
|
||||
2
src/modules/ai/index.ts
Normal file
2
src/modules/ai/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./ai.js";
|
||||
export * from "./types.js";
|
||||
6
src/modules/ai/types.ts
Normal file
6
src/modules/ai/types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
interface IAnimal {
|
||||
name: string;
|
||||
animal: string;
|
||||
}
|
||||
|
||||
export { type IAnimal };
|
||||
|
|
@ -4,22 +4,26 @@ import { config } from "../../config.js";
|
|||
|
||||
let client: MatrixClient;
|
||||
|
||||
const registerModuleTest = (
|
||||
const registerModuleBase = (
|
||||
matrixClient: MatrixClient,
|
||||
callbackStore: ICallbackStore,
|
||||
) => {
|
||||
client = matrixClient;
|
||||
|
||||
callbackStore.messageCallbacks.push({
|
||||
startCondition: `${config.app.triggerPrefix}ping`,
|
||||
startConditions: [`${config.app.triggerPrefix}ping`],
|
||||
callbackFunc: onPing,
|
||||
});
|
||||
callbackStore.messageCallbacks.push({
|
||||
startCondition: `${config.app.triggerPrefix}say `,
|
||||
startConditions: [`${config.app.triggerPrefix}say `],
|
||||
callbackFunc: onSay,
|
||||
});
|
||||
callbackStore.messageCallbacks.push({
|
||||
startCondition: `${config.app.triggerPrefix}help`,
|
||||
startConditions: [`${config.app.triggerPrefix}bowling `],
|
||||
callbackFunc: onBowling,
|
||||
});
|
||||
callbackStore.messageCallbacks.push({
|
||||
startConditions: [`${config.app.triggerPrefix}help`],
|
||||
callbackFunc: onHelp,
|
||||
});
|
||||
};
|
||||
|
|
@ -34,6 +38,12 @@ const onSay = (text: string, roomId: string) => {
|
|||
client.sendTextMessage(roomId, text.replace(trigger, ""));
|
||||
};
|
||||
|
||||
const onBowling = (text: string, _roomId: string) => {
|
||||
const trigger = `${config.app.triggerPrefix}bowling `;
|
||||
|
||||
client.sendTextMessage(config.app.bowlingRoomId, text.replace(trigger, ""));
|
||||
};
|
||||
|
||||
const onHelp = (_text: string, roomId: string) => {
|
||||
client.sendHtmlMessage(
|
||||
roomId,
|
||||
|
|
@ -42,9 +52,11 @@ const onHelp = (_text: string, roomId: string) => {
|
|||
<ul>
|
||||
<li><b>!ping</b> - Pong!</li>
|
||||
<li><b>!say {text}</b> - Repeats your message</li>
|
||||
<li><b>!bowling {text}</b> - Repeats your message in bowling</li>
|
||||
<li><b>!help</b> - Prints this help message</li>
|
||||
<li><b>!rank</b> - Prints your rank and experience</li>
|
||||
<li><b>!level</b> - Prints your level and experience</li>
|
||||
<li><b>!leaderboard</b> - Prints total user ranking</li>
|
||||
<li><b>!ai {text}</b> - Say something to Gemini 3</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<h3>Role: Moderator</h3>
|
||||
|
|
@ -60,4 +72,4 @@ const onHelp = (_text: string, roomId: string) => {
|
|||
);
|
||||
};
|
||||
|
||||
export { registerModuleTest };
|
||||
export { registerModuleBase };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { MatrixClient } from "matrix-js-sdk";
|
||||
import type { TRole } from "../store/types.js";
|
||||
import { getRank, getUserById } from "../helpers.js";
|
||||
import { getLevel, getUserById } from "../helpers.js";
|
||||
import { config } from "../config.js";
|
||||
import { state } from "../store/store.js";
|
||||
|
||||
|
|
@ -23,19 +23,19 @@ const onAnyMessage = (
|
|||
return onAnyMessage(client, _text, roomId, sender);
|
||||
}
|
||||
|
||||
const rankBefore = getRank(user.experience);
|
||||
const levelBefore = getLevel(user.experience);
|
||||
|
||||
if (date > user.lastMessageTimestamp + config.app.experience.timeout) {
|
||||
user.experience += config.app.experience.gain;
|
||||
}
|
||||
user.lastMessageTimestamp = date;
|
||||
|
||||
const rankAfter = getRank(user.experience);
|
||||
if (rankAfter.rank > rankBefore.rank) {
|
||||
const levelAfter = getLevel(user.experience);
|
||||
if (levelAfter.level > levelBefore.level) {
|
||||
client.sendHtmlMessage(
|
||||
roomId,
|
||||
"",
|
||||
`${sender} - You are now rank <b>${rankAfter.rank}</b>`,
|
||||
`${sender} - You are now level <b>${levelAfter.level}</b>`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import {
|
|||
RoomEvent,
|
||||
type IContent,
|
||||
} from "matrix-js-sdk";
|
||||
import { registerModuleTest } from "./base/base.js";
|
||||
import type { ICallback, ICallbackStore } from "./types.js";
|
||||
import { registerModuleBase } from "./base/base.js";
|
||||
import { registerModuleAdmin } from "./admin/admin.js";
|
||||
import { registerModuleUser } from "./user/user.js";
|
||||
import { registerModuleAI } from "./ai/ai.js";
|
||||
import { checkRoles, getUserById } from "../helpers.js";
|
||||
import { onAnyMessage, onMissingRole } from "./global.js";
|
||||
import { config } from "../config.js";
|
||||
|
|
@ -27,13 +28,20 @@ const checkMessageCallback = (
|
|||
return false;
|
||||
}
|
||||
|
||||
if (callback.startCondition && !text.startsWith(callback.startCondition)) {
|
||||
if (
|
||||
callback.startConditions &&
|
||||
!callback.startConditions.some((condition) =>
|
||||
text.startsWith(condition),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
callback.includesCondition &&
|
||||
!text.includes(callback.includesCondition)
|
||||
callback.includesConditions &&
|
||||
!callback.includesConditions.some((condition) =>
|
||||
text.includes(condition),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -94,9 +102,10 @@ const registerModules = (client: MatrixClient) => {
|
|||
});
|
||||
});
|
||||
|
||||
registerModuleTest(client, callbacks);
|
||||
registerModuleBase(client, callbacks);
|
||||
registerModuleAdmin(client, callbacks);
|
||||
registerModuleUser(client, callbacks);
|
||||
registerModuleAI(client, callbacks);
|
||||
};
|
||||
|
||||
export { registerModules };
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ interface ICallbackStore {
|
|||
}
|
||||
|
||||
interface ICallback {
|
||||
startCondition?: string;
|
||||
includesCondition?: string;
|
||||
startConditions?: string[];
|
||||
includesConditions?: string[];
|
||||
allowedRoles?: TRole[];
|
||||
allowedRooms?: string;
|
||||
callbackFunc: (text: string, roomId: string, sender: string) => void;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { MatrixClient } from "matrix-js-sdk";
|
||||
import type { ICallbackStore } from "../types.js";
|
||||
import { config } from "../../config.js";
|
||||
import { getRank, getUserById, getUserName } from "../../helpers.js";
|
||||
import { getLevel, getUserById, getUserName } from "../../helpers.js";
|
||||
import { state } from "../../store/store.js";
|
||||
import type { IUser } from "../../store/types.js";
|
||||
let client: MatrixClient;
|
||||
|
|
@ -13,31 +13,35 @@ const registerModuleUser = (
|
|||
client = matrixClient;
|
||||
|
||||
callbackStore.messageCallbacks.push({
|
||||
startCondition: `${config.app.triggerPrefix}rank`,
|
||||
callbackFunc: onRank,
|
||||
startConditions: [`${config.app.triggerPrefix}level`],
|
||||
callbackFunc: onLevel,
|
||||
});
|
||||
callbackStore.messageCallbacks.push({
|
||||
startCondition: `${config.app.triggerPrefix}leaderboard`,
|
||||
startConditions: [`${config.app.triggerPrefix}leaderboard`],
|
||||
callbackFunc: onLeaderboard,
|
||||
});
|
||||
};
|
||||
|
||||
const onRank = (_text: string, roomId: string, sender: string) => {
|
||||
const rank = getRank(getUserById(sender).experience);
|
||||
const onLevel = (_text: string, roomId: string, sender: string) => {
|
||||
const level = getLevel(getUserById(sender).experience);
|
||||
|
||||
client.sendHtmlMessage(
|
||||
roomId,
|
||||
"",
|
||||
`<h3>Your Rank: ${rank.rank}</h3>
|
||||
<i>Next rank progress: ${rank.experienceInRank}/${rank.expToNextRank}exp</i>`,
|
||||
`<h3>Your Level: <b>${level.level}</b></h3>
|
||||
</br>
|
||||
<i>Next level progress: ${level.experienceInLevel}/${level.expToNextLevel}xp</i>`,
|
||||
);
|
||||
};
|
||||
|
||||
const onLeaderboard = (_text: string, roomId: string) => {
|
||||
const mapUsersToLeaderboard = (user: IUser): string => {
|
||||
const rank = getRank(user.experience);
|
||||
const level = getLevel(user.experience);
|
||||
const userName = getUserName(user);
|
||||
const userNameMod =
|
||||
userName.charAt(0).toUpperCase() + userName.slice(1);
|
||||
|
||||
return `<li>${getUserName(user)}: rank ${rank.rank} (${rank.experienceInRank}/${rank.expToNextRank}exp)</li>`;
|
||||
return `<li>${userNameMod}: level <b>${level.level}</b> (${level.experienceInLevel}/${level.expToNextLevel}xp)</li>`;
|
||||
};
|
||||
|
||||
const users = state.users.sort(
|
||||
|
|
|
|||
44
src/services/ai/ai.ts
Normal file
44
src/services/ai/ai.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { GoogleGenAI } from "@google/genai";
|
||||
import { config } from "../../config.js";
|
||||
|
||||
const googleAI = new GoogleGenAI({
|
||||
apiKey: config.app.ai.api.key,
|
||||
});
|
||||
|
||||
const getTextGemini = async (input: string): Promise<string> => {
|
||||
const response = await googleAI.models.generateContent({
|
||||
model: "gemini-3-flash-preview",
|
||||
contents: input,
|
||||
});
|
||||
|
||||
return response.text ?? "AI Error";
|
||||
};
|
||||
|
||||
const getImageNanoBanana = async (
|
||||
input: string,
|
||||
): Promise<Buffer<ArrayBuffer> | undefined> => {
|
||||
const response = await googleAI.models.generateContent({
|
||||
model: "gemini-2.5-flash-image",
|
||||
contents: input,
|
||||
});
|
||||
|
||||
const firstCandidate = (response.candidates ?? [])[0];
|
||||
const parts = firstCandidate?.content?.parts ?? [];
|
||||
|
||||
let buffer: Buffer<ArrayBuffer> | undefined = undefined;
|
||||
|
||||
parts.forEach((part) => {
|
||||
if (part.inlineData) {
|
||||
const imageData = part.inlineData.data;
|
||||
if (!imageData) {
|
||||
return;
|
||||
}
|
||||
|
||||
buffer = Buffer.from(imageData, "base64");
|
||||
}
|
||||
});
|
||||
|
||||
return buffer;
|
||||
};
|
||||
|
||||
export { getTextGemini, getImageNanoBanana };
|
||||
1
src/services/ai/index.ts
Normal file
1
src/services/ai/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./ai.js";
|
||||
10
src/types.ts
10
src/types.ts
|
|
@ -1,8 +1,8 @@
|
|||
interface IRank {
|
||||
rank: number;
|
||||
interface ILevel {
|
||||
level: number;
|
||||
experience: number;
|
||||
experienceInRank: number;
|
||||
expToNextRank: number;
|
||||
experienceInLevel: number;
|
||||
expToNextLevel: number;
|
||||
}
|
||||
|
||||
export { type IRank };
|
||||
export { type ILevel };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue