mirror of
https://github.com/muerwre/vk-tg-bot.git
synced 2025-04-24 14:36:41 +07:00
added calendar service
This commit is contained in:
parent
0b663fb96f
commit
6e34090f8f
31 changed files with 1359 additions and 200 deletions
1
.env
1
.env
|
@ -1 +0,0 @@
|
||||||
EXPOSE=7003
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ config.yml
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
.idea
|
.idea
|
||||||
|
/key.local.json
|
|
@ -22,6 +22,9 @@ templates:
|
||||||
wall_post_new: templates/post_new.md
|
wall_post_new: templates/post_new.md
|
||||||
group_join: templates/group_join.md
|
group_join: templates/group_join.md
|
||||||
group_leave: templates/group_leave.md
|
group_leave: templates/group_leave.md
|
||||||
|
calendar:
|
||||||
|
keyFile: # key.json
|
||||||
|
timezone:
|
||||||
# groups:
|
# groups:
|
||||||
# - id: 0
|
# - id: 0
|
||||||
# name: 'Group name'
|
# name: 'Group name'
|
||||||
|
@ -29,6 +32,9 @@ templates:
|
||||||
# secretKey: 'groupSecretKey'
|
# secretKey: 'groupSecretKey'
|
||||||
# apiKey: 'callbackApiKey'
|
# apiKey: 'callbackApiKey'
|
||||||
# post_types: ['post','copy','reply','postpone','suggest']
|
# post_types: ['post','copy','reply','postpone','suggest']
|
||||||
|
# calendar:
|
||||||
|
# id: abcd@group.calendar.google.com
|
||||||
|
# enabled: true
|
||||||
# templates: # group's custom templates (optional)
|
# templates: # group's custom templates (optional)
|
||||||
# message_new: templates/custom/message_new.md
|
# message_new: templates/custom/message_new.md
|
||||||
# channels:
|
# channels:
|
||||||
|
|
13
key.json
Normal file
13
key.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "",
|
||||||
|
"private_key_id": "",
|
||||||
|
"private_key": "",
|
||||||
|
"client_email": "",
|
||||||
|
"client_id": "",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_x509_cert_url": "",
|
||||||
|
"universe_domain": "googleapis.com"
|
||||||
|
}
|
|
@ -12,7 +12,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"cross-fetch": "^3.1.4",
|
"cross-fetch": "^3.1.4",
|
||||||
|
"date-fns": "^3.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"google-auth-library": "^9.4.1",
|
||||||
|
"googleapis": "^130.0.0",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"http": "^0.0.1-security",
|
"http": "^0.0.1-security",
|
||||||
"js-yaml": "^4.0.0",
|
"js-yaml": "^4.0.0",
|
||||||
|
@ -33,8 +36,8 @@
|
||||||
"telegraf": "^4.3.0",
|
"telegraf": "^4.3.0",
|
||||||
"to-vfile": "^6.1.0",
|
"to-vfile": "^6.1.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typeorm": "^0.2.32",
|
"typeorm": "^0.3.19",
|
||||||
"typescript": "^4.2.3",
|
"typescript": "^4.8.2",
|
||||||
"unist-util-filter": "^3.0.0",
|
"unist-util-filter": "^3.0.0",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"vk-io": "^4.8.3",
|
"vk-io": "^4.8.3",
|
||||||
|
@ -46,6 +49,7 @@
|
||||||
"@types/axios": "^0.14.0",
|
"@types/axios": "^0.14.0",
|
||||||
"@types/express": "^4.17.11",
|
"@types/express": "^4.17.11",
|
||||||
"@types/handlebars": "^4.1.0",
|
"@types/handlebars": "^4.1.0",
|
||||||
|
"@types/jest": "^29.5.11",
|
||||||
"@types/node": "^14.14.37",
|
"@types/node": "^14.14.37",
|
||||||
"@types/winston": "^2.4.4",
|
"@types/winston": "^2.4.4",
|
||||||
"@types/yargs": "^16.0.1",
|
"@types/yargs": "^16.0.1",
|
||||||
|
|
|
@ -23,4 +23,7 @@ export const defaultConfig: Config = {
|
||||||
group_join: "templates/group_join.md",
|
group_join: "templates/group_join.md",
|
||||||
group_leave: "templates/group_leave.md",
|
group_leave: "templates/group_leave.md",
|
||||||
},
|
},
|
||||||
|
calendar: {
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,10 @@ import { validateConfig } from "./validate";
|
||||||
import { getCmdArg } from "../utils/args";
|
import { getCmdArg } from "../utils/args";
|
||||||
import { defaultConfig } from "./default";
|
import { defaultConfig } from "./default";
|
||||||
import { merge } from "lodash";
|
import { merge } from "lodash";
|
||||||
|
import {
|
||||||
|
CalendarKeyFile,
|
||||||
|
calendarKeyValidator,
|
||||||
|
} from "../service/calendar/config";
|
||||||
|
|
||||||
const configPath = getCmdArg("config");
|
const configPath = getCmdArg("config");
|
||||||
const data = fs.readFileSync(
|
const data = fs.readFileSync(
|
||||||
|
@ -21,6 +25,18 @@ const config =
|
||||||
export default function prepareConfig() {
|
export default function prepareConfig() {
|
||||||
validateConfig(config);
|
validateConfig(config);
|
||||||
|
|
||||||
|
if (config.calendar?.keyFile) {
|
||||||
|
try {
|
||||||
|
const key = JSON.parse(
|
||||||
|
fs.readFileSync(config.calendar?.keyFile).toString()
|
||||||
|
) as CalendarKeyFile;
|
||||||
|
calendarKeyValidator.validateSync(key);
|
||||||
|
config.calendarKey = key;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("tried to parse calendar key, got error", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config.telegram.templates = {
|
config.telegram.templates = {
|
||||||
help: config.templates.help,
|
help: config.templates.help,
|
||||||
help_admin: config.templates.help_admin,
|
help_admin: config.templates.help_admin,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { VkConfig, VkEvent } from "../service/vk/types";
|
||||||
import { HttpConfig } from "../api/http/types";
|
import { HttpConfig } from "../api/http/types";
|
||||||
import { LoggerConfig } from "../service/logger/types";
|
import { LoggerConfig } from "../service/logger/types";
|
||||||
import { PostgresConfig } from "../service/db/postgres/types";
|
import { PostgresConfig } from "../service/db/postgres/types";
|
||||||
|
import { CalendarConfig, CalendarKeyFile } from "src/service/calendar/config";
|
||||||
|
|
||||||
export type TemplateConfig = Record<VkEvent, string> &
|
export type TemplateConfig = Record<VkEvent, string> &
|
||||||
Partial<Record<"help" | "help_admin", string>>;
|
Partial<Record<"help" | "help_admin", string>>;
|
||||||
|
@ -14,4 +15,6 @@ export interface Config extends Record<string, any> {
|
||||||
logger: LoggerConfig;
|
logger: LoggerConfig;
|
||||||
templates: TemplateConfig;
|
templates: TemplateConfig;
|
||||||
postgres: PostgresConfig;
|
postgres: PostgresConfig;
|
||||||
|
calendar?: Partial<CalendarConfig>;
|
||||||
|
calendarKey?: CalendarKeyFile;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { vkConfigSchema } from "../service/vk/validation";
|
||||||
import { telegramConfigSchema } from "../service/telegram/validation";
|
import { telegramConfigSchema } from "../service/telegram/validation";
|
||||||
import { loggerConfigSchema } from "../service/logger/config";
|
import { loggerConfigSchema } from "../service/logger/config";
|
||||||
import { dbConfigValidatior } from "../service/db/postgres/validation";
|
import { dbConfigValidatior } from "../service/db/postgres/validation";
|
||||||
|
import { calendarConfigValidator } from "../service/calendar/config";
|
||||||
|
|
||||||
export const templateConfigSchema = object().required().shape({
|
export const templateConfigSchema = object().required().shape({
|
||||||
message_new: string().required(),
|
message_new: string().required(),
|
||||||
|
@ -29,6 +30,7 @@ const configSchema = object<Config>().required().shape({
|
||||||
logger: loggerConfigSchema,
|
logger: loggerConfigSchema,
|
||||||
templates: templateConfigSchema,
|
templates: templateConfigSchema,
|
||||||
postgres: dbConfigValidatior,
|
postgres: dbConfigValidatior,
|
||||||
|
calendar: calendarConfigValidator.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const validateConfig = (config: Config) =>
|
export const validateConfig = (config: Config) =>
|
||||||
|
|
23
src/index.ts
23
src/index.ts
|
@ -7,6 +7,7 @@ import { HttpApi } from "./api/http";
|
||||||
import { PostgresDB } from "./service/db/postgres";
|
import { PostgresDB } from "./service/db/postgres";
|
||||||
import { PgTransport } from "./service/db/postgres/loggerTransport";
|
import { PgTransport } from "./service/db/postgres/loggerTransport";
|
||||||
import { roll } from "./commands/roll";
|
import { roll } from "./commands/roll";
|
||||||
|
import { setupCalendar } from "./service/calendar/setup";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
|
@ -18,7 +19,27 @@ async function main() {
|
||||||
logger.add(new PgTransport(db, { level: "warn" }));
|
logger.add(new PgTransport(db, { level: "warn" }));
|
||||||
|
|
||||||
const telegram = new TelegramService(config.telegram);
|
const telegram = new TelegramService(config.telegram);
|
||||||
const vkService = new VkService(config.vk, telegram, config.templates, db);
|
const calendar = await setupCalendar(
|
||||||
|
logger.warn,
|
||||||
|
config.calendar,
|
||||||
|
config.calendarKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if (calendar) {
|
||||||
|
logger.info(
|
||||||
|
`calendar service started for ${config.calendarKey?.project_id}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn("calendar service not started");
|
||||||
|
}
|
||||||
|
|
||||||
|
const vkService = new VkService(
|
||||||
|
config.vk,
|
||||||
|
telegram,
|
||||||
|
config.templates,
|
||||||
|
db,
|
||||||
|
calendar
|
||||||
|
);
|
||||||
|
|
||||||
const telegramApi = new TelegramApi(telegram, db, config);
|
const telegramApi = new TelegramApi(telegram, db, config);
|
||||||
telegramApi.listen();
|
telegramApi.listen();
|
||||||
|
|
29
src/service/calendar/config.ts
Normal file
29
src/service/calendar/config.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Asserts, boolean, object, string } from "yup";
|
||||||
|
|
||||||
|
export const calendarGroupConfigValidator = object({
|
||||||
|
id: string().optional().email(),
|
||||||
|
enabled: boolean().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const calendarConfigValidator = object({
|
||||||
|
keyFile: string().optional(),
|
||||||
|
timezone: string().required().default(""),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const calendarKeyValidator = object({
|
||||||
|
type: string().required(),
|
||||||
|
project_id: string().required(),
|
||||||
|
private_key_id: string().required(),
|
||||||
|
private_key: string().required(),
|
||||||
|
client_email: string().required(),
|
||||||
|
client_id: string().required(),
|
||||||
|
auth_uri: string().required(),
|
||||||
|
token_uri: string().required(),
|
||||||
|
auth_provider_x509_cert_url: string().required(),
|
||||||
|
client_x509_cert_url: string().required(),
|
||||||
|
universe_domain: string().required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CalendarConfig = Asserts<typeof calendarConfigValidator>;
|
||||||
|
export type CalendarGroupConfig = Asserts<typeof calendarGroupConfigValidator>;
|
||||||
|
export type CalendarKeyFile = Asserts<typeof calendarKeyValidator>;
|
74
src/service/calendar/index.ts
Normal file
74
src/service/calendar/index.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { calendar_v3, google } from "googleapis";
|
||||||
|
import { KeyFile } from "./types";
|
||||||
|
import { JWT } from "google-auth-library";
|
||||||
|
|
||||||
|
export class CalendarService {
|
||||||
|
private auth!: JWT;
|
||||||
|
private calendar!: calendar_v3.Calendar;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
key: KeyFile,
|
||||||
|
private timeZone: string,
|
||||||
|
private log: (...vals: any) => void
|
||||||
|
) {
|
||||||
|
this.auth = new google.auth.JWT(
|
||||||
|
key.client_email,
|
||||||
|
undefined,
|
||||||
|
key.private_key,
|
||||||
|
[
|
||||||
|
"https://www.googleapis.com/auth/calendar",
|
||||||
|
"https://www.googleapis.com/auth/calendar.events",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
this.calendar = google.calendar({
|
||||||
|
version: "v3",
|
||||||
|
auth: this.auth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async authenticate() {
|
||||||
|
await this.auth.authorize();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async isEventExist(calendarId: string, eventID: string) {
|
||||||
|
const event = await this.calendar.events.list({
|
||||||
|
calendarId: calendarId,
|
||||||
|
iCalUID: eventID,
|
||||||
|
showDeleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Boolean(event.data.items?.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createEvent(
|
||||||
|
calendarId: string,
|
||||||
|
start: Date,
|
||||||
|
end: Date,
|
||||||
|
summary: string,
|
||||||
|
description: string,
|
||||||
|
eventId: string
|
||||||
|
) {
|
||||||
|
if (await this.isEventExist(calendarId, eventId)) {
|
||||||
|
this.log("event already exist, quitting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.calendar.events.insert({
|
||||||
|
calendarId,
|
||||||
|
requestBody: {
|
||||||
|
summary,
|
||||||
|
description,
|
||||||
|
start: {
|
||||||
|
dateTime: start.toISOString(),
|
||||||
|
timeZone: this.timeZone,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: end.toISOString(),
|
||||||
|
timeZone: this.timeZone,
|
||||||
|
},
|
||||||
|
iCalUID: eventId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
27
src/service/calendar/setup.ts
Normal file
27
src/service/calendar/setup.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { CalendarConfig, CalendarKeyFile } from "./config";
|
||||||
|
import { CalendarService } from "../calendar";
|
||||||
|
|
||||||
|
export const setupCalendar = async (
|
||||||
|
logger: (...vals: any) => void,
|
||||||
|
config?: Partial<CalendarConfig>,
|
||||||
|
keyConfig?: CalendarKeyFile
|
||||||
|
) => {
|
||||||
|
if (!keyConfig) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const service = new CalendarService(
|
||||||
|
keyConfig,
|
||||||
|
config?.timezone ?? "",
|
||||||
|
logger
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.authenticate();
|
||||||
|
|
||||||
|
return service;
|
||||||
|
} catch (error) {
|
||||||
|
logger("can't init calendar", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
13
src/service/calendar/types.ts
Normal file
13
src/service/calendar/types.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export interface KeyFile {
|
||||||
|
type: string;
|
||||||
|
project_id: string;
|
||||||
|
private_key_id: string;
|
||||||
|
private_key: string;
|
||||||
|
client_email: string;
|
||||||
|
client_id: string;
|
||||||
|
auth_uri: string;
|
||||||
|
token_uri: string;
|
||||||
|
auth_provider_x509_cert_url: string;
|
||||||
|
client_x509_cert_url: string;
|
||||||
|
universe_domain: string;
|
||||||
|
}
|
|
@ -9,14 +9,14 @@ export interface Storage {
|
||||||
tgMessageId: number,
|
tgMessageId: number,
|
||||||
groupId: number,
|
groupId: number,
|
||||||
channel: string
|
channel: string
|
||||||
): Promise<Event | undefined>;
|
): Promise<Event | null>;
|
||||||
getEventById(eventId: number): Promise<Event | undefined>;
|
getEventById(eventId: number): Promise<Event | null>;
|
||||||
getEventByVKEventId(
|
getEventByVKEventId(
|
||||||
type: VkEvent,
|
type: VkEvent,
|
||||||
eventId: number,
|
eventId: number,
|
||||||
groupId: number,
|
groupId: number,
|
||||||
channel: string
|
channel: string
|
||||||
): Promise<Event | undefined>;
|
): Promise<Event | null>;
|
||||||
createEvent(
|
createEvent(
|
||||||
type: VkEvent,
|
type: VkEvent,
|
||||||
eventId: number,
|
eventId: number,
|
||||||
|
@ -24,7 +24,7 @@ export interface Storage {
|
||||||
channel: string,
|
channel: string,
|
||||||
tgMessageId: number,
|
tgMessageId: number,
|
||||||
text: Record<any, any>
|
text: Record<any, any>
|
||||||
): Promise<Event | undefined>;
|
): Promise<Event | null>;
|
||||||
createOrUpdateLike(
|
createOrUpdateLike(
|
||||||
messageId: number,
|
messageId: number,
|
||||||
channel: string,
|
channel: string,
|
||||||
|
@ -36,12 +36,12 @@ export interface Storage {
|
||||||
channel: string,
|
channel: string,
|
||||||
messageId: number,
|
messageId: number,
|
||||||
author: number
|
author: number
|
||||||
): Promise<Like | undefined>;
|
): Promise<Like | null>;
|
||||||
createPost(
|
createPost(
|
||||||
eventId: number,
|
eventId: number,
|
||||||
text: string,
|
text: string,
|
||||||
vkPostId: number
|
vkPostId: number
|
||||||
): Promise<Post | undefined>;
|
): Promise<Post | null>;
|
||||||
findPostByEvent(eventId: number): Promise<Post | undefined>;
|
findPostByEvent(eventId: number): Promise<Post | null>;
|
||||||
healthcheck(): Promise<void>;
|
healthcheck(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,10 +55,12 @@ export class PostgresDB implements Storage {
|
||||||
channel: string
|
channel: string
|
||||||
) => {
|
) => {
|
||||||
return await this.events.findOne({
|
return await this.events.findOne({
|
||||||
type,
|
where: {
|
||||||
tgMessageId,
|
type,
|
||||||
vkGroupId,
|
tgMessageId,
|
||||||
channel,
|
vkGroupId,
|
||||||
|
channel,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -69,15 +71,19 @@ export class PostgresDB implements Storage {
|
||||||
channel: string
|
channel: string
|
||||||
) => {
|
) => {
|
||||||
return await this.events.findOne({
|
return await this.events.findOne({
|
||||||
type,
|
where: {
|
||||||
vkEventId,
|
type,
|
||||||
vkGroupId,
|
vkEventId,
|
||||||
channel,
|
vkGroupId,
|
||||||
|
channel,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
getEventById = async (id: number) => {
|
getEventById = async (id: number) => {
|
||||||
return await this.events.findOne({
|
return await this.events.findOne({
|
||||||
id,
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -109,14 +115,18 @@ export class PostgresDB implements Storage {
|
||||||
|
|
||||||
getLikeBy = async (channel, messageId, author) => {
|
getLikeBy = async (channel, messageId, author) => {
|
||||||
return this.likes.findOne({
|
return this.likes.findOne({
|
||||||
channel,
|
where: {
|
||||||
messageId,
|
channel,
|
||||||
author,
|
messageId,
|
||||||
|
author,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
createOrUpdateLike = async (messageId, channel, author, text) => {
|
createOrUpdateLike = async (messageId, channel, author, text) => {
|
||||||
const like = await this.likes.findOne({ channel, author, messageId });
|
const like = await this.likes.findOne({
|
||||||
|
where: { channel, author, messageId },
|
||||||
|
});
|
||||||
|
|
||||||
if (like) {
|
if (like) {
|
||||||
return await this.likes.save({ ...like, text });
|
return await this.likes.save({ ...like, text });
|
||||||
|
@ -131,7 +141,7 @@ export class PostgresDB implements Storage {
|
||||||
};
|
};
|
||||||
|
|
||||||
findPostByEvent = async (eventId: number) => {
|
findPostByEvent = async (eventId: number) => {
|
||||||
return this.posts.findOne({ eventId });
|
return this.posts.findOne({ where: { eventId } });
|
||||||
};
|
};
|
||||||
|
|
||||||
createPost = async (eventId: number, text: string, vkPostId: number) => {
|
createPost = async (eventId: number, text: string, vkPostId: number) => {
|
||||||
|
|
76
src/service/vk/handlers/PostNewCalendarHandler.ts
Normal file
76
src/service/vk/handlers/PostNewCalendarHandler.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { addMinutes, isBefore, secondsToMilliseconds } from "date-fns";
|
||||||
|
import { NextMiddleware } from "middleware-io";
|
||||||
|
import { WallPostContext } from "vk-io";
|
||||||
|
import { getDateFromText } from "../../../utils/date/getDateFromText";
|
||||||
|
import logger from "../../logger";
|
||||||
|
import { VkEventHandler } from "./VkEventHandler";
|
||||||
|
import { getSummaryFromText } from "../../../utils/text/getSummaryFromText";
|
||||||
|
import { maybeTrim } from "../../../utils/text/maybeTrim";
|
||||||
|
|
||||||
|
|
||||||
|
export class PostNewCalendarHandler extends VkEventHandler {
|
||||||
|
constructor(...props: ConstructorParameters<typeof VkEventHandler>) {
|
||||||
|
super(...props);
|
||||||
|
}
|
||||||
|
|
||||||
|
public execute = async (context: WallPostContext, next: NextMiddleware) => {
|
||||||
|
try {
|
||||||
|
const id = context?.wall?.id;
|
||||||
|
const postType = context?.wall?.postType;
|
||||||
|
const text = context.wall.text;
|
||||||
|
const createdAt = context.wall.createdAt && new Date(secondsToMilliseconds(context.wall.createdAt));
|
||||||
|
const eventId = context.wall.id.toString();
|
||||||
|
|
||||||
|
if (context.isRepost || !id || !this.group.calendar?.id || !this.group.calendar.enabled || !this.calendar || !text || !createdAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isValidPostType(postType)) {
|
||||||
|
logger.info(
|
||||||
|
`skipping wall_post_new for ${this.group.name
|
||||||
|
}#${id} since it have type '${postType}', which is not in [${this.channel.post_types.join(
|
||||||
|
","
|
||||||
|
)}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = getSummaryFromText(text);
|
||||||
|
|
||||||
|
const start = getDateFromText(text, createdAt);
|
||||||
|
if (!start) {
|
||||||
|
logger.warn(`can't extract date from summary: ${summary}`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBefore(start, createdAt)) {
|
||||||
|
logger.warn(`extracted date was from the past: ${start?.toISOString()}, summary was: ${summary}`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const end = addMinutes(start, 15);
|
||||||
|
const description = [this.generateVkPostUrl(context.wall.id), maybeTrim(text, 512)].join('\n\n');
|
||||||
|
|
||||||
|
this.calendar?.createEvent(this.group.calendar?.id, start, end, summary, description, eventId)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('error occurred in calendar handler', error);
|
||||||
|
} finally {
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if event of type we can handle
|
||||||
|
*/
|
||||||
|
private isValidPostType(type?: string) {
|
||||||
|
return !!type && type === "post";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates urls for postId
|
||||||
|
*/
|
||||||
|
generateVkPostUrl = (postId?: number) =>
|
||||||
|
`https://vk.com/wall-${this.group.id}_${postId}`;
|
||||||
|
|
||||||
|
}
|
|
@ -49,8 +49,7 @@ const PHOTO_CAPTION_LIMIT = 1000;
|
||||||
const POST_TEXT_LIMIT = 4096;
|
const POST_TEXT_LIMIT = 4096;
|
||||||
|
|
||||||
export class PostNewHandler extends VkEventHandler<Fields, Values> {
|
export class PostNewHandler extends VkEventHandler<Fields, Values> {
|
||||||
constructor(...props: any) {
|
constructor(...props: ConstructorParameters<typeof VkEventHandler<Fields, Values>>) {
|
||||||
// @ts-ignore
|
|
||||||
super(...props);
|
super(...props);
|
||||||
this.onInit();
|
this.onInit();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import { NextMiddleware } from "middleware-io";
|
import { NextMiddleware } from "middleware-io";
|
||||||
import { ConfigGroup, GroupChannel, GroupInstance, VkEvent } from "../types";
|
import {
|
||||||
|
Calendar,
|
||||||
|
ConfigGroup,
|
||||||
|
GroupChannel,
|
||||||
|
GroupInstance,
|
||||||
|
VkEvent,
|
||||||
|
} from "../types";
|
||||||
import { VkService } from "../index";
|
import { VkService } from "../index";
|
||||||
import { TelegramService } from "../../telegram";
|
import { TelegramService } from "../../telegram";
|
||||||
import { Template } from "../../template";
|
import { Template } from "../../template";
|
||||||
import { Storage } from "../../db";
|
import { Storage } from "../../db";
|
||||||
import { Event } from "../../db/postgres/entities/Event";
|
|
||||||
import logger from "../../logger";
|
import logger from "../../logger";
|
||||||
import safeJson from "safe-json-stringify";
|
import safeJson from "safe-json-stringify";
|
||||||
|
|
||||||
|
@ -20,7 +25,8 @@ export class VkEventHandler<
|
||||||
protected vk: VkService,
|
protected vk: VkService,
|
||||||
protected telegram: TelegramService,
|
protected telegram: TelegramService,
|
||||||
protected template: Template<F, V>,
|
protected template: Template<F, V>,
|
||||||
protected db: Storage
|
protected db: Storage,
|
||||||
|
protected calendar: Calendar | null
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute: (
|
public execute: (
|
||||||
|
@ -57,7 +63,7 @@ export class VkEventHandler<
|
||||||
/**
|
/**
|
||||||
* Checks for duplicates
|
* Checks for duplicates
|
||||||
*/
|
*/
|
||||||
getEventById = async (id?: number): Promise<Event | undefined> => {
|
getEventById = async (id?: number) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -68,7 +74,7 @@ export class VkEventHandler<
|
||||||
/**
|
/**
|
||||||
* Checks for duplicates
|
* Checks for duplicates
|
||||||
*/
|
*/
|
||||||
getEventByVkEventId = async (id?: number): Promise<Event | undefined> => {
|
getEventByVkEventId = async (id?: number) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -84,9 +90,7 @@ export class VkEventHandler<
|
||||||
/**
|
/**
|
||||||
* Checks for duplicates
|
* Checks for duplicates
|
||||||
*/
|
*/
|
||||||
getEventByTgMessageId = async (
|
getEventByTgMessageId = async (tgMessageId?: number) => {
|
||||||
tgMessageId?: number
|
|
||||||
): Promise<Event | undefined> => {
|
|
||||||
if (!tgMessageId) {
|
if (!tgMessageId) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
import { ConfigGroup, GroupChannel, GroupInstance, VkEvent } from "../types";
|
import {
|
||||||
|
Calendar,
|
||||||
|
ConfigGroup,
|
||||||
|
GroupChannel,
|
||||||
|
GroupInstance,
|
||||||
|
VkEvent,
|
||||||
|
} from "../types";
|
||||||
import { VkEventHandler } from "./VkEventHandler";
|
import { VkEventHandler } from "./VkEventHandler";
|
||||||
import { MessageNewHandler } from "./MessageNewHandler";
|
import { MessageNewHandler } from "./MessageNewHandler";
|
||||||
import { StubHandler } from "./StubHandler";
|
|
||||||
import { VkService } from "../index";
|
import { VkService } from "../index";
|
||||||
import { TelegramService } from "../../telegram";
|
import { TelegramService } from "../../telegram";
|
||||||
import { Template } from "../../template";
|
import { Template } from "../../template";
|
||||||
import { PostNewHandler } from "./PostNewHandler";
|
import { PostNewHandler } from "./PostNewHandler";
|
||||||
import { Storage } from "../../db";
|
import { Storage } from "../../db";
|
||||||
import { JoinLeaveHandler } from "./JoinLeaveHandler";
|
import { JoinLeaveHandler } from "./JoinLeaveHandler";
|
||||||
|
import { PostNewCalendarHandler } from "./PostNewCalendarHandler";
|
||||||
|
|
||||||
interface Handler {
|
interface Handler {
|
||||||
new (
|
new (
|
||||||
|
@ -18,13 +24,14 @@ interface Handler {
|
||||||
vk: VkService,
|
vk: VkService,
|
||||||
telegram: TelegramService,
|
telegram: TelegramService,
|
||||||
template: Template<any, any>,
|
template: Template<any, any>,
|
||||||
db: Storage
|
db: Storage,
|
||||||
|
calendar: Calendar | null
|
||||||
): VkEventHandler;
|
): VkEventHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const vkEventToHandler: Record<VkEvent, Handler> = {
|
export const vkEventToHandler: Record<VkEvent, Handler[]> = {
|
||||||
[VkEvent.GroupJoin]: JoinLeaveHandler,
|
[VkEvent.GroupJoin]: [JoinLeaveHandler],
|
||||||
[VkEvent.GroupLeave]: JoinLeaveHandler,
|
[VkEvent.GroupLeave]: [JoinLeaveHandler],
|
||||||
[VkEvent.MessageNew]: MessageNewHandler,
|
[VkEvent.MessageNew]: [MessageNewHandler],
|
||||||
[VkEvent.WallPostNew]: PostNewHandler,
|
[VkEvent.WallPostNew]: [PostNewHandler, PostNewCalendarHandler],
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { ConfigGroup, GroupInstance, VkConfig, VkEvent } from "./types";
|
||||||
import { API, Updates, Upload } from "vk-io";
|
import { API, Updates, Upload } from "vk-io";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { flatten, has, keys, prop } from "ramda";
|
import { flatten, has, keys } from "ramda";
|
||||||
import { NextFunction } from "connect";
|
import { NextFunction } from "connect";
|
||||||
import { VkEventHandler } from "./handlers/VkEventHandler";
|
import { VkEventHandler } from "./handlers/VkEventHandler";
|
||||||
import { vkEventToHandler } from "./handlers";
|
import { vkEventToHandler } from "./handlers";
|
||||||
|
@ -10,6 +10,7 @@ import { TelegramService } from "../telegram";
|
||||||
import { Template } from "../template";
|
import { Template } from "../template";
|
||||||
import { TemplateConfig } from "../../config/types";
|
import { TemplateConfig } from "../../config/types";
|
||||||
import { PostgresDB } from "../db/postgres";
|
import { PostgresDB } from "../db/postgres";
|
||||||
|
import { CalendarService } from "../calendar";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to handle VK to Telegram interactions
|
* Service to handle VK to Telegram interactions
|
||||||
|
@ -26,7 +27,8 @@ export class VkService {
|
||||||
private config: VkConfig,
|
private config: VkConfig,
|
||||||
private telegram: TelegramService,
|
private telegram: TelegramService,
|
||||||
private templates: TemplateConfig,
|
private templates: TemplateConfig,
|
||||||
private db: PostgresDB
|
private db: PostgresDB,
|
||||||
|
private calendar: CalendarService | null
|
||||||
) {
|
) {
|
||||||
if (!config.groups.length) {
|
if (!config.groups.length) {
|
||||||
throw new Error("No vk groups to handle. Specify them in config");
|
throw new Error("No vk groups to handle. Specify them in config");
|
||||||
|
@ -120,9 +122,11 @@ export class VkService {
|
||||||
const handlers = this.setupHandlers(group, instance);
|
const handlers = this.setupHandlers(group, instance);
|
||||||
|
|
||||||
handlers.forEach((channel) => {
|
handlers.forEach((channel) => {
|
||||||
keys(channel).forEach((event) => {
|
keys(channel).forEach((event: VkEvent) => {
|
||||||
logger.info(` - ${group.name} listens for ${String(event)}`);
|
logger.info(` - ${group.name} listens for ${String(event)}`);
|
||||||
updates.on(event as any, channel[event].execute);
|
channel[event].forEach((handler) => {
|
||||||
|
updates.on(event as any, handler.execute);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -135,27 +139,31 @@ export class VkService {
|
||||||
private setupHandlers(
|
private setupHandlers(
|
||||||
group: ConfigGroup,
|
group: ConfigGroup,
|
||||||
instance: GroupInstance
|
instance: GroupInstance
|
||||||
): Record<VkEvent, VkEventHandler>[] {
|
): Record<VkEvent, VkEventHandler[]>[] {
|
||||||
return flatten(
|
return flatten(
|
||||||
group.channels.map((chan) =>
|
group.channels.map((chan) =>
|
||||||
chan.events.reduce((acc, event) => {
|
chan.events.reduce((acc, event) => {
|
||||||
const template = new Template(
|
const template = new Template(
|
||||||
prop(event, chan?.templates) ||
|
chan?.templates?.[event] ??
|
||||||
prop(event, group?.templates) ||
|
group?.templates?.[event] ??
|
||||||
prop(event, this.templates)
|
this.templates?.[event]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handler = new vkEventToHandler[event](
|
const handlers = vkEventToHandler[event]?.map(
|
||||||
event,
|
(handler) =>
|
||||||
group,
|
new handler(
|
||||||
chan,
|
event,
|
||||||
instance,
|
group,
|
||||||
this,
|
chan,
|
||||||
this.telegram,
|
instance,
|
||||||
template,
|
this,
|
||||||
this.db
|
this.telegram,
|
||||||
|
template,
|
||||||
|
this.db,
|
||||||
|
this.calendar
|
||||||
|
)
|
||||||
);
|
);
|
||||||
return { ...acc, [event]: handler };
|
return { ...acc, [event]: handlers };
|
||||||
}, {} as Record<VkEvent, VkEventHandler>[])
|
}, {} as Record<VkEvent, VkEventHandler>[])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { API, Upload, Updates } from "vk-io";
|
import { API, Upload, Updates } from "vk-io";
|
||||||
import { WallPostType } from "vk-io/lib/api/schemas/objects";
|
import { WallPostType } from "vk-io/lib/api/schemas/objects";
|
||||||
import { TemplateConfig } from "../../config/types";
|
import { TemplateConfig } from "../../config/types";
|
||||||
|
import { CalendarGroupConfig } from "../calendar/config";
|
||||||
|
|
||||||
export interface VkConfig extends Record<string, any> {
|
export interface VkConfig extends Record<string, any> {
|
||||||
groups: ConfigGroup[];
|
groups: ConfigGroup[];
|
||||||
|
@ -15,6 +16,7 @@ export interface ConfigGroup {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
channels: GroupChannel[];
|
channels: GroupChannel[];
|
||||||
templates: Partial<TemplateConfig>;
|
templates: Partial<TemplateConfig>;
|
||||||
|
calendar?: Partial<CalendarGroupConfig>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupChannel {
|
export interface GroupChannel {
|
||||||
|
@ -37,3 +39,14 @@ export interface GroupInstance {
|
||||||
upload: Upload;
|
upload: Upload;
|
||||||
updates: Updates;
|
updates: Updates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Calendar {
|
||||||
|
createEvent: (
|
||||||
|
calendarId: string,
|
||||||
|
start: Date,
|
||||||
|
end: Date,
|
||||||
|
summary: string,
|
||||||
|
description: string,
|
||||||
|
eventId: string
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import { VkConfig, VkEvent } from "./types";
|
import { VkConfig, VkEvent } from "./types";
|
||||||
import { templateOptionalSchema } from "../../config/validate";
|
import { templateOptionalSchema } from "../../config/validate";
|
||||||
|
import { calendarGroupConfigValidator } from "../calendar/config";
|
||||||
|
|
||||||
const vkChannelEventSchema = yup.string().oneOf(Object.values(VkEvent));
|
const vkChannelEventSchema = yup.string().oneOf(Object.values(VkEvent));
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ export const vkConfigSchema = yup
|
||||||
apiKey: yup.string().required(),
|
apiKey: yup.string().required(),
|
||||||
channels: yup.array().of(vkChannelSchema),
|
channels: yup.array().of(vkChannelSchema),
|
||||||
templates: templateOptionalSchema,
|
templates: templateOptionalSchema,
|
||||||
|
calendar: calendarGroupConfigValidator.optional(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
16
src/utils/date/__tests__/getDayMonthFromText.test.ts
Normal file
16
src/utils/date/__tests__/getDayMonthFromText.test.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { getDayMonthFromText } from "../getDayMonthFromText";
|
||||||
|
|
||||||
|
describe("getDayMonthFromText", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
text:
|
||||||
|
"Попытка номер два.\n\nСегодня. Старт в 19:32 от ГПНТБ. 80км. Асфальт. Темп бодрый. Две остановки на поесть и передохнуть. Маршрут тот же. Автоматическая отмена поката при граде размером более 0.5см.\n\nhttps://map.vault48.org/04092023_razmyatsya_po_asfaltu",
|
||||||
|
created: new Date("2023-09-06T04:24:53.000Z"),
|
||||||
|
expected: [6, 8],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(cases)("$text", ({ text, created, expected }) => {
|
||||||
|
expect(getDayMonthFromText(text, created)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
42
src/utils/date/getDateFromText.ts
Normal file
42
src/utils/date/getDateFromText.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { getDayMonthFromText } from "./getDayMonthFromText";
|
||||||
|
import { getTimeFromString } from "./getTimeFromString";
|
||||||
|
|
||||||
|
export const getDateFromText = (
|
||||||
|
val: string,
|
||||||
|
createdAt: Date
|
||||||
|
): Date | undefined => {
|
||||||
|
const text = val.toLowerCase();
|
||||||
|
|
||||||
|
const time = getTimeFromString(text);
|
||||||
|
if (!time) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayMonth = getDayMonthFromText(text, createdAt);
|
||||||
|
if (!dayMonth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(
|
||||||
|
createdAt.getFullYear(),
|
||||||
|
dayMonth[1],
|
||||||
|
dayMonth[0],
|
||||||
|
time[0],
|
||||||
|
time[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: handle jan and feb posts from december or november
|
||||||
|
// if createdAt is december and date is in January or February
|
||||||
|
// like this:
|
||||||
|
// if (isBefore(date, createdAt) && differenceInMonths(addYears(date, 1), createdAt) < 3) {
|
||||||
|
// return addYears(date, 1)
|
||||||
|
// }
|
||||||
|
// if (isBefore(date, createdAt)) {
|
||||||
|
// const diff = differenceInMonths(date, createdAt);
|
||||||
|
// return diff > 9 && dayMonth[1] >= 10
|
||||||
|
// ? addYears(date, 1)
|
||||||
|
// : undefined;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return date;
|
||||||
|
};
|
85
src/utils/date/getDayMonthFromText.ts
Normal file
85
src/utils/date/getDayMonthFromText.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { addDays } from "date-fns";
|
||||||
|
|
||||||
|
const months = [
|
||||||
|
"янв",
|
||||||
|
"фев",
|
||||||
|
"мар",
|
||||||
|
"апр",
|
||||||
|
"мая",
|
||||||
|
"июн",
|
||||||
|
"июл",
|
||||||
|
"авг",
|
||||||
|
"сент",
|
||||||
|
"октя",
|
||||||
|
"нояб",
|
||||||
|
];
|
||||||
|
|
||||||
|
type DayMonth = [number, number];
|
||||||
|
|
||||||
|
/** Searches for strings like 1 января */
|
||||||
|
const byText = (val: string): DayMonth | undefined => {
|
||||||
|
const text = val.toLowerCase();
|
||||||
|
const match = text.match(
|
||||||
|
/(\d{1,2})\s?(янв|фев|мар|апр|мая|июн|июл|авг|сент|октя|нояб).*/
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!match?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const day = parseInt(match[1]);
|
||||||
|
const month = months.indexOf(match[2]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isFinite(day) ||
|
||||||
|
!Number.isFinite(month) ||
|
||||||
|
day < 0 ||
|
||||||
|
day > 31 ||
|
||||||
|
month < 0 ||
|
||||||
|
month >= months.length
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [day, month];
|
||||||
|
};
|
||||||
|
|
||||||
|
const byNumber = (val: string): DayMonth | undefined => {
|
||||||
|
const text = val.toLowerCase();
|
||||||
|
|
||||||
|
const match = text.match(/\b([0-2]?[0-9]|31)\.([1]?[0-2]|0?[0-9])\b/);
|
||||||
|
if (!match?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const day = parseInt(match[1]);
|
||||||
|
const month = parseInt(match[2]);
|
||||||
|
if (
|
||||||
|
!Number.isFinite(day) ||
|
||||||
|
!Number.isFinite(month) ||
|
||||||
|
day < 1 ||
|
||||||
|
day > 31 ||
|
||||||
|
month < 1 ||
|
||||||
|
month > 12
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [day, month];
|
||||||
|
};
|
||||||
|
|
||||||
|
const byToday = (val: string, refDate: Date): DayMonth | undefined => {
|
||||||
|
const text = val.toLowerCase();
|
||||||
|
if (text.match(/сегодня/)) {
|
||||||
|
return [refDate.getDate(), refDate.getMonth()];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.match(/завтра/)) {
|
||||||
|
const tomorrow = addDays(refDate, 1);
|
||||||
|
return [tomorrow.getDate(), tomorrow.getMonth()];
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
export const getDayMonthFromText = (val: string, refDate: Date) =>
|
||||||
|
byText(val) || byNumber(val) || byToday(val, refDate);
|
18
src/utils/date/getTimeFromString.ts
Normal file
18
src/utils/date/getTimeFromString.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
export const getTimeFromString = (
|
||||||
|
val: string
|
||||||
|
): [number, number] | undefined => {
|
||||||
|
const matches = val.match(/\b([01]?[0-9]|2[0-3])[:\-]([0-5][0-9])\b/);
|
||||||
|
|
||||||
|
if (!matches?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = parseInt(matches[1]);
|
||||||
|
const minutes = parseInt(matches[2]);
|
||||||
|
|
||||||
|
if (hours < 0 || hours > 24 || minutes < 0 || minutes > 60) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [hours, minutes];
|
||||||
|
};
|
8
src/utils/text/getSummaryFromText.ts
Normal file
8
src/utils/text/getSummaryFromText.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { maybeTrim } from './maybeTrim';
|
||||||
|
|
||||||
|
/** Makes summary from first 3 strings of text */
|
||||||
|
export const getSummaryFromText = (text: string) => {
|
||||||
|
const match = text.match(/(.*\n?){0,3}/)
|
||||||
|
|
||||||
|
return match?.[0] ? maybeTrim(match[0].replace(/\n/gm, ''), 96) : '';
|
||||||
|
}
|
1
src/utils/text/maybeTrim.ts
Normal file
1
src/utils/text/maybeTrim.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const maybeTrim = (text: string, maxChars: number) => text.length > maxChars ? `${text.slice(0, maxChars)}...` : text;
|
|
@ -13,10 +13,8 @@
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {},
|
"paths": {},
|
||||||
"lib": ["esnext"],
|
"lib": ["esnext"],
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["./**/*", "./custom.d.ts"]
|
||||||
"./**/*",
|
|
||||||
"./custom.d.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue