Skip to content

Commit

Permalink
feat(notifier-api): add a new route to create a category (#247)
Browse files Browse the repository at this point in the history
  • Loading branch information
alimd authored Oct 25, 2024
2 parents acefed5 + 434ea0f commit b71b46e
Show file tree
Hide file tree
Showing 18 changed files with 239 additions and 216 deletions.
16 changes: 7 additions & 9 deletions packages/notifier-api/src/command/start-command.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {config, logger} from '../config.js';
import {logger} from '../config.js';
import {bot} from '../lib/bot.js';
import {message} from '../lib/message.js';
import {nitrobase} from '../lib/nitrobase.js';

import type {Category} from '../type.js';
import {openCategoryCollection} from '../lib/nitrobase.js';

bot.command(
'start',
Expand Down Expand Up @@ -31,9 +29,9 @@ bot.command(
return;
}

const categoriesCollection = await nitrobase.openCollection<Category>(config.nitrobase.categoriesCollection);
const categoryCollection = await openCategoryCollection();

if (categoriesCollection.hasItem(categoryId) === false) {
if (categoryCollection.hasItem(categoryId) === false) {
logger.incident?.('startCommand', 'category_not_found', {categoryId, from: ctx.from, chat: ctx.chat});
await ctx.reply(message.invalid_data_submitted, {
reply_parameters: {
Expand All @@ -43,7 +41,7 @@ bot.command(
return;
}

const members = categoriesCollection.getItemData(categoryId).members;
const members = categoryCollection.getItemData(categoryId).members;

if (members.findIndex((member) => member.id === ctx.chat.id) !== -1) {
await ctx.reply(message.already_added_to_list);
Expand All @@ -58,12 +56,12 @@ bot.command(
lastName: ctx.chat.last_name,
username: ctx.chat.username,
});
categoriesCollection.save();
categoryCollection.save();

await ctx.reply(message.success_added_to_list);
}
catch (error) {
logger.error?.('startCommand', 'unexpected_error', error, {categoryId, from: ctx.from, chat: ctx.chat});
}
}
},
);
10 changes: 4 additions & 6 deletions packages/notifier-api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,10 @@ export const logger = /* #__PURE__ */ createLogger(__package_name__);
const env = /* #__PURE__ */ (() => {
const devConfig = {
dbPath: './db',
tokenSecret: 'DEV_SECRET',
host: '0.0.0.0',
port: 8000,
botToken: 'BOT_TOKEN',
botUsername: 'BOT_USERNAME',
botFirstName: 'BOT_FIRST_NAME',
dropPendingUpdates: '1',
botAdminChatId: 'ADMIN_CHAT_ID',
accessToken: 'ADMIN_TOKEN',
} as const;

const env_ = {
Expand All @@ -35,6 +31,8 @@ const env = /* #__PURE__ */ (() => {
})();

export const config = {
accessToken: env.accessToken!,

nanotronApiServer: {
host: env.host!,
port: +env.port!,
Expand All @@ -46,7 +44,7 @@ export const config = {
rootPath: env.dbPath!,
} as AlwatrNitrobaseConfig,

categoriesCollection: {
categoryCollection: {
name: 'categories',
region: Region.Managers,
type: StoreFileType.Collection,
Expand Down
51 changes: 0 additions & 51 deletions packages/notifier-api/src/handler/notify-validation.ts

This file was deleted.

4 changes: 1 addition & 3 deletions packages/notifier-api/src/handler/parse-body-as-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import {logger} from '../config.js';

import type {NanotronClientRequest} from 'alwatr/nanotron';

export async function parseBodyAsJson(
this: NanotronClientRequest<{body?: JsonObject}>,
): Promise<void> {
export async function parseBodyAsJson(this: NanotronClientRequest<{body?: JsonObject}>): Promise<void> {
const bodyBuffer = await this.getBodyRaw();
if (bodyBuffer.length === 0) {
logger.error('parseBodyAsJson', 'body_required');
Expand Down
28 changes: 28 additions & 0 deletions packages/notifier-api/src/handler/require-access-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {HttpStatusCodes, type NanotronClientRequest} from 'alwatr/nanotron';

import {config, logger} from '../config.js';
import {getAuthBearer} from '../lib/get-auth-bearer.js';

export async function requireAccessToken(this: NanotronClientRequest): Promise<void> {
const token = getAuthBearer(this.headers.authorization);
logger.logMethodArgs?.('requireAccessToken', {token});

if (token === null) {
this.serverResponse.statusCode = HttpStatusCodes.Error_Client_401_Unauthorized;
this.serverResponse.replyErrorResponse({
ok: false,
errorCode: 'authorization_required',
errorMessage: 'Authorization token required',
});
return;
}

if (token !== config.accessToken) {
this.serverResponse.statusCode = HttpStatusCodes.Error_Client_403_Forbidden;
this.serverResponse.replyErrorResponse({
ok: false,
errorCode: 'access_denied',
errorMessage: 'Access denied, token is invalid!',
});
}
}
2 changes: 1 addition & 1 deletion packages/notifier-api/src/lib/escape-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @see https://core.telegram.org/bots/api#markdownv2-style
*/
const scapeChars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'];
const scapeCharsRegex = new RegExp(`[${scapeChars.map(char => `\\${char}`).join('')}]`, 'g');
const scapeCharsRegex = new RegExp(`[${scapeChars.map((char) => `\\${char}`).join('')}]`, 'g');

export function escapeMessage(message: string): string {
return message.replace(scapeCharsRegex, '\\$&');
Expand Down
11 changes: 11 additions & 0 deletions packages/notifier-api/src/lib/get-auth-bearer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Get the token placed in the request header.
*/
export function getAuthBearer(authorizationHeader?: string): string | null {
const auth = authorizationHeader?.split(' ');
if (!auth || auth[0].toLowerCase() !== 'bearer' || !auth[1]) {
return null;
}
// else
return auth[1];
}
4 changes: 2 additions & 2 deletions packages/notifier-api/src/lib/initialize-nitrobase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {config} from '../config.js';
import {nitrobase} from './nitrobase.js';

export function initializeNitrobase() {
if (nitrobase.hasStore(config.nitrobase.categoriesCollection) === false) {
nitrobase.newCollection(config.nitrobase.categoriesCollection);
if (nitrobase.hasStore(config.nitrobase.categoryCollection) === false) {
nitrobase.newCollection(config.nitrobase.categoryCollection);
}
}
4 changes: 4 additions & 0 deletions packages/notifier-api/src/lib/nitrobase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ import {AlwatrNitrobase} from 'alwatr/nitrobase';

import {config} from '../config.js';

import type {Category} from '../type.js';

export const nitrobase = new AlwatrNitrobase(config.nitrobase.config);

export const openCategoryCollection = async () => await nitrobase.openCollection<Category>(config.nitrobase.categoryCollection);
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import {config, logger} from '../config.js';
import {logger} from '../config.js';
import {bot} from './bot.js';
import {nitrobase} from './nitrobase.js';
import {openCategoryCollection} from './nitrobase.js';

import type {Category, NotifyOption} from '../type.js';
import type {GrammyError} from 'grammy';

export type TelegramNotifyOption = {
categoryId: string;
message: string;
markdown: boolean;
};

/**
* Send a message to all members of the `categoryId`.
*/
export async function notifyTelegram(option: NotifyOption): Promise<void> {
logger.logMethodArgs?.('notifyTelegram', option);
const categoriesCollection = await nitrobase.openCollection<Category>(config.nitrobase.categoriesCollection);
const members = categoriesCollection.getItemData(option.categoryId).members;
export async function telegramNotify(option: TelegramNotifyOption): Promise<void> {
logger.logMethodArgs?.('telegramNotify', option);
const categoryCollection = await openCategoryCollection();
const members = categoryCollection.getItemData(option.categoryId).members;
for (let i = members.length - 1; i >= 0; i--) {
const member = members[i];
try {
Expand All @@ -27,12 +32,12 @@ export async function notifyTelegram(option: NotifyOption): Promise<void> {
if (error.error_code === 403) {
const memberIndex = members.indexOf(member);
if (memberIndex === -1) {
logger.accident?.('notifyTelegram', 'unexpected_member_not_found_to_remove', {member, members});
logger.accident?.('telegramNotify', 'unexpected_member_not_found_to_remove', {member, members});
}
members.splice(memberIndex, 1);
categoriesCollection.save();
categoryCollection.save();
}
}
}
logger.logStep?.('notify', 'done');
logger.logStep?.('telegramNotify', 'done');
}
3 changes: 2 additions & 1 deletion packages/notifier-api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {logger} from './config.js';
import {startBot} from './lib/bot.js';
import {initializeNitrobase} from './lib/initialize-nitrobase.js';
import './route/home.js';
import './route/notifyRoute.js';
import './route/new-category.js';
import './route/notify.js';

logger.banner(__package_name__);

Expand Down
80 changes: 80 additions & 0 deletions packages/notifier-api/src/route/new-category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {HttpStatusCodes, type NanotronClientRequest} from 'alwatr/nanotron';

import {logger} from '../config.js';
import {parseBodyAsJson} from '../handler/parse-body-as-json.js';
import {requireAccessToken} from '../handler/require-access-token.js';
import {bot} from '../lib/bot.js';
import {openCategoryCollection} from '../lib/nitrobase.js';
import {nanotronApiServer} from '../lib/server.js';

export type NewCategoryOption = {
id: string;
title: string;
};

nanotronApiServer.defineRoute<{body: NewCategoryOption}>({
method: 'POST',
url: 'new-category',
preHandlers: [requireAccessToken, parseBodyAsJson, newCategoryValidation],
async handler() {
logger.logMethod?.('newCategoryRoute');

const {id, title} = this.sharedMeta.body;

const categoryCollection = await openCategoryCollection();

categoryCollection.addItem(id, {title, members: []});

const botInfo = bot.botInfo;

this.serverResponse.replyJson({
ok: true,
data: {
subscribe: `https://t.me/${botInfo.username}?start=${id}`,
},
});
},
});

export async function newCategoryValidation(this: NanotronClientRequest<{body: JsonObject}>): Promise<void> {
const {id, title} = this.sharedMeta.body;
logger.logMethodArgs?.('newCategoryValidation', {id, title});

if (title === undefined || typeof title !== 'string') {
this.serverResponse.statusCode = HttpStatusCodes.Error_Client_400_Bad_Request;
this.serverResponse.replyErrorResponse({
ok: false,
errorCode: 'title_required',
errorMessage: 'Title is required.',
});
return;
}

if (id === undefined || typeof id !== 'string') {
this.serverResponse.statusCode = HttpStatusCodes.Error_Client_400_Bad_Request;
this.serverResponse.replyErrorResponse({
ok: false,
errorCode: 'id_required',
errorMessage: 'Id is required.',
});
return;
}

const categoryCollection = await openCategoryCollection();

if (categoryCollection.hasItem(id)) {
this.serverResponse.statusCode = HttpStatusCodes.Error_Client_400_Bad_Request;
this.serverResponse.replyErrorResponse({
ok: false,
errorCode: 'category_exist',
errorMessage: `Category ${id} already exist.`,
});
return;
}

// just for type validation and cleanup extra
(this.sharedMeta.body as NewCategoryOption) = {
id,
title,
};
}
Loading

0 comments on commit b71b46e

Please sign in to comment.