mirror of
https://github.com/muerwre/vk-tg-bot.git
synced 2025-04-24 22:46:41 +07:00
#4 working likes on posts
This commit is contained in:
parent
a6e6209770
commit
5535a36cb8
6 changed files with 190 additions and 59 deletions
|
@ -1,15 +1,21 @@
|
||||||
import { VkEvent } from "../vk/types";
|
import { VkEvent } from "../vk/types";
|
||||||
import { Like } from "./postgres/entities/Like";
|
import { Like } from "./postgres/entities/Like";
|
||||||
import { Event } from "./postgres/entities/Event";
|
import { Event } from "./postgres/entities/Event";
|
||||||
|
import { Post } from "./postgres/entities/Post";
|
||||||
|
|
||||||
export interface Storage {
|
export interface Storage {
|
||||||
getEvent(
|
getEventByMessageId(
|
||||||
|
type: VkEvent,
|
||||||
|
tgMessageId: number,
|
||||||
|
groupId: number,
|
||||||
|
channel: string
|
||||||
|
): Promise<Event>;
|
||||||
|
getEventById(
|
||||||
type: VkEvent,
|
type: VkEvent,
|
||||||
eventId: number,
|
eventId: number,
|
||||||
groupId: number,
|
groupId: number,
|
||||||
channel: string
|
channel: string
|
||||||
): Promise<Event>;
|
): Promise<Event>;
|
||||||
|
|
||||||
createEvent(
|
createEvent(
|
||||||
type: VkEvent,
|
type: VkEvent,
|
||||||
eventId: number,
|
eventId: number,
|
||||||
|
@ -18,10 +24,14 @@ export interface Storage {
|
||||||
tgMessageId: number,
|
tgMessageId: number,
|
||||||
text: Record<any, any>
|
text: Record<any, any>
|
||||||
): Promise<Event>;
|
): Promise<Event>;
|
||||||
|
createOrUpdateLike(
|
||||||
createOrUpdateLike(like: Partial<Like>): Promise<Like>;
|
messageId: number,
|
||||||
|
channel: string,
|
||||||
|
author: number,
|
||||||
|
text: string
|
||||||
|
): Promise<Like>;
|
||||||
getLikesFor(channel: string, messageId: number): Promise<Like[]>;
|
getLikesFor(channel: string, messageId: number): Promise<Like[]>;
|
||||||
|
|
||||||
getLikeBy(channel: string, messageId: number, author: number): Promise<Like>;
|
getLikeBy(channel: string, messageId: number, author: number): Promise<Like>;
|
||||||
|
createPost(eventId: number, text: string): Promise<Post>;
|
||||||
|
findPostByEvent(eventId: number): Promise<Post>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,9 @@ export class Event {
|
||||||
@Column()
|
@Column()
|
||||||
type: VkEvent;
|
type: VkEvent;
|
||||||
@Column()
|
@Column()
|
||||||
eventId: number;
|
vkEventId: number;
|
||||||
@Column()
|
@Column()
|
||||||
groupId: number;
|
vkGroupId: number;
|
||||||
@Column()
|
@Column()
|
||||||
channel: string;
|
channel: string;
|
||||||
@Column()
|
@Column()
|
||||||
|
|
21
src/service/db/postgres/entities/Post.ts
Normal file
21
src/service/db/postgres/entities/Post.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from "typeorm";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Post {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
@Column()
|
||||||
|
eventId: number;
|
||||||
|
@Column({ type: "text" })
|
||||||
|
text: string;
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import logger from "../../logger";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { Like } from "./entities/Like";
|
import { Like } from "./entities/Like";
|
||||||
import { Event } from "./entities/Event";
|
import { Event } from "./entities/Event";
|
||||||
|
import { Post } from "./entities/Post";
|
||||||
|
|
||||||
const entities = [path.join(__dirname, "./entities/*")];
|
const entities = [path.join(__dirname, "./entities/*")];
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ export class PostgresDB implements Storage {
|
||||||
private connection: Connection;
|
private connection: Connection;
|
||||||
private events: Repository<Event>;
|
private events: Repository<Event>;
|
||||||
private likes: Repository<Like>;
|
private likes: Repository<Like>;
|
||||||
|
private posts: Repository<Post>;
|
||||||
|
|
||||||
constructor(private config: PostgresConfig) {}
|
constructor(private config: PostgresConfig) {}
|
||||||
|
|
||||||
|
@ -29,17 +31,37 @@ export class PostgresDB implements Storage {
|
||||||
|
|
||||||
this.events = this.connection.getRepository(Event);
|
this.events = this.connection.getRepository(Event);
|
||||||
this.likes = this.connection.getRepository(Like);
|
this.likes = this.connection.getRepository(Like);
|
||||||
|
this.posts = this.connection.getRepository(Post);
|
||||||
|
|
||||||
logger.info(`db connected to ${this.config.uri}`);
|
logger.info(`db connected to ${this.config.uri}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
getEvent = async (
|
getEventByMessageId = async (
|
||||||
type: VkEvent,
|
type: VkEvent,
|
||||||
eventId: number,
|
tgMessageId: number,
|
||||||
groupId: number,
|
vkGroupId: number,
|
||||||
channel: string
|
channel: string
|
||||||
) => {
|
) => {
|
||||||
return await this.events.findOne({ type, eventId, groupId, channel });
|
return await this.events.findOne({
|
||||||
|
type,
|
||||||
|
tgMessageId,
|
||||||
|
vkGroupId,
|
||||||
|
channel,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getEventById = async (
|
||||||
|
type: VkEvent,
|
||||||
|
id: number,
|
||||||
|
vkGroupId: number,
|
||||||
|
channel: string
|
||||||
|
) => {
|
||||||
|
return await this.events.findOne({
|
||||||
|
type,
|
||||||
|
id,
|
||||||
|
vkGroupId,
|
||||||
|
channel,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
createEvent = async (
|
createEvent = async (
|
||||||
|
@ -52,8 +74,8 @@ export class PostgresDB implements Storage {
|
||||||
) => {
|
) => {
|
||||||
const event = this.events.create({
|
const event = this.events.create({
|
||||||
type,
|
type,
|
||||||
eventId,
|
vkEventId: eventId,
|
||||||
groupId,
|
vkGroupId: groupId,
|
||||||
channel,
|
channel,
|
||||||
tgMessageId,
|
tgMessageId,
|
||||||
text,
|
text,
|
||||||
|
@ -76,25 +98,26 @@ export class PostgresDB implements Storage {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
createOrUpdateLike = async ({
|
createOrUpdateLike = async (messageId, channel, author, text) => {
|
||||||
channel,
|
|
||||||
author,
|
|
||||||
text,
|
|
||||||
messageId,
|
|
||||||
}: Partial<Like>) => {
|
|
||||||
const like = await this.likes.findOne({ channel, author, messageId });
|
const like = await this.likes.findOne({ channel, author, messageId });
|
||||||
|
|
||||||
if (like) {
|
if (like) {
|
||||||
like.text = text;
|
return await this.likes.save({ ...like, text });
|
||||||
return await this.likes.save(like);
|
|
||||||
} else {
|
} else {
|
||||||
const created = await this.likes.create({
|
return this.likes.save({
|
||||||
channel,
|
channel,
|
||||||
author,
|
author,
|
||||||
text,
|
text,
|
||||||
messageId,
|
messageId,
|
||||||
});
|
});
|
||||||
return created[0];
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
findPostByEvent = async (eventId: number) => {
|
||||||
|
return this.posts.findOne({ eventId });
|
||||||
|
};
|
||||||
|
|
||||||
|
createPost = async (eventId: number, text: string) => {
|
||||||
|
return this.posts.save({ eventId, text });
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { NextMiddleware } from "middleware-io";
|
||||||
import { UsersUserFull } from "vk-io/lib/api/schemas/objects";
|
import { UsersUserFull } from "vk-io/lib/api/schemas/objects";
|
||||||
import { ConfigGroup } from "../types";
|
import { ConfigGroup } from "../types";
|
||||||
import { ExtraReplyMessage } from "telegraf/typings/telegram-types";
|
import { ExtraReplyMessage } from "telegraf/typings/telegram-types";
|
||||||
import { InlineKeyboardButton, Update } from "typegram";
|
import { InlineKeyboardButton, InlineKeyboardMarkup, Update } from "typegram";
|
||||||
import { keys } from "ramda";
|
import { keys } from "ramda";
|
||||||
import { extractURLs } from "../../../utils/extract";
|
import { extractURLs } from "../../../utils/extract";
|
||||||
import logger from "../../logger";
|
import logger from "../../logger";
|
||||||
|
@ -15,8 +15,8 @@ type Button = "links" | "likes";
|
||||||
type UrlPrefix = string;
|
type UrlPrefix = string;
|
||||||
type ExtraGenerator = (
|
type ExtraGenerator = (
|
||||||
text: string,
|
text: string,
|
||||||
messageId?: number
|
eventId?: number
|
||||||
) => InlineKeyboardButton[];
|
) => Promise<InlineKeyboardButton[]>;
|
||||||
|
|
||||||
interface Fields {
|
interface Fields {
|
||||||
image?: boolean;
|
image?: boolean;
|
||||||
|
@ -55,7 +55,7 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.getEvent(id);
|
const exist = await this.getEventById(id);
|
||||||
if (exist) {
|
if (exist) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`received duplicate entry for ${this.group.name}, ${this.type}, ${id}`
|
`received duplicate entry for ${this.group.name}, ${this.type}, ${id}`
|
||||||
|
@ -78,17 +78,21 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
|
||||||
|
|
||||||
const extras: ExtraReplyMessage = {
|
const extras: ExtraReplyMessage = {
|
||||||
disable_web_page_preview: true,
|
disable_web_page_preview: true,
|
||||||
|
reply_markup: await this.createKeyboard(text),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.appendExtras(extras, text);
|
|
||||||
|
|
||||||
const msg = await this.telegram.sendMessageToChan(
|
const msg = await this.telegram.sendMessageToChan(
|
||||||
this.channel,
|
this.channel,
|
||||||
parsed,
|
parsed,
|
||||||
extras
|
extras
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.createEvent(id, msg.message_id, context.wall.toJSON());
|
const event = await this.createEvent(
|
||||||
|
id,
|
||||||
|
msg.message_id,
|
||||||
|
context.wall.toJSON()
|
||||||
|
);
|
||||||
|
await this.db.createPost(event.id, context.wall.text);
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
|
@ -103,33 +107,32 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
|
||||||
/**
|
/**
|
||||||
* Creates extras
|
* Creates extras
|
||||||
*/
|
*/
|
||||||
private appendExtras = (
|
private createKeyboard = async (
|
||||||
extras: ExtraReplyMessage,
|
|
||||||
text: string,
|
text: string,
|
||||||
messageId?: number
|
eventId?: number
|
||||||
) => {
|
): Promise<InlineKeyboardMarkup> => {
|
||||||
const { buttons } = this.template.fields;
|
const { buttons } = this.template.fields;
|
||||||
|
|
||||||
if (!buttons?.length) {
|
if (!buttons?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyboard = buttons
|
const rows = await Promise.all(
|
||||||
.map((button) => this.extrasGenerators[button](text, messageId))
|
buttons.map((button) => this.extrasGenerators[button](text, eventId))
|
||||||
.filter((el) => el && el.length);
|
);
|
||||||
|
const inline_keyboard = rows.filter((el) => el && el.length);
|
||||||
|
|
||||||
if (!keyboard.length) {
|
if (!inline_keyboard.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
extras.reply_markup = {
|
return { inline_keyboard };
|
||||||
inline_keyboard: keyboard,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates link buttons for post
|
* Generates link buttons for post
|
||||||
*/
|
*/
|
||||||
private generateLinks: ExtraGenerator = (text) => {
|
private generateLinks: ExtraGenerator = async (text) => {
|
||||||
const links = this.template.fields.links;
|
const links = this.template.fields.links;
|
||||||
|
|
||||||
if (!links) {
|
if (!links) {
|
||||||
|
@ -156,8 +159,25 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
|
||||||
/**
|
/**
|
||||||
* Generates like button
|
* Generates like button
|
||||||
*/
|
*/
|
||||||
private generateLikes: ExtraGenerator = () => {
|
private generateLikes: ExtraGenerator = async (text, eventId) => {
|
||||||
return this.likes.map((like, i) => ({
|
if (eventId) {
|
||||||
|
const event = await this.getEventById(eventId);
|
||||||
|
const likes = await this.db.getLikesFor(this.channel, event.tgMessageId);
|
||||||
|
const withCount = likes.reduce(
|
||||||
|
(acc, like) => ({
|
||||||
|
...acc,
|
||||||
|
[like.text]: acc[like.text] ? acc[like.text] + 1 : 1,
|
||||||
|
}),
|
||||||
|
{} as Record<string, number>
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.likes.map((like) => ({
|
||||||
|
text: withCount[like] ? `${like} ${withCount[like]}` : like,
|
||||||
|
callback_data: `/like ${this.channel} ${like}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.likes.map((like) => ({
|
||||||
text: like,
|
text: like,
|
||||||
callback_data: `/like ${this.channel} ${like}`,
|
callback_data: `/like ${this.channel} ${like}`,
|
||||||
}));
|
}));
|
||||||
|
@ -191,14 +211,15 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
|
||||||
*/
|
*/
|
||||||
onLikeAction = async (ctx: LikeCtx, next) => {
|
onLikeAction = async (ctx: LikeCtx, next) => {
|
||||||
const id = ctx.update.callback_query.message.message_id;
|
const id = ctx.update.callback_query.message.message_id;
|
||||||
const [_, channel, emo] = ctx.match;
|
const author = ctx.update.callback_query.from.id;
|
||||||
const exist = await this.getEvent(id);
|
const [, channel, emo] = ctx.match;
|
||||||
|
const event = await this.getEventByTgMessageId(id);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!channel ||
|
!channel ||
|
||||||
!emo ||
|
!emo ||
|
||||||
!id ||
|
!id ||
|
||||||
!exist ||
|
!event ||
|
||||||
channel != this.channel ||
|
channel != this.channel ||
|
||||||
!this.likes.includes(emo)
|
!this.likes.includes(emo)
|
||||||
) {
|
) {
|
||||||
|
@ -206,17 +227,50 @@ export class PostNewHandler extends VkEventHandler<Fields, Values> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// const extras: ExtraReplyMessage = {};
|
const post = await this.db.findPostByEvent(event.id);
|
||||||
// this.appendExtras(extras, exist.text);
|
if (!post) {
|
||||||
// await ctx.telegram.editMessageReplyMarkup(
|
await next();
|
||||||
// ctx.chat.id,
|
return;
|
||||||
// id,
|
}
|
||||||
// ctx.inlineMessageId,
|
|
||||||
// extras.reply_markup.inline_keyboard
|
|
||||||
// );
|
|
||||||
|
|
||||||
logger.warn(
|
const like = await this.getLike(author, id);
|
||||||
|
if (like?.text === emo) {
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.createOrUpdateLike(author, event.tgMessageId, emo);
|
||||||
|
|
||||||
|
const markup = await this.createKeyboard(post.text, event.id);
|
||||||
|
|
||||||
|
await ctx.telegram.editMessageReplyMarkup(
|
||||||
|
ctx.chat.id,
|
||||||
|
id,
|
||||||
|
ctx.inlineMessageId,
|
||||||
|
markup
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
`someone reacted with ${emo} to message ${id} on channel ${channel}`
|
`someone reacted with ${emo} to message ${id} on channel ${channel}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
createOrUpdateLike = async (
|
||||||
|
author: number,
|
||||||
|
messageId: number,
|
||||||
|
emo: string
|
||||||
|
) => {
|
||||||
|
return await this.db.createOrUpdateLike(
|
||||||
|
messageId,
|
||||||
|
this.channel,
|
||||||
|
author,
|
||||||
|
emo
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
getLike = async (author: number, messageId: number) => {
|
||||||
|
return await this.db.getLikeBy(this.channel, messageId, author);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,12 +51,35 @@ export class VkEventHandler<
|
||||||
/**
|
/**
|
||||||
* Checks for duplicates
|
* Checks for duplicates
|
||||||
*/
|
*/
|
||||||
getEvent = async (id?: number): Promise<Event | undefined> => {
|
getEventById = async (id?: number): Promise<Event | undefined> => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.db.getEvent(this.type, id, this.group.id, this.channel);
|
return await this.db.getEventById(
|
||||||
|
this.type,
|
||||||
|
id,
|
||||||
|
this.group.id,
|
||||||
|
this.channel
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for duplicates
|
||||||
|
*/
|
||||||
|
getEventByTgMessageId = async (
|
||||||
|
tgMessageId?: number
|
||||||
|
): Promise<Event | undefined> => {
|
||||||
|
if (!tgMessageId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.db.getEventByMessageId(
|
||||||
|
this.type,
|
||||||
|
tgMessageId,
|
||||||
|
this.group.id,
|
||||||
|
this.channel
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue