Compare commits

9 Commits

Author SHA1 Message Date
c088d7c4a5 Added new commands. getGroupStats and registerGroup.
All checks were successful
Main / build-and-push-docker-image (20.x) (push) Successful in 3m3s
2025-12-16 15:25:02 -05:00
66fb9cef85 Remove cache exchange 2025-12-16 15:24:34 -05:00
789803da59 Update imports and group info extraction from context. 2025-12-16 15:24:16 -05:00
8d8b7f4c3a Added new query 2025-12-16 15:23:24 -05:00
0177aae79a Update file name 2025-12-16 15:23:12 -05:00
8076b984f5 Update groupID type for mutations. 2025-12-16 15:23:04 -05:00
2050e61706 Added mutation key to features.
All checks were successful
Main / build-and-push-docker-image (20.x) (push) Successful in 3m32s
2025-12-10 19:39:15 -05:00
465fbf96c9 fixed env variable not working.
All checks were successful
Main / build-and-push-docker-image (20.x) (push) Successful in 5m58s
2025-12-06 23:45:46 -05:00
30546606d7 Merge pull request 'graphql' (#81) from graphql into main
All checks were successful
Main / build-and-push-docker-image (20.x) (push) Successful in 6m0s
Reviewed-on: #81
2025-12-07 03:32:59 +00:00
16 changed files with 308 additions and 82 deletions

View File

@@ -1,13 +1,15 @@
import { Composer } from "grammy";
import type { Context } from "#root/bot/context.js";
import { logHandle } from "#root/bot/helpers/logging.js";
import urql from "#root/lib/urql.js";
import increment from "#root/lib/graphql/mutations/incrimentMutation.js";
import { urql } from "#root/main.js";
import increment from "#root/lib/graphql/mutations/incrementMutation.js";
const composer = new Composer<Context>();
const feature = composer.chatType(["private", "group", "supergroup"]);
const mutationKey = process.env.GRAPHQL_MUTATION_KEY || "";
/**
* What triggers this feature and adds to the log when it has been triggered.
* The trigger is the command "/botInfo"
@@ -16,7 +18,7 @@ feature.hears(
/^\/botInfo/,
logHandle("bot-info-command"),
async (ctx: Context) => {
await urql.mutation(increment, { trigger: true });
await urql.mutation(increment, { trigger: true, mutationKey });
// Checks if the context includes a message property.
if (ctx.msg && ctx.chat && ctx.msg.from) {
@@ -30,7 +32,7 @@ feature.hears(
}
await urql
.mutation(increment, { command: true })
.mutation(increment, { command: true, mutationKey })
.toPromise()
.then(async () => {
if (ctx.msg) {

View File

@@ -3,8 +3,8 @@ import type { Context } from "#root/bot/context.js";
import { logHandle } from "#root/bot/helpers/logging.js";
import metaLinkCheck from "#root/lib/metaLinkCheck.js";
import twitterLinkCheck from "#root/lib/twitterLinkCheck.js";
import urql from "#root/lib/urql.js";
import increment from "#root/lib/graphql/mutations/incrimentMutation.js";
import { urql } from "#root/main.js";
import increment from "#root/lib/graphql/mutations/incrementMutation.js";
import addGroup from "#root/lib/graphql/mutations/addGroupMutation.js";
import incrementGroup from "#root/lib/graphql/mutations/incrementGroupMutation.js";
@@ -12,17 +12,19 @@ const composer = new Composer<Context>();
const feature = composer.chatType(["group", "supergroup"]);
const mutationKey = process.env.GRAPHQL_MUTATION_KEY || "";
/**
* What triggers this feature and adds to the log when it has been triggered.
* The trigger is anytime an embedded url is detected.
*/
feature.on("message::url", logHandle("embed-check"), async (ctx: Context) => {
await urql.mutation(increment, { trigger: true });
await urql.mutation(increment, { trigger: true, mutationKey });
if (ctx.chat && ctx.msg) {
const groupName = ctx.chat?.title;
const groupID = ctx.chat?.id;
const groupUsername = ctx.chat?.username;
const groupName = ctx.chat?.title || "";
const groupID = ctx.chat?.id.toString() || "";
const groupUsername = ctx.chat?.username || "";
let deletedLinks = 0;
const username = ctx.msg.from?.username;
@@ -49,7 +51,7 @@ feature.on("message::url", logHandle("embed-check"), async (ctx: Context) => {
deletedLinks++;
await urql
.mutation(increment, { link: true })
.mutation(increment, { link: true, mutationKey })
.toPromise()
.then(async () => {
if (ctx.msg) {
@@ -69,7 +71,7 @@ feature.on("message::url", logHandle("embed-check"), async (ctx: Context) => {
deletedLinks++;
await urql
.mutation(increment, { link: true })
.mutation(increment, { link: true, mutationKey })
.toPromise()
.then(async () => {
if (ctx.msg) {
@@ -89,7 +91,7 @@ feature.on("message::url", logHandle("embed-check"), async (ctx: Context) => {
deletedLinks++;
await urql
.mutation(increment, { link: true })
.mutation(increment, { link: true, mutationKey })
.toPromise()
.then(async () => {
if (ctx.msg) {
@@ -114,7 +116,7 @@ feature.on("message::url", logHandle("embed-check"), async (ctx: Context) => {
deletedLinks++;
await urql
.mutation(increment, { link: true })
.mutation(increment, { link: true, mutationKey })
.toPromise()
.then(async () => {
if (ctx.msg) {
@@ -134,7 +136,7 @@ feature.on("message::url", logHandle("embed-check"), async (ctx: Context) => {
deletedLinks++;
await urql
.mutation(increment, { link: true })
.mutation(increment, { link: true, mutationKey })
.toPromise()
.then(async () => {
if (ctx.msg) {
@@ -154,7 +156,7 @@ feature.on("message::url", logHandle("embed-check"), async (ctx: Context) => {
deletedLinks++;
await urql
.mutation(increment, { link: true })
.mutation(increment, { link: true, mutationKey })
.toPromise()
.then(async () => {
if (ctx.msg) {
@@ -169,12 +171,18 @@ feature.on("message::url", logHandle("embed-check"), async (ctx: Context) => {
if (deletedLinks) {
return await urql
.mutation(addGroup, { groupID, groupName, groupUsername })
.mutation(addGroup, {
groupID,
groupName,
groupUsername,
mutationKey
})
.toPromise()
.then(() =>
urql.mutation(incrementGroup, {
groupID,
linksDeleted: deletedLinks
linksDeleted: deletedLinks,
mutationKey
})
);
}

View File

@@ -0,0 +1,96 @@
import { Composer } from "grammy";
import type { Context } from "#root/bot/context.js";
import { logHandle } from "#root/bot/helpers/logging.js";
import { urql } from "#root/main.js";
import increment from "#root/lib/graphql/mutations/incrementMutation.js";
import getGroupStats from "#root/lib/graphql/queries/getGroupStatsQuery.js";
const composer = new Composer<Context>();
const feature = composer.chatType(["group", "supergroup"]);
const mutationKey = process.env.GRAPHQL_MUTATION_KEY || "";
/**
* What triggers this feature and adds to the log when it has been triggered.
* The trigger is the command "/groupStats"
*/
feature.hears(
/^\/groupStats/,
logHandle("groups-stats-command"),
async (ctx: Context) => {
await urql.mutation(increment, { trigger: true, mutationKey });
// Checks if the context includes a message property.
if (ctx.msg && ctx.chat && ctx.msg.from) {
// Doesn't respond to regular users in groups. This is to prevent users spamming the command.
const chatMember = await ctx.getChatMember(ctx.msg.from.id);
if (!["creator", "administrator"].includes(chatMember.status)) {
return;
}
// Stringify the groupID
const groupID = ctx.chat?.id.toString() || "";
// Query to get group stats.
await urql
.query(getGroupStats, {
groupID
})
.toPromise()
.then(async res => {
// Replies to the message.
if (ctx.msg) {
// Check if the group has a document in the database and respond accordingly.
if (res.data.getGroupStats !== null) {
const { name, username, linksDeleted } = res.data.getGroupStats;
await ctx.reply(
`Your group is registered in the database as "${name}" ${
username.length
? `with a username of ${name}`
: `without a public username`
}\\.\n\nThe bot has successfully deleted ${linksDeleted} links from your group\\.`,
{
parse_mode: "MarkdownV2",
reply_parameters: { message_id: ctx.msg.message_id }
}
);
return await ctx.reply(
`If you need to update this information you can use the \\/registerGroup command\\.`,
{
parse_mode: "MarkdownV2",
reply_parameters: { message_id: ctx.msg.message_id }
}
);
}
// Default response //
await ctx.reply(
`Your group was not found in the database\\. You can use the \\/registerGroup command to add your group to the database\\.`,
{
parse_mode: "MarkdownV2",
reply_parameters: { message_id: ctx.msg.message_id }
}
);
return await ctx.reply(
`This is optional\\. If the bot ever removes a link in your group then the group information will be added to the database and tracked from then on\\.`,
{
parse_mode: "MarkdownV2",
reply_parameters: { message_id: ctx.msg.message_id }
}
);
}
return await urql
.mutation(increment, { command: true, mutationKey })
.toPromise();
});
}
}
);
export { composer as getGroupStats };

View File

@@ -1,19 +1,21 @@
import { Composer } from "grammy";
import type { Context } from "#root/bot/context.js";
import { logHandle } from "#root/bot/helpers/logging.js";
import urql from "#root/lib/urql.js";
import increment from "#root/lib/graphql/mutations/incrimentMutation.js";
import { urql } from "#root/main.js";
import increment from "#root/lib/graphql/mutations/incrementMutation.js";
const composer = new Composer<Context>();
const feature = composer.chatType(["group", "supergroup", "private"]);
const mutationKey = process.env.GRAPHQL_MUTATION_KEY || "";
/**
* What triggers this feature and adds to the log when it has been triggered.
* The trigger is the command "/help"
*/
feature.hears(/^\/help/, logHandle("help"), async (ctx: Context) => {
await urql.mutation(increment, { trigger: true });
await urql.mutation(increment, { trigger: true, mutationKey });
// const GROUP_IDS = process.env.GROUP_IDS
// ? process.env.GROUP_IDS.split(",")
@@ -22,7 +24,7 @@ feature.hears(/^\/help/, logHandle("help"), async (ctx: Context) => {
// Checks there is a chat and msg property in the context.
if (ctx.chat && ctx.msg && ctx.msg.from) {
await urql
.mutation(increment, { command: true })
.mutation(increment, { command: true, mutationKey })
.toPromise()
.then(async () => {
if (ctx.msg && ctx.chat && ctx.msg.from) {

View File

@@ -2,8 +2,8 @@ import { Composer } from "grammy";
import type { Context } from "#root/bot/context.js";
import { logHandle } from "#root/bot/helpers/logging.js";
import { metaRegex } from "#root/lib/metaLinkCheck.js";
import urql from "#root/lib/urql.js";
import increment from "#root/lib/graphql/mutations/incrimentMutation.js";
import { urql } from "#root/main.js";
import increment from "#root/lib/graphql/mutations/incrementMutation.js";
import addGroup from "#root/lib/graphql/mutations/addGroupMutation.js";
import incrementGroup from "#root/lib/graphql/mutations/incrementGroupMutation.js";
@@ -11,6 +11,8 @@ const composer = new Composer<Context>();
const feature = composer.chatType(["group", "supergroup"]);
const mutationKey = process.env.GRAPHQL_MUTATION_KEY || "";
/**
* What triggers this feature and adds to the log when it has been triggered.
* The trigger uses the global Twitter regex to detect Twitter and X links within messages.
@@ -19,7 +21,7 @@ feature.hears(
metaRegex,
logHandle("blacklist-detection-meta"),
async (ctx: Context) => {
await urql.mutation(increment, { trigger: true });
await urql.mutation(increment, { trigger: true, mutationKey });
if (ctx.chat && ctx.msg) {
const username = ctx.msg.from?.username;
@@ -27,25 +29,34 @@ feature.hears(
ctx.msg.delete();
return await urql
.mutation(increment, { link: true })
.mutation(increment, { link: true, mutationKey })
.toPromise()
.then(async () => {
if (ctx.msg) {
if (ctx.msg && ctx.chat) {
// Replies to the user informing them of the action.
await ctx.reply(
`@${username} Facebook and meta links along with with links to meta\\-owned services are not allowed here\\. Please consider sharing the media directly or from other social media sources or websites\\. No administration action was taken against you other than the message being deleted\\.`,
{ parse_mode: "MarkdownV2" }
);
const groupName = ctx.chat?.title;
const groupID = ctx.chat?.id;
const groupUsername = ctx.chat?.username;
const groupName = ctx.chat?.title || "";
const groupID = ctx.chat?.id.toString() || "";
const groupUsername = ctx.chat?.username || "";
return await urql
.mutation(addGroup, { groupID, groupName, groupUsername })
.mutation(addGroup, {
groupID,
groupName,
groupUsername,
mutationKey
})
.toPromise()
.then(() =>
urql.mutation(incrementGroup, { groupID, linksDeleted: 1 })
urql.mutation(incrementGroup, {
groupID,
linksDeleted: 1,
mutationKey
})
);
}
});

View File

@@ -0,0 +1,65 @@
import { Composer } from "grammy";
import type { Context } from "#root/bot/context.js";
import { logHandle } from "#root/bot/helpers/logging.js";
import { urql } from "#root/main.js";
import increment from "#root/lib/graphql/mutations/incrementMutation.js";
import addGroup from "#root/lib/graphql/mutations/addGroupMutation.js";
const composer = new Composer<Context>();
const feature = composer.chatType(["group", "supergroup"]);
const mutationKey = process.env.GRAPHQL_MUTATION_KEY || "";
/**
* What triggers this feature and adds to the log when it has been triggered.
* The trigger is the command "/registerGroup"
*/
feature.hears(
/^\/registerGroup/,
logHandle("register-group-command"),
async (ctx: Context) => {
await urql.mutation(increment, { trigger: true, mutationKey });
// Checks if the context includes a message property.
if (ctx.msg && ctx.chat && ctx.msg.from) {
// Doesn't respond to regular users in groups. This is to prevent users spamming the command.
const chatMember = await ctx.getChatMember(ctx.msg.from.id);
if (!["creator", "administrator"].includes(chatMember.status)) {
return;
}
const groupName = ctx.chat?.title || "";
const groupID = ctx.chat?.id.toString() || ""; // Stringify the groupID
const groupUsername = ctx.chat?.username || "";
// Add or update the group
await urql
.mutation(addGroup, {
groupID,
groupName,
groupUsername,
mutationKey
})
.toPromise()
.then(async () => {
await urql
.mutation(increment, { command: true, mutationKey })
.toPromise();
if (ctx.msg) {
// Replies to the message.
return await ctx.reply(
`Your group has been successfully added to the database\\.`,
{
parse_mode: "MarkdownV2",
reply_parameters: { message_id: ctx.msg.message_id }
}
);
}
});
}
}
);
export { composer as registerGroup };

View File

@@ -2,8 +2,8 @@ import { Composer } from "grammy";
import type { Context } from "#root/bot/context.js";
import { logHandle } from "#root/bot/helpers/logging.js";
import { twitterRegex } from "#root/lib/twitterLinkCheck.js";
import urql from "#root/lib/urql.js";
import increment from "#root/lib/graphql/mutations/incrimentMutation.js";
import { urql } from "#root/main.js";
import increment from "#root/lib/graphql/mutations/incrementMutation.js";
import addGroup from "#root/lib/graphql/mutations/addGroupMutation.js";
import incrementGroup from "#root/lib/graphql/mutations/incrementGroupMutation.js";
@@ -11,6 +11,8 @@ const composer = new Composer<Context>();
const feature = composer.chatType(["group", "supergroup"]);
const mutationKey = process.env.GRAPHQL_MUTATION_KEY || "";
/**
* What triggers this feature and adds to the log when it has been triggered.
* The trigger uses the global Twitter regex to detect Twitter and X links within messages.
@@ -19,7 +21,7 @@ feature.hears(
twitterRegex,
logHandle("blacklist-detection-twitter"),
async (ctx: Context) => {
await urql.mutation(increment, { trigger: true });
await urql.mutation(increment, { trigger: true, mutationKey });
if (ctx.chat && ctx.msg) {
const username = ctx.msg.from?.username;
@@ -27,25 +29,34 @@ feature.hears(
ctx.msg.delete();
return await urql
.mutation(increment, { link: true })
.mutation(increment, { link: true, mutationKey })
.toPromise()
.then(async () => {
if (ctx.msg) {
if (ctx.msg && ctx.chat) {
// Replies to the user informing them of the action.
await ctx.reply(
`@${username} Twitter and X links along with reformatting services for Twitter posts are not allowed here\\. Please consider sharing the media directly or from other social media sources or websites\\. No administration action was taken against you other than the message being deleted\\.`,
{ parse_mode: "MarkdownV2" }
);
const groupName = ctx.chat?.title;
const groupID = ctx.chat?.id;
const groupUsername = ctx.chat?.username;
const groupName = ctx.chat?.title || "";
const groupID = ctx.chat?.id.toString() || "";
const groupUsername = ctx.chat?.username || "";
return await urql
.mutation(addGroup, { groupID, groupName, groupUsername })
.mutation(addGroup, {
groupID,
groupName,
groupUsername,
mutationKey
})
.toPromise()
.then(() =>
urql.mutation(incrementGroup, { groupID, linksDeleted: 1 })
urql.mutation(incrementGroup, {
groupID,
linksDeleted: 1,
mutationKey
})
);
}
});

View File

@@ -1,21 +1,24 @@
import { Composer } from "grammy";
import type { Context } from "#root/bot/context.js";
import { logHandle } from "#root/bot/helpers/logging.js";
import urql from "#root/lib/urql.js";
import increment from "#root/lib/graphql/mutations/incrimentMutation.js";
import { urql } from "#root/main.js";
import increment from "#root/lib/graphql/mutations/incrementMutation.js";
const composer = new Composer<Context>();
const feature = composer.chatType("private");
const mutationKey = process.env.GRAPHQL_MUTATION_KEY || "";
/**
* What triggers this feature and adds to the log when it has been triggered.
* The trigger is the command "/start" or "start"
*/
feature.command("start", logHandle("command-start"), async ctx => {
await urql.mutation(increment, { trigger: true });
await urql.mutation(increment, { trigger: true, mutationKey });
await urql
.mutation(increment, { command: true })
.mutation(increment, { command: true, mutationKey })
.toPromise()
.then(async () => {
if (ctx.msg) {

View File

@@ -18,6 +18,8 @@ import { metaBlacklist } from "./features/metaBlacklist.js";
import { botInfoCommand } from "./features/botInfoCommand.js";
import { helpCommand } from "./features/helpCommand.js";
import { embedCheck } from "./features/embedCheck.js";
import { registerGroup } from "./features/registerBotCommand.js";
import { getGroupStats } from "./features/getGroupStatsCommand.js";
interface Dependencies {
config: Config;
@@ -70,6 +72,8 @@ export function createBot(
// Commands
protectedBot.use(botInfoCommand);
protectedBot.use(helpCommand);
protectedBot.use(registerGroup);
protectedBot.use(getGroupStats);
// Blacklist Feature
protectedBot.use(twitterBlacklist);

View File

@@ -2,21 +2,19 @@ import { gql } from "@urql/core";
const addGroup = gql`
mutation addGroup(
$groupID: BigInt
$groupName: String
$groupID: String!
$groupName: String!
$groupUsername: String
$mutationKey: String
) {
addGroup(
groupID: $groupID
groupName: $groupName
groupUsername: $groupUsername
mutationKey: $mutationKey
) {
telegramID
name
username
linksDeleted
createdAt
updatedAt
}
}
`;

View File

@@ -1,14 +1,19 @@
import { gql } from "@urql/core";
const incrementGroup = gql`
mutation incrementGroup($groupID: BigInt, $linksDeleted: Int) {
incrementGroup(groupID: $groupID, linksDeleted: $linksDeleted) {
telegramID
mutation incrementGroup(
$groupID: String!
$linksDeleted: Int!
$mutationKey: String
) {
incrementGroup(
groupID: $groupID
linksDeleted: $linksDeleted
mutationKey: $mutationKey
) {
name
username
linksDeleted
createdAt
updatedAt
}
}
`;

View File

@@ -0,0 +1,22 @@
import { gql } from "@urql/core";
const increment = gql`
mutation increment(
$command: Boolean
$link: Boolean
$trigger: Boolean
$mutationKey: String
) {
increment(
command: $command
link: $link
trigger: $trigger
mutationKey: $mutationKey
) {
createdAt
updatedAt
}
}
`;
export default increment;

View File

@@ -1,12 +0,0 @@
import { gql } from "@urql/core";
const increment = gql`
mutation increment($command: Boolean, $link: Boolean, $trigger: Boolean) {
increment(command: $command, link: $link, trigger: $trigger) {
createdAt
updatedAt
}
}
`;
export default increment;

View File

@@ -0,0 +1,13 @@
import { gql } from "@urql/core";
const getGroupStats = gql`
query getGroupStats($groupID: String!) {
getGroupStats(groupID: $groupID) {
name
username
linksDeleted
}
}
`;
export default getGroupStats;

View File

@@ -1,13 +0,0 @@
import { Client, cacheExchange, fetchExchange } from "@urql/core";
const urql = new Client({
url: process.env.GRAPHQL_URL || "",
exchanges: [cacheExchange, fetchExchange],
fetchOptions: {
headers: {
"x-api-key": process.env?.GRAPHQL_API_TOKEN || ""
}
}
});
export default urql;

View File

@@ -8,6 +8,7 @@ import { config } from "#root/config.js";
import { logger } from "#root/logger.js";
import { createServer, createServerManager } from "#root/server/index.js";
import { run } from "@grammyjs/runner";
import { Client, fetchExchange } from "@urql/core";
async function startPolling(config: PollingConfig) {
const bot = createBot(config.botToken, {
@@ -112,3 +113,13 @@ function onShutdown(cleanUp: () => Promise<void>) {
process.on("SIGINT", handleShutdown);
process.on("SIGTERM", handleShutdown);
}
export const urql = new Client({
url: process.env.GRAPHQL_URL || "",
exchanges: [fetchExchange],
fetchOptions: {
headers: {
"x-api-key": process.env?.GRAPHQL_API_TOKEN || ""
}
}
});