387 lines
12 KiB
TypeScript
387 lines
12 KiB
TypeScript
|
import { PlatformAdapter, FetchOptions } from "./platform-adapter";
|
||
|
import {
|
||
|
Message as StdMessage,
|
||
|
SentMessage,
|
||
|
User as StdUser,
|
||
|
Attachment,
|
||
|
User,
|
||
|
} from "./message";
|
||
|
import {
|
||
|
Client,
|
||
|
GatewayIntentBits,
|
||
|
Message as DiscordMessage,
|
||
|
TextChannel,
|
||
|
Partials,
|
||
|
ChannelType,
|
||
|
ActivityType,
|
||
|
} from "discord.js";
|
||
|
import { UserConfig, userConfigs } from "../config";
|
||
|
|
||
|
export class DiscordAdapter implements PlatformAdapter {
|
||
|
private client: Client;
|
||
|
private botUserId: string = "";
|
||
|
|
||
|
public config = {
|
||
|
indicators: {
|
||
|
typing: true,
|
||
|
processing: true,
|
||
|
},
|
||
|
};
|
||
|
|
||
|
constructor() {
|
||
|
this.client = new Client({
|
||
|
intents: [
|
||
|
GatewayIntentBits.Guilds,
|
||
|
GatewayIntentBits.GuildMessages,
|
||
|
GatewayIntentBits.MessageContent,
|
||
|
GatewayIntentBits.DirectMessages,
|
||
|
],
|
||
|
partials: [Partials.Channel],
|
||
|
});
|
||
|
|
||
|
this.client.on("ready", () => {
|
||
|
console.log(`Logged in as ${this.client.user?.tag}!`);
|
||
|
this.botUserId = this.client.user?.id || "";
|
||
|
this.client.user?.setActivity("as Human", {
|
||
|
type: Number(ActivityType.Playing),
|
||
|
});
|
||
|
});
|
||
|
|
||
|
this.client.login(process.env.DISCORD_BOT_TOKEN);
|
||
|
}
|
||
|
|
||
|
public getUserById(userId: string): UserConfig | null {
|
||
|
const userConfig = userConfigs.find((user) =>
|
||
|
user.identities.some(
|
||
|
(identity) => identity.platform === "discord" && identity.id === userId
|
||
|
)
|
||
|
);
|
||
|
|
||
|
if (!userConfig) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return userConfig;
|
||
|
}
|
||
|
|
||
|
public onMessage(callback: (message: StdMessage) => void): void {
|
||
|
this.client.on("messageCreate", async (discordMessage: DiscordMessage) => {
|
||
|
if (discordMessage.author.bot) return;
|
||
|
|
||
|
if (discordMessage.channel.type !== ChannelType.DM) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// if user does not exist in userConfigs dont reply
|
||
|
const userConfig = this.getUserById(discordMessage.author.id);
|
||
|
if (!userConfig) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const message = await this.convertMessage(discordMessage);
|
||
|
callback(message);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
public async sendMessage(channelId: string, content: string): Promise<void> {
|
||
|
const channel = await this.client.channels.fetch(channelId);
|
||
|
if (
|
||
|
channel?.type !== ChannelType.GuildText &&
|
||
|
channel?.type !== ChannelType.DM
|
||
|
) {
|
||
|
console.error("Invalid channel type", channel?.type, channelId);
|
||
|
return;
|
||
|
}
|
||
|
await (channel as TextChannel).send(content);
|
||
|
}
|
||
|
public async fetchMessageById(
|
||
|
channelId: string,
|
||
|
messageId: string
|
||
|
): Promise<StdMessage | null> {
|
||
|
const channel = await this.client.channels.fetch(channelId);
|
||
|
if (
|
||
|
!channel ||
|
||
|
(channel.type !== ChannelType.GuildText &&
|
||
|
channel.type !== ChannelType.DM)
|
||
|
) {
|
||
|
throw new Error("Invalid channel type");
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
const discordMessage = await (channel as TextChannel).messages.fetch(
|
||
|
messageId
|
||
|
);
|
||
|
const stdMessage = await this.convertMessage(discordMessage);
|
||
|
return stdMessage;
|
||
|
} catch (error) {
|
||
|
console.error(`Failed to fetch message by ID: ${error}`);
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
public async fetchMessages(
|
||
|
channelId: string,
|
||
|
options: FetchOptions
|
||
|
): Promise<StdMessage[]> {
|
||
|
const channel = await this.client.channels.fetch(channelId);
|
||
|
if (
|
||
|
!channel ||
|
||
|
(channel.type !== ChannelType.GuildText &&
|
||
|
channel.type !== ChannelType.DM)
|
||
|
) {
|
||
|
throw new Error("Invalid channel type");
|
||
|
}
|
||
|
|
||
|
const messages = await (channel as TextChannel).messages.fetch({
|
||
|
limit: options.limit || 10,
|
||
|
});
|
||
|
const stdMessages: StdMessage[] = [];
|
||
|
|
||
|
for (const msg of messages.values()) {
|
||
|
const stdMsg = await this.convertMessage(msg);
|
||
|
stdMessages.push(stdMsg);
|
||
|
}
|
||
|
|
||
|
// Return messages in chronological order
|
||
|
return stdMessages;
|
||
|
}
|
||
|
|
||
|
public getBotId(): string {
|
||
|
return this.botUserId;
|
||
|
}
|
||
|
|
||
|
public async sendSystemLog(content: string) {
|
||
|
if (process.env.DISCORD_LOG_CHANNEL_ID)
|
||
|
return await this.sendMessage(
|
||
|
process.env.DISCORD_LOG_CHANNEL_ID || "",
|
||
|
content
|
||
|
);
|
||
|
}
|
||
|
|
||
|
public async searchUser(query: string): Promise<User[]> {
|
||
|
const users = this.client.users.cache;
|
||
|
return users
|
||
|
.filter((user) =>
|
||
|
user.username.toLowerCase().includes(query.toLowerCase())
|
||
|
)
|
||
|
.map((user) => ({
|
||
|
id: user.id,
|
||
|
username: user.username,
|
||
|
config: this.getUserById(user.id),
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
// Method to create a Message interface for a user ID
|
||
|
public async createMessageInterface(userId: string): Promise<StdMessage> {
|
||
|
try {
|
||
|
const user = await this.client.users.fetch(userId);
|
||
|
|
||
|
console.log("creating message interface for: ", userId, user.username);
|
||
|
|
||
|
const stdMessage: StdMessage = {
|
||
|
platform: "discord",
|
||
|
platformAdapter: this,
|
||
|
id: userId,
|
||
|
author: {
|
||
|
id: userId,
|
||
|
username: user.username,
|
||
|
config: this.getUserById(userId),
|
||
|
},
|
||
|
content: "",
|
||
|
timestamp: new Date(),
|
||
|
channelId: "",
|
||
|
source: null,
|
||
|
threadId: undefined,
|
||
|
isDirectMessage: async () => true,
|
||
|
send: async (messageData) => {
|
||
|
const sentMessage = await user.send(messageData);
|
||
|
return this.convertSentMessage(sentMessage);
|
||
|
},
|
||
|
reply: async (messageData) => {
|
||
|
const sentMessage = await user.send(messageData);
|
||
|
return this.convertSentMessage(sentMessage);
|
||
|
},
|
||
|
getUserRoles: () => {
|
||
|
const userConfig = userConfigs.find((userConfig) =>
|
||
|
userConfig.identities.some(
|
||
|
(identity) =>
|
||
|
identity.platform === "discord" && identity.id === userId
|
||
|
)
|
||
|
);
|
||
|
return userConfig ? userConfig.roles : ["user"];
|
||
|
},
|
||
|
sendDirectMessage: async (userId, messageData) => {
|
||
|
const user = await this.client.users.fetch(userId);
|
||
|
console.log("sending message to: ", userId);
|
||
|
await user.send(messageData);
|
||
|
},
|
||
|
sendMessageToChannel: async (channelId, messageData) => {
|
||
|
const channel = await this.client.channels.fetch(channelId);
|
||
|
if (channel?.isTextBased()) {
|
||
|
await (channel as TextChannel).send(messageData);
|
||
|
}
|
||
|
},
|
||
|
fetchChannelMessages: async (limit: number) => {
|
||
|
const messages = await user.dmChannel?.messages.fetch({ limit });
|
||
|
return Promise.all(
|
||
|
messages?.map((msg) => this.convertMessage(msg)) || []
|
||
|
);
|
||
|
},
|
||
|
sendFile: async (fileUrl, fileName) => {
|
||
|
await user.dmChannel?.send({
|
||
|
files: [{ attachment: fileUrl, name: fileName }],
|
||
|
});
|
||
|
},
|
||
|
sendTyping: async () => {
|
||
|
await user.dmChannel?.sendTyping();
|
||
|
},
|
||
|
};
|
||
|
|
||
|
return stdMessage;
|
||
|
} catch (error) {
|
||
|
throw new Error(
|
||
|
`Failed to create message interface for Discord user ${userId}: ${error}`
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// You may also need to expose this method so it can be accessed elsewhere
|
||
|
public getMessageInterface = this.createMessageInterface;
|
||
|
|
||
|
private async convertMessage(
|
||
|
discordMessage: DiscordMessage
|
||
|
): Promise<StdMessage> {
|
||
|
const stdUser: StdUser = {
|
||
|
id: discordMessage.author.id,
|
||
|
username: discordMessage.author.username,
|
||
|
config: this.getUserById(discordMessage.author.id),
|
||
|
};
|
||
|
|
||
|
const attachments: Attachment[] = discordMessage.attachments.map(
|
||
|
(attachment) => ({
|
||
|
url: attachment.url,
|
||
|
contentType: attachment.contentType || undefined,
|
||
|
})
|
||
|
);
|
||
|
|
||
|
const stdMessage: StdMessage = {
|
||
|
id: discordMessage.id,
|
||
|
content: discordMessage.content,
|
||
|
platformAdapter: this,
|
||
|
author: stdUser,
|
||
|
timestamp: discordMessage.createdAt,
|
||
|
channelId: discordMessage.channelId,
|
||
|
threadId: discordMessage.reference?.messageId || undefined,
|
||
|
source: discordMessage,
|
||
|
platform: "discord",
|
||
|
attachments,
|
||
|
isDirectMessage: async () =>
|
||
|
discordMessage.channel.type === ChannelType.DM,
|
||
|
send: async (messageData) => {
|
||
|
const sentMessage = await discordMessage.channel.send(messageData);
|
||
|
return this.convertSentMessage(sentMessage);
|
||
|
},
|
||
|
reply: async (messageData) => {
|
||
|
const sentMessage = await discordMessage.reply(messageData);
|
||
|
return this.convertSentMessage(sentMessage);
|
||
|
},
|
||
|
getUserRoles: () => {
|
||
|
const userConfig = userConfigs.find((user) =>
|
||
|
user.identities.some(
|
||
|
(identity) =>
|
||
|
identity.platform === "discord" &&
|
||
|
identity.id === discordMessage.author.id
|
||
|
)
|
||
|
);
|
||
|
return userConfig ? userConfig.roles : ["user"];
|
||
|
},
|
||
|
sendDirectMessage: async (userId, messageData) => {
|
||
|
const user = await this.client.users.fetch(userId);
|
||
|
await user.send(messageData);
|
||
|
},
|
||
|
sendMessageToChannel: async (channelId, messageData) => {
|
||
|
const channel = await this.client.channels.fetch(channelId);
|
||
|
if (channel?.isTextBased()) {
|
||
|
await (channel as TextChannel).send(messageData);
|
||
|
}
|
||
|
},
|
||
|
fetchChannelMessages: async (limit: number) => {
|
||
|
const messages = await discordMessage.channel.messages.fetch({ limit });
|
||
|
return Promise.all(messages.map((msg) => this.convertMessage(msg)));
|
||
|
},
|
||
|
sendFile: async (fileUrl, fileName) => {
|
||
|
await discordMessage.channel.send({
|
||
|
files: [{ attachment: fileUrl, name: fileName }],
|
||
|
});
|
||
|
},
|
||
|
sendTyping: async () => {
|
||
|
await discordMessage.channel.sendTyping();
|
||
|
},
|
||
|
};
|
||
|
|
||
|
return stdMessage;
|
||
|
}
|
||
|
|
||
|
private convertSentMessage(discordMessage: DiscordMessage): SentMessage {
|
||
|
return {
|
||
|
id: discordMessage.id,
|
||
|
platformAdapter: this,
|
||
|
content: discordMessage.content,
|
||
|
author: {
|
||
|
id: discordMessage.author.id,
|
||
|
username: discordMessage.author.username,
|
||
|
config: this.getUserById(discordMessage.author.id),
|
||
|
},
|
||
|
timestamp: discordMessage.createdAt,
|
||
|
channelId: discordMessage.channelId,
|
||
|
threadId: discordMessage.reference?.messageId || undefined,
|
||
|
source: discordMessage,
|
||
|
platform: "discord",
|
||
|
deletable: discordMessage.deletable,
|
||
|
delete: async () => {
|
||
|
if (discordMessage.deletable) {
|
||
|
await discordMessage.delete();
|
||
|
}
|
||
|
},
|
||
|
edit: async (data) => {
|
||
|
await discordMessage.edit(data);
|
||
|
},
|
||
|
getUserRoles: () => {
|
||
|
// Since this is a message sent by the bot, return bot's roles or empty array
|
||
|
return [];
|
||
|
},
|
||
|
isDirectMessage: async () =>
|
||
|
discordMessage.channel.type === ChannelType.DM,
|
||
|
sendDirectMessage: async (userId, messageData) => {
|
||
|
const user = await this.client.users.fetch(userId);
|
||
|
await user.send(messageData);
|
||
|
},
|
||
|
sendMessageToChannel: async (channelId, messageData) => {
|
||
|
const channel = await this.client.channels.fetch(channelId);
|
||
|
if (channel?.isTextBased()) {
|
||
|
await (channel as TextChannel).send(messageData);
|
||
|
}
|
||
|
},
|
||
|
fetchChannelMessages: async (limit: number) => {
|
||
|
const messages = await discordMessage.channel.messages.fetch({ limit });
|
||
|
return Promise.all(messages.map((msg) => this.convertMessage(msg)));
|
||
|
},
|
||
|
sendFile: async (fileUrl, fileName) => {
|
||
|
await discordMessage.channel.send({
|
||
|
files: [{ attachment: fileUrl, name: fileName }],
|
||
|
});
|
||
|
},
|
||
|
sendTyping: async () => {
|
||
|
await discordMessage.channel.sendTyping();
|
||
|
},
|
||
|
reply: async (messageData) => {
|
||
|
const sentMessage = await discordMessage.reply(messageData);
|
||
|
return this.convertSentMessage(sentMessage);
|
||
|
},
|
||
|
send: async (messageData) => {
|
||
|
const sentMessage = await discordMessage.channel.send(messageData);
|
||
|
return this.convertSentMessage(sentMessage);
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
}
|