1
0
Fork 0
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:
Fedor Katurov 2024-01-10 21:31:06 +07:00
parent 0b663fb96f
commit 6e34090f8f
31 changed files with 1359 additions and 200 deletions

1
.env
View file

@ -1 +0,0 @@
EXPOSE=7003

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ config.yml
node_modules
dist
.idea
/key.local.json

View file

@ -22,6 +22,9 @@ templates:
wall_post_new: templates/post_new.md
group_join: templates/group_join.md
group_leave: templates/group_leave.md
calendar:
keyFile: # key.json
timezone:
# groups:
# - id: 0
# name: 'Group name'
@ -29,6 +32,9 @@ templates:
# secretKey: 'groupSecretKey'
# apiKey: 'callbackApiKey'
# post_types: ['post','copy','reply','postpone','suggest']
# calendar:
# id: abcd@group.calendar.google.com
# enabled: true
# templates: # group's custom templates (optional)
# message_new: templates/custom/message_new.md
# channels:

13
key.json Normal file
View 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"
}

View file

@ -12,7 +12,10 @@
"dependencies": {
"axios": "^0.21.1",
"cross-fetch": "^3.1.4",
"date-fns": "^3.2.0",
"express": "^4.17.1",
"google-auth-library": "^9.4.1",
"googleapis": "^130.0.0",
"handlebars": "^4.7.7",
"http": "^0.0.1-security",
"js-yaml": "^4.0.0",
@ -33,8 +36,8 @@
"telegraf": "^4.3.0",
"to-vfile": "^6.1.0",
"ts-node": "^10.9.2",
"typeorm": "^0.2.32",
"typescript": "^4.2.3",
"typeorm": "^0.3.19",
"typescript": "^4.8.2",
"unist-util-filter": "^3.0.0",
"url": "^0.11.0",
"vk-io": "^4.8.3",
@ -46,6 +49,7 @@
"@types/axios": "^0.14.0",
"@types/express": "^4.17.11",
"@types/handlebars": "^4.1.0",
"@types/jest": "^29.5.11",
"@types/node": "^14.14.37",
"@types/winston": "^2.4.4",
"@types/yargs": "^16.0.1",

View file

@ -23,4 +23,7 @@ export const defaultConfig: Config = {
group_join: "templates/group_join.md",
group_leave: "templates/group_leave.md",
},
calendar: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
};

View file

@ -6,6 +6,10 @@ import { validateConfig } from "./validate";
import { getCmdArg } from "../utils/args";
import { defaultConfig } from "./default";
import { merge } from "lodash";
import {
CalendarKeyFile,
calendarKeyValidator,
} from "../service/calendar/config";
const configPath = getCmdArg("config");
const data = fs.readFileSync(
@ -21,6 +25,18 @@ const config =
export default function prepareConfig() {
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 = {
help: config.templates.help,
help_admin: config.templates.help_admin,

View file

@ -3,6 +3,7 @@ import { VkConfig, VkEvent } from "../service/vk/types";
import { HttpConfig } from "../api/http/types";
import { LoggerConfig } from "../service/logger/types";
import { PostgresConfig } from "../service/db/postgres/types";
import { CalendarConfig, CalendarKeyFile } from "src/service/calendar/config";
export type TemplateConfig = Record<VkEvent, string> &
Partial<Record<"help" | "help_admin", string>>;
@ -14,4 +15,6 @@ export interface Config extends Record<string, any> {
logger: LoggerConfig;
templates: TemplateConfig;
postgres: PostgresConfig;
calendar?: Partial<CalendarConfig>;
calendarKey?: CalendarKeyFile;
}

View file

@ -5,6 +5,7 @@ import { vkConfigSchema } from "../service/vk/validation";
import { telegramConfigSchema } from "../service/telegram/validation";
import { loggerConfigSchema } from "../service/logger/config";
import { dbConfigValidatior } from "../service/db/postgres/validation";
import { calendarConfigValidator } from "../service/calendar/config";
export const templateConfigSchema = object().required().shape({
message_new: string().required(),
@ -29,6 +30,7 @@ const configSchema = object<Config>().required().shape({
logger: loggerConfigSchema,
templates: templateConfigSchema,
postgres: dbConfigValidatior,
calendar: calendarConfigValidator.optional(),
});
export const validateConfig = (config: Config) =>

View file

@ -7,6 +7,7 @@ import { HttpApi } from "./api/http";
import { PostgresDB } from "./service/db/postgres";
import { PgTransport } from "./service/db/postgres/loggerTransport";
import { roll } from "./commands/roll";
import { setupCalendar } from "./service/calendar/setup";
async function main() {
try {
@ -18,7 +19,27 @@ async function main() {
logger.add(new PgTransport(db, { level: "warn" }));
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);
telegramApi.listen();

View 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>;

View 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,
},
});
}
}

View 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;
}
};

View 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;
}

View file

@ -9,14 +9,14 @@ export interface Storage {
tgMessageId: number,
groupId: number,
channel: string
): Promise<Event | undefined>;
getEventById(eventId: number): Promise<Event | undefined>;
): Promise<Event | null>;
getEventById(eventId: number): Promise<Event | null>;
getEventByVKEventId(
type: VkEvent,
eventId: number,
groupId: number,
channel: string
): Promise<Event | undefined>;
): Promise<Event | null>;
createEvent(
type: VkEvent,
eventId: number,
@ -24,7 +24,7 @@ export interface Storage {
channel: string,
tgMessageId: number,
text: Record<any, any>
): Promise<Event | undefined>;
): Promise<Event | null>;
createOrUpdateLike(
messageId: number,
channel: string,
@ -36,12 +36,12 @@ export interface Storage {
channel: string,
messageId: number,
author: number
): Promise<Like | undefined>;
): Promise<Like | null>;
createPost(
eventId: number,
text: string,
vkPostId: number
): Promise<Post | undefined>;
findPostByEvent(eventId: number): Promise<Post | undefined>;
): Promise<Post | null>;
findPostByEvent(eventId: number): Promise<Post | null>;
healthcheck(): Promise<void>;
}

View file

@ -55,10 +55,12 @@ export class PostgresDB implements Storage {
channel: string
) => {
return await this.events.findOne({
type,
tgMessageId,
vkGroupId,
channel,
where: {
type,
tgMessageId,
vkGroupId,
channel,
},
});
};
@ -69,15 +71,19 @@ export class PostgresDB implements Storage {
channel: string
) => {
return await this.events.findOne({
type,
vkEventId,
vkGroupId,
channel,
where: {
type,
vkEventId,
vkGroupId,
channel,
},
});
};
getEventById = async (id: number) => {
return await this.events.findOne({
id,
where: {
id,
},
});
};
@ -109,14 +115,18 @@ export class PostgresDB implements Storage {
getLikeBy = async (channel, messageId, author) => {
return this.likes.findOne({
channel,
messageId,
author,
where: {
channel,
messageId,
author,
},
});
};
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) {
return await this.likes.save({ ...like, text });
@ -131,7 +141,7 @@ export class PostgresDB implements Storage {
};
findPostByEvent = async (eventId: number) => {
return this.posts.findOne({ eventId });
return this.posts.findOne({ where: { eventId } });
};
createPost = async (eventId: number, text: string, vkPostId: number) => {

View 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}`;
}

View file

@ -49,8 +49,7 @@ const PHOTO_CAPTION_LIMIT = 1000;
const POST_TEXT_LIMIT = 4096;
export class PostNewHandler extends VkEventHandler<Fields, Values> {
constructor(...props: any) {
// @ts-ignore
constructor(...props: ConstructorParameters<typeof VkEventHandler<Fields, Values>>) {
super(...props);
this.onInit();
}

View file

@ -1,10 +1,15 @@
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 { TelegramService } from "../../telegram";
import { Template } from "../../template";
import { Storage } from "../../db";
import { Event } from "../../db/postgres/entities/Event";
import logger from "../../logger";
import safeJson from "safe-json-stringify";
@ -20,7 +25,8 @@ export class VkEventHandler<
protected vk: VkService,
protected telegram: TelegramService,
protected template: Template<F, V>,
protected db: Storage
protected db: Storage,
protected calendar: Calendar | null
) {}
public execute: (
@ -57,7 +63,7 @@ export class VkEventHandler<
/**
* Checks for duplicates
*/
getEventById = async (id?: number): Promise<Event | undefined> => {
getEventById = async (id?: number) => {
if (!id) {
return undefined;
}
@ -68,7 +74,7 @@ export class VkEventHandler<
/**
* Checks for duplicates
*/
getEventByVkEventId = async (id?: number): Promise<Event | undefined> => {
getEventByVkEventId = async (id?: number) => {
if (!id) {
return undefined;
}
@ -84,9 +90,7 @@ export class VkEventHandler<
/**
* Checks for duplicates
*/
getEventByTgMessageId = async (
tgMessageId?: number
): Promise<Event | undefined> => {
getEventByTgMessageId = async (tgMessageId?: number) => {
if (!tgMessageId) {
return undefined;
}

View file

@ -1,13 +1,19 @@
import { ConfigGroup, GroupChannel, GroupInstance, VkEvent } from "../types";
import {
Calendar,
ConfigGroup,
GroupChannel,
GroupInstance,
VkEvent,
} from "../types";
import { VkEventHandler } from "./VkEventHandler";
import { MessageNewHandler } from "./MessageNewHandler";
import { StubHandler } from "./StubHandler";
import { VkService } from "../index";
import { TelegramService } from "../../telegram";
import { Template } from "../../template";
import { PostNewHandler } from "./PostNewHandler";
import { Storage } from "../../db";
import { JoinLeaveHandler } from "./JoinLeaveHandler";
import { PostNewCalendarHandler } from "./PostNewCalendarHandler";
interface Handler {
new (
@ -18,13 +24,14 @@ interface Handler {
vk: VkService,
telegram: TelegramService,
template: Template<any, any>,
db: Storage
db: Storage,
calendar: Calendar | null
): VkEventHandler;
}
export const vkEventToHandler: Record<VkEvent, Handler> = {
[VkEvent.GroupJoin]: JoinLeaveHandler,
[VkEvent.GroupLeave]: JoinLeaveHandler,
[VkEvent.MessageNew]: MessageNewHandler,
[VkEvent.WallPostNew]: PostNewHandler,
export const vkEventToHandler: Record<VkEvent, Handler[]> = {
[VkEvent.GroupJoin]: [JoinLeaveHandler],
[VkEvent.GroupLeave]: [JoinLeaveHandler],
[VkEvent.MessageNew]: [MessageNewHandler],
[VkEvent.WallPostNew]: [PostNewHandler, PostNewCalendarHandler],
};

View file

@ -2,7 +2,7 @@ import { ConfigGroup, GroupInstance, VkConfig, VkEvent } from "./types";
import { API, Updates, Upload } from "vk-io";
import logger from "../logger";
import { Request, Response } from "express";
import { flatten, has, keys, prop } from "ramda";
import { flatten, has, keys } from "ramda";
import { NextFunction } from "connect";
import { VkEventHandler } from "./handlers/VkEventHandler";
import { vkEventToHandler } from "./handlers";
@ -10,6 +10,7 @@ import { TelegramService } from "../telegram";
import { Template } from "../template";
import { TemplateConfig } from "../../config/types";
import { PostgresDB } from "../db/postgres";
import { CalendarService } from "../calendar";
/**
* Service to handle VK to Telegram interactions
@ -26,7 +27,8 @@ export class VkService {
private config: VkConfig,
private telegram: TelegramService,
private templates: TemplateConfig,
private db: PostgresDB
private db: PostgresDB,
private calendar: CalendarService | null
) {
if (!config.groups.length) {
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);
handlers.forEach((channel) => {
keys(channel).forEach((event) => {
keys(channel).forEach((event: VkEvent) => {
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(
group: ConfigGroup,
instance: GroupInstance
): Record<VkEvent, VkEventHandler>[] {
): Record<VkEvent, VkEventHandler[]>[] {
return flatten(
group.channels.map((chan) =>
chan.events.reduce((acc, event) => {
const template = new Template(
prop(event, chan?.templates) ||
prop(event, group?.templates) ||
prop(event, this.templates)
chan?.templates?.[event] ??
group?.templates?.[event] ??
this.templates?.[event]
);
const handler = new vkEventToHandler[event](
event,
group,
chan,
instance,
this,
this.telegram,
template,
this.db
const handlers = vkEventToHandler[event]?.map(
(handler) =>
new handler(
event,
group,
chan,
instance,
this,
this.telegram,
template,
this.db,
this.calendar
)
);
return { ...acc, [event]: handler };
return { ...acc, [event]: handlers };
}, {} as Record<VkEvent, VkEventHandler>[])
)
);

View file

@ -1,6 +1,7 @@
import { API, Upload, Updates } from "vk-io";
import { WallPostType } from "vk-io/lib/api/schemas/objects";
import { TemplateConfig } from "../../config/types";
import { CalendarGroupConfig } from "../calendar/config";
export interface VkConfig extends Record<string, any> {
groups: ConfigGroup[];
@ -15,6 +16,7 @@ export interface ConfigGroup {
apiKey: string;
channels: GroupChannel[];
templates: Partial<TemplateConfig>;
calendar?: Partial<CalendarGroupConfig>;
}
export interface GroupChannel {
@ -37,3 +39,14 @@ export interface GroupInstance {
upload: Upload;
updates: Updates;
}
export interface Calendar {
createEvent: (
calendarId: string,
start: Date,
end: Date,
summary: string,
description: string,
eventId: string
) => Promise<void>;
}

View file

@ -1,6 +1,7 @@
import * as yup from "yup";
import { VkConfig, VkEvent } from "./types";
import { templateOptionalSchema } from "../../config/validate";
import { calendarGroupConfigValidator } from "../calendar/config";
const vkChannelEventSchema = yup.string().oneOf(Object.values(VkEvent));
@ -34,6 +35,7 @@ export const vkConfigSchema = yup
apiKey: yup.string().required(),
channels: yup.array().of(vkChannelSchema),
templates: templateOptionalSchema,
calendar: calendarGroupConfigValidator.optional(),
})
),
});

View 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);
});
});

View 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;
};

View 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);

View 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];
};

View 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) : '';
}

View file

@ -0,0 +1 @@
export const maybeTrim = (text: string, maxChars: number) => text.length > maxChars ? `${text.slice(0, maxChars)}...` : text;

View file

@ -13,10 +13,8 @@
"baseUrl": ".",
"paths": {},
"lib": ["esnext"],
"strict": true
"strict": true,
"skipLibCheck": true
},
"include": [
"./**/*",
"./custom.d.ts"
]
"include": ["./**/*", "./custom.d.ts"]
}

929
yarn.lock

File diff suppressed because it is too large Load diff