Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Feature/cli-to-edit-user-settings #18

Merged
merged 3 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,66 @@ Ensure you have met the following requirements:
npm test
```

## User Management

### Displaying Help Information

To display help information about available commands, use the following command:

```sh
npm run help
```

### Setting a User as PREMIUM

You can set a user as PREMIUM via the command line.

1. Ensure all dependencies are installed:

```sh
npm install
```

2. Use the following command to set a user as PREMIUM:

```sh
npm run set-premium <userId>
```

Replace `<userId>` with the ID of the user you want to set as PREMIUM.

Example:

```sh
npm run set-premium 123456789
```

Please google how to get the user ID in Telegram, as it is not the same as the username.

### Removing PREMIUM Status from a User

To remove the PREMIUM status from a user, use the following command:

```sh
npm run remove-premium <userId>
```

Example:

```sh
npm run remove-premium 123456789
```

### Listing All PREMIUM Users

To print list of all PREMIUM users, use the following command:

```sh
npm run list-premium
```

The output will include the user ID, username, and the creation date formatted as `Created at: YYYY-MM-DD HH:MM:SS UTC`, sorted by the most recent creation date first.

## Table Entities Description

- The `users` table stores information about the bot users.
Expand Down
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
"license": "MIT",
"scripts": {
"start": "ts-node --transpile-only src/bot.ts",
"test": "jest"
"test": "jest",
"help": "ts-node src/cli.ts --help",
"set-premium": "ts-node src/cli.ts set-premium",
"remove-premium": "ts-node src/cli.ts remove-premium",
"list-premium": "ts-node src/cli.ts list-premium"
},
"dependencies": {
"@pinecone-database/pinecone": "^2.2.1",
"axios": "^1.4.0",
"body-parser": "^1.19.0",
"commander": "^12.1.0",
"cross-fetch": "^4.0.0",
"dotenv": "^16.4.5",
"express": "^4.17.1",
Expand All @@ -29,6 +34,7 @@
"yaml": "^2.3.4"
},
"devDependencies": {
"@types/commander": "^2.12.2",
"@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21",
"@types/jest": "^29.5.12",
Expand Down
4 changes: 2 additions & 2 deletions src/botHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Telegraf } from "telegraf";
import { message } from "telegraf/filters";
import { MyContext, User } from './types';
import {
addOrUpdateUser,
upsertUserIfNotExists,
disableMessagesByChatId,
addSimpleEvent,
storeCommand,
Expand Down Expand Up @@ -30,7 +30,7 @@ export function initializeBotHandlers(bot: Telegraf<MyContext>) {
bot.start(async (ctx: MyContext) => {
console.log(formatLogMessage(ctx, 'start command received'));
if (ctx.from && ctx.from.id) {
await addOrUpdateUser({
await upsertUserIfNotExists({
user_id: ctx.from.id,
username: ctx.from?.username || null,
default_language_code: ctx.from.language_code,
Expand Down
82 changes: 82 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Command } from 'commander';
import { updateUserForce, getUserByUserId, getAllPremiumUsers } from './database/database';
import { pool } from './database/database';

const program = new Command();

program
.command('set-premium <userId>')
.description('Set a user as premium')
.action(async (userId) => {
try {
const user = await getUserByUserId(Number(userId));
if (user) {
user.usage_type = 'premium';
await updateUserForce(user);
console.log(`User ${userId} is now set as PREMIUM.`);
} else {
console.log(`User with ID ${userId} not found.`);
}
} catch (error) {
console.error('Error setting user as PREMIUM:', error);
} finally {
pool.end(); // Close the database connection
}
});

program
.command('remove-premium <userId>')
.description('Remove premium status from a user')
.action(async (userId) => {
try {
const user = await getUserByUserId(Number(userId));
if (user) {
user.usage_type = null;
await updateUserForce(user);
console.log(`User ${userId} is no longer PREMIUM.`);
} else {
console.log(`User with ID ${userId} not found.`);
}
} catch (error) {
console.error('Error removing PREMIUM status from user:', error);
} finally {
pool.end(); // Close the database connection
}
});

program
.command('list-premium')
.description('List all premium users')
.action(async () => {
try {
const premiumUsers = await getAllPremiumUsers();

// Sort users: first by created_at descending, then nulls last
premiumUsers.sort((a, b) => {
if (a.created_at && b.created_at) {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
}
if (a.created_at) return -1;
if (b.created_at) return 1;
return 0;
});

if (premiumUsers.length > 0) {
console.log('Premium Users:');
premiumUsers.forEach(user => {
const createdAt = user.created_at
? `Created at: ${new Date(user.created_at).toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC')}`
: 'Creation date not available';
console.log(`ID: ${user.user_id}, Username: ${user.username}, ${createdAt}`);
});
} else {
console.log('No PREMIUM users found.');
}
} catch (error) {
console.error('Error fetching PREMIUM users:', error);
} finally {
pool.end(); // Close the database connection
}
});

program.parse(process.argv);
31 changes: 30 additions & 1 deletion src/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const addMessage = async ({role, content, chat_id, user_id}: MyMessage) =
return res.rows[0];
}

export const addOrUpdateUser = async ({user_id, username, default_language_code, language_code=default_language_code, openai_api_key=null, usage_type=null}: User) => {
export const upsertUserIfNotExists = async ({user_id, username, default_language_code, language_code=default_language_code, openai_api_key=null, usage_type=null}: User) => {
try {
const res = await pool.query(`
INSERT INTO users (user_id, username, default_language_code, language_code, openai_api_key, usage_type, created_at)
Expand All @@ -105,6 +105,25 @@ export const addOrUpdateUser = async ({user_id, username, default_language_code,
throw error;
}
}

export const updateUserForce = async ({user_id, username, default_language_code, language_code=default_language_code, openai_api_key=null, usage_type=null}: User) => {
try {
const res = await pool.query(`
UPDATE users SET
username = $2,
default_language_code = $3,
language_code = $4,
openai_api_key = $5,
usage_type = $6,
created_at = CURRENT_TIMESTAMP
WHERE user_id = $1
RETURNING *;
`, [user_id, username, default_language_code, language_code, openai_api_key, usage_type]);
return res.rows[0];
} catch (error) {
throw error;
}
}

export const addEvent = async (event: Event) => {
event.time = new Date();
Expand Down Expand Up @@ -340,3 +359,13 @@ export async function addTranscriptionEvent(ctx: MyContext, transcriptionText: s
console.log(formatLogMessage(ctx, `[ERROR] error in saving the model_transcription to the database: ${error}`));
}
}

export const getAllPremiumUsers = async () => {
const res = await pool.query(`
SELECT user_id, username, created_at
FROM users
WHERE usage_type = $1
ORDER BY created_at DESC NULLS LAST
`, ['premium']);
return res.rows;
};
10 changes: 5 additions & 5 deletions src/openAIFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import pTimeout from 'p-timeout';
import { formatLogMessage } from './utils/utils';
import { tokenizeText, convertTokensToText } from './utils/encodingUtils';
import { CHAT_GPT_DEFAULT_TIMEOUT_MS, GPT_MODEL, MAX_TOKENS_THRESHOLD_TO_REDUCE_HISTORY, DEFAULT_PROMPT_MESSAGE } from './config';
import { getUserUsedTokens, addOrUpdateUser, getUserByUserId } from './database/database';
import { getUserUsedTokens, upsertUserIfNotExists, getUserByUserId } from './database/database';
import { MAX_TRIAL_TOKENS, OPENAI_API_KEY } from './config';

export const APPROX_IMAGE_TOKENS = 800;
Expand Down Expand Up @@ -44,14 +44,14 @@ export async function getUserSettingsAndOpenAi(ctx: MyContext): Promise<UserData
default_language_code: ctx.from?.language_code || null,
language_code: ctx.from?.language_code || null,
};
await addOrUpdateUser(userSettings); // Insert the new user
await upsertUserIfNotExists(userSettings); // Insert the new user
console.log(formatLogMessage(ctx, "User created in the database"));
} else {
// If the user is found, update their data
userSettings.username = ctx.from?.username || userSettings.username;
userSettings.default_language_code = ctx.from?.language_code || userSettings.default_language_code;
userSettings.language_code = ctx.from?.language_code || userSettings.language_code;
await addOrUpdateUser(userSettings); // Update the user's data
await upsertUserIfNotExists(userSettings); // Update the user's data
console.log(formatLogMessage(ctx, "User data updated in the database"));
}

Expand All @@ -65,12 +65,12 @@ export async function getUserSettingsAndOpenAi(ctx: MyContext): Promise<UserData
const usedTokens = await getUserUsedTokens(user_id);
if (usedTokens < MAX_TRIAL_TOKENS) {
userSettings.usage_type = 'trial_active';
await addOrUpdateUser(userSettings);
await upsertUserIfNotExists(userSettings);
userSettings.openai_api_key = OPENAI_API_KEY;
console.log(formatLogMessage(ctx, `[ACCESS GRANTED] user is trial and user did not exceed the message limit. User used tokens: ${usedTokens} out of ${MAX_TRIAL_TOKENS}. openai_api_key set from environment variable.`));
} else {
userSettings.usage_type = 'trial_ended';
await addOrUpdateUser(userSettings);
await upsertUserIfNotExists(userSettings);
console.log(formatLogMessage(ctx, `[ACCESS DENIED] user is not premium and has no custom openai_api_key and exceeded the message limit. User used tokens: ${usedTokens} out of ${MAX_TRIAL_TOKENS}.`));
throw new NoOpenAiApiKeyError(`User with user_id ${user_id} has no openai_api_key`);
}
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,13 @@
"@types/connect" "*"
"@types/node" "*"

"@types/commander@^2.12.2":
version "2.12.2"
resolved "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz"
integrity sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==
dependencies:
commander "*"

"@types/connect@*":
version "3.4.38"
resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz"
Expand Down Expand Up @@ -1273,6 +1280,11 @@ combined-stream@^1.0.8:
dependencies:
delayed-stream "~1.0.0"

commander@*, commander@^12.1.0:
version "12.1.0"
resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz"
integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==

component-emitter@^1.3.0:
version "1.3.1"
resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz"
Expand Down