diff --git a/packages/api-plugin-template.erxes/src/index.ts b/packages/api-plugin-template.erxes/src/index.ts index b3f009dabd..703023b7b9 100644 --- a/packages/api-plugin-template.erxes/src/index.ts +++ b/packages/api-plugin-template.erxes/src/index.ts @@ -101,6 +101,7 @@ app.disable('x-powered-by'); app.use(cors()); +//@ts-ignore app.use(cookieParser()); // for health checking diff --git a/packages/plugin-telegram-api/.env.sample b/packages/plugin-telegram-api/.env.sample new file mode 100644 index 0000000000..aa9e5b1498 --- /dev/null +++ b/packages/plugin-telegram-api/.env.sample @@ -0,0 +1,14 @@ +# general +NODE_ENV=development +PORT=4011 + +# MongoDB +MONGO_URL=mongodb://localhost/erxes + +# RabbitMQ +RABBITMQ_HOST=amqp://localhost + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= \ No newline at end of file diff --git a/packages/plugin-telegram-api/package.json b/packages/plugin-telegram-api/package.json new file mode 100644 index 0000000000..663fe32c47 --- /dev/null +++ b/packages/plugin-telegram-api/package.json @@ -0,0 +1,13 @@ +{ + "name": "@erxes/plugin-telegram-api", + "version": "1.0.0", + "scripts": { + "install-deps": "cd .erxes && yarn install", + "dev": "cd .erxes && yarn dev", + "build": "cd .erxes && yarn build", + "start": "cd .erxes/dist/plugin-telegram-api/.erxes && node src" + }, + "dependencies": { + "telegraf": "^4.12.2" + } +} diff --git a/packages/plugin-telegram-api/src/bot.ts b/packages/plugin-telegram-api/src/bot.ts new file mode 100644 index 0000000000..421a1d3f28 --- /dev/null +++ b/packages/plugin-telegram-api/src/bot.ts @@ -0,0 +1,72 @@ +import { Telegraf } from 'telegraf'; +import { receiveMessage } from './receiveMessage'; +import { message } from 'telegraf/filters'; +import { Chats } from './models'; + +export class TelegramBot { + private _bot; + private _info; + error?: string; + + constructor(botToken: string) { + try { + this._bot = new Telegraf(botToken); + } catch (e) { + this.error = e.message; + } + } + + getMe = async () => { + const me = await this._bot.telegram.getMe(); + this._info = me; + return me; + }; + + run = async accountId => { + if (!this._info) { + await this.getMe(); + } + this._bot.on(message('text'), receiveMessage(accountId)); + + this._bot.on(['my_chat_member'], async ctx => { + await this.updateChat(ctx, accountId); + }); + + this._bot.launch(); + console.log(`Bot "${this._info.username}" is running`); + return; + }; + + updateChat = async (ctx, botAccountId) => { + const update = ctx.update; + const memberUpdate = update.my_chat_member; + const { chat, new_chat_member } = memberUpdate; + const { id: telegramId, title, type: chatType } = chat; + + switch (new_chat_member.status) { + case 'kicked': + case 'left': + console.log(`Removing telegram chat: ${telegramId}`); + const removingResult = await Chats.remove({ + botAccountId, + telegramId + }); + return; + case 'member': + case 'administrator': + console.log(`Add/Updating telegram chat: ${telegramId}`); + const addingChat = await Chats.createOrUpdate( + { telegramId, botAccountId }, + { + botAccountId, + telegramId, + title, + chatType, + memberType: new_chat_member.status + } + ); + default: + return; + } + }; +} diff --git a/packages/plugin-telegram-api/src/configs.ts b/packages/plugin-telegram-api/src/configs.ts new file mode 100644 index 0000000000..b653963672 --- /dev/null +++ b/packages/plugin-telegram-api/src/configs.ts @@ -0,0 +1,43 @@ +import typeDefs from './graphql/typeDefs'; +import resolvers from './graphql/resolvers'; + +import { initBroker } from './messageBroker'; +import init from './controller'; + +export let mainDb; +export let graphqlPubsub; +export let serviceDiscovery; + +export let debug; + +export default { + name: 'telegram', + graphql: sd => { + serviceDiscovery = sd; + return { + typeDefs, + resolvers + }; + }, + meta: { + inboxIntegration: { + kind: 'telegram', + label: 'Telegram' + } + }, + apolloServerContext: async context => { + return context; + }, + + onServerInit: async options => { + const app = options.app; + mainDb = options.db; + + debug = options.debug; + graphqlPubsub = options.pubsubClient; + + initBroker(options.messageBrokerClient); + + init(app); + } +}; diff --git a/packages/plugin-telegram-api/src/controller.ts b/packages/plugin-telegram-api/src/controller.ts new file mode 100644 index 0000000000..5ad23ee3ca --- /dev/null +++ b/packages/plugin-telegram-api/src/controller.ts @@ -0,0 +1,38 @@ +import { Accounts, Chats } from './models'; +import { TelegramBot } from './bot'; + +const searchMessages = (linkedin, criteria) => { + return new Promise((resolve, reject) => { + const messages: any = []; + }); +}; + +// controller for telegram +const init = async app => { + const accounts = await Accounts.find({}); + + accounts.forEach(acct => { + const bot = new TelegramBot(acct.token); + bot.run(acct.id); + }); + // TODO: read accounts from mongo and spawn a bot for each account + + app.get('/chats', async (req, res) => { + const chats = await Chats.find({}); + res.send(chats); + }); + + app.post('/receive', async (req, res, next) => { + try { + // write receive code here + + res.send('Successfully receiving message'); + } catch (e) { + return next(new Error(e)); + } + + res.sendStatus(200); + }); +}; + +export default init; diff --git a/packages/plugin-telegram-api/src/graphql/index.ts b/packages/plugin-telegram-api/src/graphql/index.ts new file mode 100644 index 0000000000..1e974c15bd --- /dev/null +++ b/packages/plugin-telegram-api/src/graphql/index.ts @@ -0,0 +1,9 @@ +import resolvers from './resolvers'; +import typeDefs from './typeDefs'; + +const mod = { + resolvers, + typeDefs +}; + +export default mod; diff --git a/packages/plugin-telegram-api/src/graphql/resolvers/index.ts b/packages/plugin-telegram-api/src/graphql/resolvers/index.ts new file mode 100644 index 0000000000..deaf15dab9 --- /dev/null +++ b/packages/plugin-telegram-api/src/graphql/resolvers/index.ts @@ -0,0 +1,15 @@ +import customScalars from '@erxes/api-utils/src/customScalars'; +import mutations from './mutations'; +import queries from './queries'; + +const resolvers: any = { + ...customScalars, + Mutation: { + ...mutations + }, + Query: { + ...queries + } +}; + +export default resolvers; diff --git a/packages/plugin-telegram-api/src/graphql/resolvers/mutations.ts b/packages/plugin-telegram-api/src/graphql/resolvers/mutations.ts new file mode 100644 index 0000000000..0cfc61a27a --- /dev/null +++ b/packages/plugin-telegram-api/src/graphql/resolvers/mutations.ts @@ -0,0 +1,36 @@ +import { TelegramBot } from '../../bot'; +import { Accounts, Chats } from '../../models'; +import { IContext } from '@erxes/api-utils/src/types'; + +const telegramMutations = { + async telegramAccountRemove( + _root, + { _id }: { _id: string }, + _context: IContext + ) { + await Accounts.removeAccount(_id); + + return 'deleted'; + }, + + async telegramAccountAdd(_root, { token }, _context: IContext) { + const currentAccount = await Accounts.findOne({ token }); + + if (!currentAccount) { + const bot = new TelegramBot(token); + const info = await bot.getMe(); + const account = await Accounts.create({ + token, + name: info.username + }); + + bot.run(account.id); + + return 'Account created'; + } + + throw new Error(`Account already exists: ${currentAccount.name}`); + } +}; + +export default telegramMutations; diff --git a/packages/plugin-telegram-api/src/graphql/resolvers/queries.ts b/packages/plugin-telegram-api/src/graphql/resolvers/queries.ts new file mode 100644 index 0000000000..4ea4d6bb60 --- /dev/null +++ b/packages/plugin-telegram-api/src/graphql/resolvers/queries.ts @@ -0,0 +1,42 @@ +import { IContext } from '@erxes/api-utils/src/types'; +import { Accounts, Chats, Messages } from '../../models'; + +const queries = { + async telegramConversationDetail( + _root, + { conversationId }, + _context: IContext + ) { + const messages = await Messages.find({ + inboxConversationId: conversationId + }); + + const convertEmails = emails => + (emails || []).map(item => ({ name: item.name, email: item.address })); + + return messages.map(message => { + return { + _id: message._id, + mailData: { + messageId: message.messageId, + from: convertEmails(message.from), + to: convertEmails(message.to), + cc: convertEmails(message.cc), + bcc: convertEmails(message.bcc), + subject: message.subject, + body: message.body + } + }; + }); + }, + + async telegramAccounts(_root, _args, _context: IContext) { + return Accounts.getAccounts(); + }, + + async telegramChats(_root, _args, _context: IContext) { + return Chats.getAllChats(); + } +}; + +export default queries; diff --git a/packages/plugin-telegram-api/src/graphql/typeDefs.ts b/packages/plugin-telegram-api/src/graphql/typeDefs.ts new file mode 100644 index 0000000000..7a59b8bf31 --- /dev/null +++ b/packages/plugin-telegram-api/src/graphql/typeDefs.ts @@ -0,0 +1,37 @@ +import { gql } from 'apollo-server-express'; + +const types = ` + type Telegram { + _id: String! + title: String + mailData: JSON + } +`; + +const queries = ` + telegramConversationDetail(conversationId: String!): [Telegram] + telegramAccounts: JSON + telegramChats: JSON +`; + +const mutations = ` + telegramAccountRemove(_id: String!): String + telegramAccountAdd(token: String!): String +`; + +const typeDefs = gql` + scalar JSON + scalar Date + + ${types} + + extend type Query { + ${queries} + } + + extend type Mutation { + ${mutations} + } +`; + +export default typeDefs; diff --git a/packages/plugin-telegram-api/src/messageBroker.ts b/packages/plugin-telegram-api/src/messageBroker.ts new file mode 100644 index 0000000000..77cd6ff5a6 --- /dev/null +++ b/packages/plugin-telegram-api/src/messageBroker.ts @@ -0,0 +1,122 @@ +import * as dotenv from 'dotenv'; +import * as strip from 'strip'; +import { + ISendMessageArgs, + sendMessage as sendCommonMessage +} from '@erxes/api-utils/src/core'; +import { serviceDiscovery } from './configs'; +import { Accounts, Chats, Customers, Integrations, Messages } from './models'; +import { Telegraf } from 'telegraf'; + +dotenv.config(); + +let client; + +export const initBroker = async cl => { + client = cl; + + const { consumeRPCQueue } = client; + + consumeRPCQueue('telegram:createIntegration', async ({ data }) => { + const { doc, integrationId } = data; + + const customData = JSON.parse(doc.data); + await Integrations.create({ + inboxIntegrationId: integrationId, + ...(doc || {}), + ...customData + }); + + return { + status: 'success' + }; + }); + + consumeRPCQueue( + 'telegram:removeIntegrations', + async ({ data: { integrationId } }) => { + await Messages.remove({ inboxIntegrationId: integrationId }); + await Customers.remove({ inboxIntegrationId: integrationId }); + await Integrations.remove({ inboxIntegrationId: integrationId }); + + return { + status: 'success' + }; + } + ); + + consumeRPCQueue('telegram:api_to_integrations', async ({ data }) => { + const { action, payload } = data; + const doc = JSON.parse(payload || '{}'); + + if (!doc.internal) { + const { integrationId, conversationId, content } = doc; + + const integration = await Integrations.findOne({ + inboxIntegrationId: integrationId + }); + + if (!integration) { + return { status: 'error', data: 'Integration not found' }; + } + + const account = await Accounts.findOne({ _id: integration.accountId }); + + if (!account) { + return { status: 'error', data: 'Telegram account not found' }; + } + + const chat = await Chats.findOne({ _id: integration.telegramChatId }); + + let strippedContent = strip(content); + strippedContent = strippedContent.replace(/&/g, '&'); + + const client = new Telegraf(account.token); + const response = await client.telegram.sendMessage( + chat.telegramId, + strippedContent + ); + + const localMessage = await Messages.create({ + ...doc, + inboxIntegrationId: integrationId, + inboxConversationId: conversationId, + messageId: response.message_id, + subject: strippedContent, + body: strippedContent, + from: { + address: response.from?.id, + name: `${response.from?.first_name} ${response.from?.last_name}` + }, + chatId: response.chat.id + }); + + return { + status: 'success', + data: { ...localMessage.toObject(), conversationId } + }; + } + }); +}; + +export default function() { + return client; +} + +export const sendContactsMessage = (args: ISendMessageArgs) => { + return sendCommonMessage({ + client, + serviceDiscovery, + serviceName: 'contacts', + ...args + }); +}; + +export const sendInboxMessage = (args: ISendMessageArgs) => { + return sendCommonMessage({ + client, + serviceDiscovery, + serviceName: 'inbox', + ...args + }); +}; diff --git a/packages/plugin-telegram-api/src/models.ts b/packages/plugin-telegram-api/src/models.ts new file mode 100644 index 0000000000..fe9ff6bf4b --- /dev/null +++ b/packages/plugin-telegram-api/src/models.ts @@ -0,0 +1,132 @@ +import { Schema, model } from 'mongoose'; + +export const chatSchema = new Schema({ + botAccountId: String, + telegramId: { type: String, unique: true }, + title: String, + chatType: String, + memberType: String +}); + +export const loadChatClass = () => { + class Chat { + static async getAllChats() { + return Chats.find({}); + } + + static async createOrUpdate(find, chat) { + return Chats.update(find, chat, { upsert: true }); + } + } + + chatSchema.loadClass(Chat); + + return chatSchema; +}; + +export const customerSchema = new Schema({ + inboxIntegrationId: String, + contactsId: String, + telegramId: { type: String, unique: true }, + firstName: String, + lastName: String +}); + +export const loadCustomerClass = () => { + class Customer {} + + customerSchema.loadClass(Customer); + + return customerSchema; +}; + +const telegramAddressSchema = new Schema( + { + name: String, + address: String + }, + { _id: false } +); + +export const messageSchema = new Schema({ + inboxIntegrationId: String, + inboxConversationId: String, + subject: String, + messageId: { type: String, unique: true }, + chatId: String, + inReplyTo: String, + references: [String], + body: String, + to: telegramAddressSchema, + cc: telegramAddressSchema, + bcc: telegramAddressSchema, + from: telegramAddressSchema, + createdAt: { type: Date, index: true, default: new Date() } +}); + +export const loadMessageClass = () => { + class Message {} + + messageSchema.loadClass(Message); + + return messageSchema; +}; + +// schema for integration document +export const integrationSchema = new Schema({ + inboxIntegrationId: String, + accountId: String, + telegramChatId: String +}); + +export const loadIntegrationClass = () => { + class Integration {} + + integrationSchema.loadClass(Integration); + + return integrationSchema; +}; + +// schema for integration account +export const accountSchema = new Schema({ + name: String, + token: String +}); + +export const loadAccountClass = () => { + class Account { + static async removeAccount(_id) { + return Accounts.deleteOne({ _id }); + } + + static async getAccounts() { + return Accounts.find({}); + } + } + + accountSchema.loadClass(Account); + + return accountSchema; +}; + +export const Chats = model('telegram_chats', loadChatClass()); + +export const Customers = model( + 'telegram_customers', + loadCustomerClass() +); + +export const Integrations = model( + 'telegram_integrations', + loadIntegrationClass() +); + +export const Messages = model( + 'telegram_messages', + loadMessageClass() +); + +export const Accounts = model( + 'telegram_accounts', + loadAccountClass() +); diff --git a/packages/plugin-telegram-api/src/receiveMessage.ts b/packages/plugin-telegram-api/src/receiveMessage.ts new file mode 100644 index 0000000000..38ffc197a5 --- /dev/null +++ b/packages/plugin-telegram-api/src/receiveMessage.ts @@ -0,0 +1,139 @@ +import { Message } from 'telegraf/typings/core/types/typegram'; +import { Chats, Customers, Integrations, Messages } from './models'; +import { sendContactsMessage, sendInboxMessage } from './messageBroker'; +import { Context } from 'telegraf'; + +const getOrCreateCustomer = async ( + message: Message.TextMessage, + integration +) => { + const from = message.from?.id; + + const prev = await Customers.findOne({ telegramId: from }); + + let customerId; + + if (prev) { + return prev.contactsId; + } + + const customer = await sendContactsMessage({ + subdomain: 'os', + action: 'customers.findOne', + data: { + telegramId: from + }, + isRPC: true + }); + + if (customer) { + customerId = customer._id; + } else { + const apiCustomerResponse = await sendContactsMessage({ + subdomain: 'os', + action: 'customers.createCustomer', + data: { + firstName: message.from?.first_name, + lastName: message.from?.last_name, + telegramId: from + }, + isRPC: true + }); + + customerId = apiCustomerResponse._id; + } + + await Customers.create({ + inboxIntegrationId: integration._id, + contactsId: customerId, + telegramId: from + }); + + return customerId; +}; + +export const receiveMessage = (accountId: string) => async (ctx: Context) => { + // @ts-ignore + const message = ctx.update.message as Message.TextMessage; + + // Don't listen for bot messages + if (message.from?.is_bot) return; + + // Ignore private messages + if (message.chat.type === 'private') return; + + const foundMessage = await Messages.findOne({ + messageId: message.message_id, + chatId: message.chat.id + }); + + if (foundMessage) { + return; + } + + const chat = await Chats.findOne({ telegramId: message.chat.id }); + + const integration = await Integrations.findOne({ + telegramChatId: chat._id, + accountId: accountId + }); + if (!integration) { + return; + } + + const customerId = await getOrCreateCustomer(message, integration); + + let conversationId; + + const relatedMessage = await Messages.findOne({ + chatId: message.chat.id + }); + + conversationId = relatedMessage?.inboxConversationId; + const conversation = await sendInboxMessage({ + subdomain: 'os', + action: 'integrations.receive', + data: { + action: 'create-or-update-conversation', + payload: JSON.stringify({ + integrationId: integration.inboxIntegrationId, + conversationId, + customerId, + createdAt: message.date, + content: message.text + }) + }, + isRPC: true + }); + + conversationId = conversation?._id; + + await sendInboxMessage({ + subdomain: 'os', + action: 'integrations.receive', + data: { + action: 'create-conversation-message', + payload: JSON.stringify({ + integrationId: integration.inboxIntegrationId, + customerId, + conversationId, + createdAt: message.date, + content: message.text + }) + } + }); + + await Messages.create({ + inboxIntegrationId: integration.inboxIntegrationId, + inboxConversationId: conversationId, + createdAt: message.date, + messageId: message.message_id, + chatId: message.chat.id, + subject: message.text, + body: message.text, + from: message.from && { + address: message.from.id, + name: `${message.from.first_name} ${message.from.last_name}` + } + }); +}; diff --git a/packages/plugin-telegram-api/tsconfig.json b/packages/plugin-telegram-api/tsconfig.json new file mode 100644 index 0000000000..51adf3baab --- /dev/null +++ b/packages/plugin-telegram-api/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.erxes/tsconfig.json" +} diff --git a/packages/plugin-telegram-ui/package.json b/packages/plugin-telegram-ui/package.json new file mode 100644 index 0000000000..7f07233e3a --- /dev/null +++ b/packages/plugin-telegram-ui/package.json @@ -0,0 +1,9 @@ +{ + "name": "plugin-telegram-ui", + "version": "1.0.0", + "scripts": { + "install-deps": "cd .erxes && yarn install", + "start": "cd .erxes && yarn start", + "build": "cd .erxes && yarn build" + } +} diff --git a/packages/plugin-telegram-ui/src/App.tsx b/packages/plugin-telegram-ui/src/App.tsx new file mode 100644 index 0000000000..eec2e4dd31 --- /dev/null +++ b/packages/plugin-telegram-ui/src/App.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { PluginLayout } from '@erxes/ui/src/styles/main'; + +const App = () => { + return ; +}; + +export default App; diff --git a/packages/plugin-telegram-ui/src/components/Accounts.tsx b/packages/plugin-telegram-ui/src/components/Accounts.tsx new file mode 100644 index 0000000000..fe6fd3fdd3 --- /dev/null +++ b/packages/plugin-telegram-ui/src/components/Accounts.tsx @@ -0,0 +1,120 @@ +import { + AccountBox, + AccountItem, + AccountTitle +} from '@erxes/ui-inbox/src/settings/integrations/styles'; +import { CenterText } from '@erxes/ui-log/src/activityLogs/styles'; +import Button from '@erxes/ui/src/components/Button'; +import EmptyState from '@erxes/ui/src/components/EmptyState'; +import { + ControlLabel, + Form, + FormControl, + FormGroup +} from '@erxes/ui/src/components/form'; +import { __, confirm } from '@erxes/ui/src/utils'; +import React from 'react'; + +type Props = { + accounts: any[]; + addAccount: (token: string) => void; + removeAccount: (accountId: string) => void; + onSelectAccount: (accountId: string) => void; + accountId: string; +}; + +class Accounts extends React.Component { + constructor(props) { + super(props); + + this.state = { + newBotKey: '' + }; + } + onRemove(accountId: string) { + const { removeAccount } = this.props; + + confirm().then(() => { + removeAccount(accountId); + this.setState({ accountId: '' }); + }); + } + + renderAccountAction() { + const handleSubmit = ({ token }) => { + this.props.addAccount(token); + }; + + return ( +
( + <> + + Bot Token +

{__('Paste the bot token here')}

+ +
+ + + )} + /> + ); + } + + renderAccounts() { + const { accounts, onSelectAccount, accountId } = this.props; + + if (accounts.length === 0) { + return ( + + ); + } + + return accounts.map(account => ( + + {account.name} + +
+ + + +
+
+ )); + } + + render() { + return ( + <> + + {__('Linked Accounts')} + {this.renderAccounts()} + + {__('OR')} + {this.renderAccountAction()} + + ); + } +} + +export default Accounts; diff --git a/packages/plugin-telegram-ui/src/components/ConversationDetail.tsx b/packages/plugin-telegram-ui/src/components/ConversationDetail.tsx new file mode 100644 index 0000000000..b7b5c97506 --- /dev/null +++ b/packages/plugin-telegram-ui/src/components/ConversationDetail.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { gql } from '@apollo/client'; +import * as compose from 'lodash.flowright'; +import { graphql } from '@apollo/client/react/hoc'; +import MailConversation from '@erxes/ui-inbox/src/inbox/components/conversationDetail/workarea/mail/MailConversation'; +import { queries } from '../graphql'; + +class Detail extends React.Component { + render() { + const { currentConversation, messagesQuery } = this.props; + + if (messagesQuery.loading) { + return null; + } + + const messages = messagesQuery.telegramConversationDetail || []; + + return ( + + ); + } +} + +const WithQuery = compose( + graphql(gql(queries.detail), { + name: 'messagesQuery', + options: ({ currentId }) => { + return { + variables: { + conversationId: currentId + }, + fetchPolicy: 'network-only' + }; + } + }) +)(Detail); + +export default WithQuery; diff --git a/packages/plugin-telegram-ui/src/components/Form.tsx b/packages/plugin-telegram-ui/src/components/Form.tsx new file mode 100644 index 0000000000..73d688a8a4 --- /dev/null +++ b/packages/plugin-telegram-ui/src/components/Form.tsx @@ -0,0 +1,296 @@ +import Button from '@erxes/ui/src/components/Button'; +import FormControl from '@erxes/ui/src/components/form/Control'; +import Form from '@erxes/ui/src/components/form/Form'; +import FormGroup from '@erxes/ui/src/components/form/Group'; +import ControlLabel from '@erxes/ui/src/components/form/Label'; +import Info from '@erxes/ui/src/components/Info'; +import { Step, Steps } from '@erxes/ui/src/components/step'; +import { + ControlWrapper, + FlexItem, + Indicator, + LeftItem, + Preview, + StepWrapper +} from '@erxes/ui/src/components/step/styles'; +import { IButtonMutateProps, IFormProps } from '@erxes/ui/src/types'; +import { Alert, __ } from '@erxes/ui/src/utils'; +import Wrapper from '@erxes/ui/src/layout/components/Wrapper'; +import client from '@erxes/ui/src/apolloClient'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +import SelectBrand from '@erxes/ui-inbox/src/settings/integrations/containers/SelectBrand'; +import SelectChannels from '@erxes/ui-inbox/src/settings/integrations/containers/SelectChannels'; +import { + AccountBox, + AccountItem, + AccountTitle, + Content, + ImageWrapper, + MessengerPreview, + TextWrapper +} from '@erxes/ui-inbox/src/settings/integrations/styles'; +import Accounts from '../containers/Accounts'; +import gql from 'graphql-tag'; +import { queries } from '../graphql'; +import Spinner from '@erxes/ui/src/components/Spinner'; +import EmptyState from '@erxes/ui/src/components/EmptyState'; + +interface TelegramChat { + _id: string; + type: string; + title: string; +} + +type Props = { + renderButton: (props: IButtonMutateProps) => JSX.Element; +}; + +type State = { + channelIds: string[]; + accountId: string; + selectedTelegramChatId?: string; + loadingTelegramChats: boolean; + telegramChats: TelegramChat[]; +}; + +class Telegram extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + channelIds: [], + accountId: '', + selectedTelegramChatId: undefined, + loadingTelegramChats: false, + telegramChats: [] + }; + } + + generateDoc = (values: { name: string; brandId: string }) => { + const { channelIds, accountId, selectedTelegramChatId } = this.state; + + return { + ...values, + kind: 'telegram', + channelIds, + accountId, + data: { + telegramChatId: selectedTelegramChatId + } + }; + }; + + channelOnChange = (values: string[]) => { + this.setState({ channelIds: values } as Pick); + }; + + onSelectAccount = (accountId: string) => { + if (!accountId) { + this.setState({ accountId: '' }); + } + + this.setState({ loadingTelegramChats: true }); + + client + .query({ + query: gql(queries.telegramChats) + }) + .then(({ data, loading }) => { + if (!loading) { + this.setState({ + accountId, + telegramChats: data.telegramChats, + loadingTelegramChats: false + }); + } + }) + .catch(error => { + Alert.error(error.message); + this.setState({ loadingTelegramChats: false }); + }); + }; + + onSelectChat = (id: string) => { + this.setState({ selectedTelegramChatId: id }); + }; + + renderChats = () => { + const { telegramChats, loadingTelegramChats } = this.state; + + if (loadingTelegramChats) { + return ; + } + + if (telegramChats.length == 0) { + return ( + + ); + } + + return ( + + + + {__('Telegram Chats')} + {telegramChats.map(chat => ( + + {chat.title} + + + ))} + + + + ); + }; + + renderContent = (formProps: IFormProps) => { + const { renderButton } = this.props; + const { values, isSubmitted } = formProps; + + return ( + <> + + + + + + + {__('Add bot token')} +
+ Add a bot token to watch chats that they are an admin of. + See https://telegram.me/BotFather for information on + creating a bot token +
+
+ + +
+
+
+ + + {this.renderChats()} + + + + + + + Integration Name +

+ {__('Name this integration to differentiate from the rest')} +

+ +
+ + + + +
+
+
+
+ + + {__('You are creating')} + {__('Telegram')} {__('integration')} + + + + + + {renderButton({ + values: this.generateDoc(values), + isSubmitted + })} + + + + ); + }; + + renderForm = () => { + return ; + }; + + render() { + const title = __('Telegram'); + + const breadcrumb = [ + { title: __('Settings'), link: '/settings' }, + { title: __('Integrations'), link: '/settings/integrations' }, + { title } + ]; + + return ( + + + + {this.renderForm()} + + + + + +

+ {__('Connect your')} {title} +

+

+ {__( + 'Connect your Telegram to start receiving emails in your team inbox' + )} +

+ {title} +
+
+
+
+
+
+ ); + } +} + +export default Telegram; diff --git a/packages/plugin-telegram-ui/src/components/IntegrationSettings.tsx b/packages/plugin-telegram-ui/src/components/IntegrationSettings.tsx new file mode 100644 index 0000000000..dd76eb834a --- /dev/null +++ b/packages/plugin-telegram-ui/src/components/IntegrationSettings.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import CollapseContent from '@erxes/ui/src/components/CollapseContent'; + +class Settings extends React.Component { + render() { + const { renderItem } = this.props; + + return ( + + {renderItem('TELEGRAM_API_ID', '', '', '', 'App api_id')} + {renderItem('TELEGRAM_API_HASH', '', '', '', 'App api_hash')} + + ); + } +} + +export default Settings; diff --git a/packages/plugin-telegram-ui/src/configs.js b/packages/plugin-telegram-ui/src/configs.js new file mode 100644 index 0000000000..88e180845e --- /dev/null +++ b/packages/plugin-telegram-ui/src/configs.js @@ -0,0 +1,24 @@ +module.exports = { + name: 'telegram', + scope: 'telegram', + port: 3024, + exposes: { + './routes': './src/routes.tsx', + './inboxConversationDetail': './src/components/ConversationDetail.tsx' + }, + routes: { + url: 'http://localhost:3024/remoteEntry.js', + scope: 'telegram', + module: './routes' + }, + inboxConversationDetail: './inboxConversationDetail', + inboxIntegrations: [{ + name: 'Telegram', + description: + 'Connect telegram chats to inbox', + isAvailable: true, + kind: 'telegram', + logo: '/images/integrations/telegram.png', + createUrl: '/settings/integrations/createTelegram' + }] +}; diff --git a/packages/plugin-telegram-ui/src/constants.ts b/packages/plugin-telegram-ui/src/constants.ts new file mode 100644 index 0000000000..4905c7a207 --- /dev/null +++ b/packages/plugin-telegram-ui/src/constants.ts @@ -0,0 +1,11 @@ +export const INTEGRATIONS = [ + { + name: 'Telegram', + description: 'Connect telegram chats to inbox', + inMessenger: false, + isAvailable: true, + kind: 'telegram', + logo: '/images/integrations/telegram.png', + createUrl: '/settings/integrations/createTelegram' + } +]; diff --git a/packages/plugin-telegram-ui/src/containers/Accounts.tsx b/packages/plugin-telegram-ui/src/containers/Accounts.tsx new file mode 100644 index 0000000000..01f8e00cb7 --- /dev/null +++ b/packages/plugin-telegram-ui/src/containers/Accounts.tsx @@ -0,0 +1,106 @@ +import * as compose from 'lodash.flowright'; +import { Alert, getEnv, withProps } from '@erxes/ui/src/utils'; + +import Accounts from '../components/Accounts'; +import Info from '@erxes/ui/src/components/Info'; +import React from 'react'; +import Spinner from '@erxes/ui/src/components/Spinner'; +import { gql } from '@apollo/client'; +import { graphql } from '@apollo/client/react/hoc'; +import { queries, mutations } from '../graphql'; + +type Props = { + onSelectAccount: (accountId: string) => void; + accountId: string; +}; + +type FinalProps = { + accountsQuery: any; + removeAccount: any; + addAccount: any; +} & Props; +class AccountContainer extends React.Component { + popupWindow(url, title, win, w, h) { + const y = win.top.outerHeight / 2 + win.top.screenY - h / 2; + const x = win.top.outerWidth / 2 + win.top.screenX - w / 2; + + return win.open( + url, + title, + `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=${w}, height=${h}, top=${y}, left=${x}` + ); + } + + addAccount = (token: string) => { + const { addAccount } = this.props; + + addAccount({ variables: { token } }) + .then(res => { + const message = res.data.telegramAccountAdd; + Alert.success(message); + + this.props.accountsQuery.refetch(); + }) + .catch(e => { + Alert.error(e.message); + }); + }; + + removeAccount = (accountId: string) => { + const { removeAccount } = this.props; + + removeAccount({ variables: { _id: accountId } }) + .then(() => { + Alert.success('You successfully removed an account'); + + this.props.accountsQuery.refetch(); + }) + .catch(e => { + Alert.error(e.message); + }); + }; + + render() { + const { accountsQuery, onSelectAccount, accountId } = this.props; + + if (accountsQuery.loading) { + return ; + } + + if (accountsQuery.error) { + return {accountsQuery.error.message}; + } + + const accounts = accountsQuery.telegramAccounts || []; + + return ( + + ); + } +} + +export default withProps( + compose( + graphql(gql(mutations.removeAccount), { + name: 'removeAccount', + options: { + refetchQueries: ['accounts'] + } + }), + graphql(gql(queries.accounts), { + name: 'accountsQuery' + }), + graphql(gql(mutations.addAccount), { + name: 'addAccount', + options: { + refetchQueries: ['accounts'] + } + }) + )(AccountContainer) +); diff --git a/packages/plugin-telegram-ui/src/containers/Form.tsx b/packages/plugin-telegram-ui/src/containers/Form.tsx new file mode 100644 index 0000000000..30a850c6c5 --- /dev/null +++ b/packages/plugin-telegram-ui/src/containers/Form.tsx @@ -0,0 +1,62 @@ +import ButtonMutate from '@erxes/ui/src/components/ButtonMutate'; +import { gql } from '@apollo/client'; +import { IButtonMutateProps, IRouterProps } from '@erxes/ui/src/types'; +import Form from '../components/Form'; +import * as React from 'react'; +import { withRouter } from 'react-router-dom'; +import { + mutations, + queries +} from '@erxes/ui-inbox/src/settings/integrations/graphql'; + +type Props = {} & IRouterProps; + +class TelegramContainer extends React.Component { + renderButton = ({ values, isSubmitted }: IButtonMutateProps) => { + const { history } = this.props; + + const callback = () => { + history.push('/settings/integrations'); + }; + + return ( + + ); + }; + + render() { + const updatedProps = { + ...this.props, + renderButton: this.renderButton + }; + + return ; + } +} + +const getRefetchQueries = (kind: string) => { + return [ + { + query: gql(queries.integrations), + variables: { + kind + } + }, + { + query: gql(queries.integrationTotalCount), + variables: { + kind + } + } + ]; +}; + +export default withRouter(TelegramContainer); diff --git a/packages/plugin-telegram-ui/src/graphql/index.ts b/packages/plugin-telegram-ui/src/graphql/index.ts new file mode 100644 index 0000000000..e8c2c1f8fe --- /dev/null +++ b/packages/plugin-telegram-ui/src/graphql/index.ts @@ -0,0 +1,4 @@ +import queries from './queries'; +import mutations from './mutations'; + +export { queries, mutations }; diff --git a/packages/plugin-telegram-ui/src/graphql/mutations.ts b/packages/plugin-telegram-ui/src/graphql/mutations.ts new file mode 100644 index 0000000000..4a554ce7f3 --- /dev/null +++ b/packages/plugin-telegram-ui/src/graphql/mutations.ts @@ -0,0 +1,16 @@ +const removeAccount = ` + mutation telegramAccountRemove($_id: String!) { + telegramAccountRemove(_id: $_id) + } +`; + +const addAccount = ` + mutation telegramAccountAdd($token: String!) { + telegramAccountAdd(token: $token) + } +`; + +export default { + removeAccount, + addAccount +}; diff --git a/packages/plugin-telegram-ui/src/graphql/queries.ts b/packages/plugin-telegram-ui/src/graphql/queries.ts new file mode 100644 index 0000000000..7b7f6af2bb --- /dev/null +++ b/packages/plugin-telegram-ui/src/graphql/queries.ts @@ -0,0 +1,26 @@ +const detail = ` + query telegram($conversationId: String!) { + telegramConversationDetail(conversationId: $conversationId) { + _id + mailData + } + } +`; + +const accounts = ` + query telegramAccounts { + telegramAccounts + } +`; + +const telegramChats = ` + query telegramChats { + telegramChats + } +`; + +export default { + detail, + accounts, + telegramChats +}; diff --git a/packages/plugin-telegram-ui/src/index.js b/packages/plugin-telegram-ui/src/index.js new file mode 100644 index 0000000000..0ce8d3c645 --- /dev/null +++ b/packages/plugin-telegram-ui/src/index.js @@ -0,0 +1,3 @@ +import App from './App'; + +export default App; \ No newline at end of file diff --git a/packages/plugin-telegram-ui/src/routes.tsx b/packages/plugin-telegram-ui/src/routes.tsx new file mode 100644 index 0000000000..b6fe97aa42 --- /dev/null +++ b/packages/plugin-telegram-ui/src/routes.tsx @@ -0,0 +1,26 @@ +import asyncComponent from '@erxes/ui/src/components/AsyncComponent'; +import React from 'react'; +import { Route } from 'react-router-dom'; + +const CreateTelegram = asyncComponent(() => + import(/* webpackChunkName: "Settings CreateTelegram" */ './containers/Form') +); + +const createTelegram = () => { + return ; +}; + +const routes = () => { + return ( + + + + ); +}; + +export default routes; diff --git a/packages/plugin-telegram-ui/yarn.lock b/packages/plugin-telegram-ui/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/packages/plugin-telegram-ui/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/yarn.lock b/yarn.lock index e52fbc690c..369f35a203 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9558,7 +9558,7 @@ debug@^3.1.1, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.3: +debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -17166,7 +17166,7 @@ mquery@3.2.2: safe-buffer "5.1.2" sliced "1.0.1" -mri@^1.1.0: +mri@^1.1.0, mri@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== @@ -17463,6 +17463,13 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" +node-fetch@^2.6.8: + version "2.6.11" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" + integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== + dependencies: + whatwg-url "^5.0.0" + node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" @@ -18238,6 +18245,11 @@ p-retry@^4.5.0: "@types/retry" "^0.12.0" retry "^0.13.1" +p-timeout@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-4.1.0.tgz#788253c0452ab0ffecf18a62dff94ff1bd09ca0a" + integrity sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw== + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -20851,6 +20863,13 @@ safe-buffer@5.2.1, safe-buffer@^5.1.1, safe-buffer@^5.2.0, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-compare@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/safe-compare/-/safe-compare-1.1.4.tgz#5e0128538a82820e2e9250cd78e45da6786ba593" + integrity sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ== + dependencies: + buffer-alloc "^1.2.0" + safe-identifier@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb" @@ -20882,6 +20901,11 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sandwich-stream@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/sandwich-stream/-/sandwich-stream-2.0.2.tgz#6d1feb6cf7e9fe9fadb41513459a72c2e84000fa" + integrity sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ== + sane@^2.0.0: version "2.5.2" resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa" @@ -22334,6 +22358,20 @@ teeny-request@^6.0.0: stream-events "^1.0.5" uuid "^7.0.0" +telegraf@^4.12.2: + version "4.12.2" + resolved "https://registry.yarnpkg.com/telegraf/-/telegraf-4.12.2.tgz#1cf4f38c275e04416f1282f3581833994870f0bc" + integrity sha512-PgwqI4wD86cMqVfFtEM9JkGGnMHgvgLJbReZMmwW4z35QeOi4DvbdItONld4bPnYn3A1jcO0SRKs0BXmR+x+Ew== + dependencies: + abort-controller "^3.0.0" + debug "^4.3.4" + mri "^1.2.0" + node-fetch "^2.6.8" + p-timeout "^4.1.0" + safe-compare "^1.1.4" + sandwich-stream "^2.0.2" + typegram "^4.3.0" + telnyx@^1.7.2: version "1.23.0" resolved "https://registry.yarnpkg.com/telnyx/-/telnyx-1.23.0.tgz#0d949a11f7c819b0d5ce8ae8c36b80bd02e351c8" @@ -23072,6 +23110,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typegram@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/typegram/-/typegram-4.3.0.tgz#690ec1287f771608070e149c92de4fca42e54db0" + integrity sha512-pS4STyOZoJ++Mwa9GPMTNjOwEzMkxFfFt1By6IbMOJfheP0utMP/H1ga6J9R4DTjAYBr0UDn4eQg++LpWBvcAg== + typescript@3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6"