├── .gitignore
├── CHANGELOG.md
├── app
├── components
│ ├── index.jsx
│ ├── App
│ │ ├── Mixins.css
│ │ ├── App.css
│ │ └── App.jsx
│ ├── CircularProgress
│ │ ├── CircularProgress.jsx
│ │ └── CircularProgress.css
│ ├── Telegraph
│ │ ├── Telegraph.css
│ │ └── Telegraph.jsx
│ ├── Conversation
│ │ ├── Conversation.css
│ │ └── Conversation.jsx
│ └── AddBotDialog
│ │ └── AddBotDialog.jsx
├── index.html
└── main.jsx
├── README.md
├── api
├── tools.js
└── server.js
├── webpack.dev.config.js
├── webpack.prod.config.js
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/
3 | npm-debug.log
4 | api/store.json
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | #### v1.0.1
2 |
3 | - UI improvement
4 | - Better list clawing
5 |
6 | #### v1.0.0
7 |
8 | - Search animes by year
--------------------------------------------------------------------------------
/app/components/index.jsx:
--------------------------------------------------------------------------------
1 | export AddBotDialog from './AddBotDialog/AddBotDialog'
2 | export App from './App/App'
3 | export CircularProgress from './CircularProgress/CircularProgress'
4 | export Conversation from './Conversation/Conversation'
5 | export Telegraph from './Telegraph/Telegraph'
--------------------------------------------------------------------------------
/app/components/App/Mixins.css:
--------------------------------------------------------------------------------
1 | @define-mixin flexColumn {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
6 | @define-mixin flexAlignCenter {
7 | align-items: center;
8 | justify-content: center;
9 | }
10 |
11 | @define-mixin sameHeight $height {
12 | height: $height;
13 | line-height: $height;
14 | }
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | telegraph
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/components/CircularProgress/CircularProgress.jsx:
--------------------------------------------------------------------------------
1 | import $ from './CircularProgress.css';
2 | import React from 'react';
3 |
4 | export default class CircularProgress extends React.Component {
5 | render() {
6 | return (
7 | this.props.completed ? null :
8 |
9 |
12 |
13 | );
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/components/Telegraph/Telegraph.css:
--------------------------------------------------------------------------------
1 | @import '../App/Mixins';
2 |
3 | .content, .infoWrapper {
4 | display: flex;
5 | flex: 1;
6 | position: relative;
7 | }
8 |
9 | .chatsScrollWrapper {
10 | overflow-x: hidden;
11 | width: 100%;
12 | }
13 |
14 | .chatsContainer {
15 | border-right: 1px solid #eee;
16 | display: flex;
17 | min-width: 320px;
18 | max-width: 320px;
19 | overflow-x: hidden;
20 | position: relative;
21 | z-index: 2;
22 | }
23 |
24 | .infoWrapper {
25 | height: 100%;
26 | @mixin flexAlignCenter;
27 | }
28 |
29 | .fabAdd {
30 | bottom: 20px;
31 | position: absolute;
32 | right: 20px;
33 | z-index: 3;
34 | }
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | telegraph
6 |
7 |
8 | web ui for telegram bots
9 |
10 |
11 | #### Developing
12 |
13 | > Please ensure your machine has enough resources to perform compilation during installation
14 |
15 | ```bash
16 | $ npm install
17 | $ npm start
18 | $ open http://127.0.0.1:8080
19 | ```
20 |
21 | #### Deploying
22 |
23 | ```bash
24 | $ npm run deploy
25 | $ cd dist && ls
26 | ```
27 |
28 | #### Screenshots
29 |
30 | 
31 |
32 | #### License
33 |
34 | MIT
35 |
--------------------------------------------------------------------------------
/app/components/App/App.css:
--------------------------------------------------------------------------------
1 | @import 'https://cdn.materialdesignicons.com/1.6.50/css/materialdesignicons.min.css';
2 | @import 'https://fonts.googleapis.com/css?family=Roboto:300,500';
3 | @import 'normalize.css';
4 | @import './Mixins.css';
5 |
6 | html *[type] {
7 | -webkit-appearance: none;
8 | }
9 |
10 | [class^="mdi"] {
11 | font-family: 'Material Design Icons';
12 | }
13 |
14 | .flexWrapper {
15 | @mixin flexAlignCenter;
16 | display: flex;
17 | height: 100%;
18 | }
19 |
20 | .main {
21 | display: flex;
22 | flex-direction: column;
23 | font-weight: 300;
24 | height: 640px;
25 | margin: 0 auto;
26 | width: 960px;
27 |
28 | @media only screen and (max-width:960px) {
29 | & {
30 | height: 100%;
31 | width: 100%;
32 | }
33 | }
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/app/components/CircularProgress/CircularProgress.css:
--------------------------------------------------------------------------------
1 | @keyframes rotate {
2 | to { transform: rotate(360deg); }
3 | }
4 |
5 | @keyframes dash {
6 | 0% {
7 | stroke-dasharray: 1,150;
8 | stroke-dashoffset: 0;
9 | }
10 | 50% {
11 | stroke-dasharray: 90,150;
12 | stroke-dashoffset: -35;
13 | }
14 | to {
15 | stroke-dasharray: 90,150;
16 | stroke-dashoffset: -124;
17 | }
18 | }
19 |
20 | .wrapper {
21 | align-items: center;
22 | display: flex;
23 | height: 100%;
24 | justify-content: center;
25 | left: 0;
26 | position: absolute;
27 | top: 0;
28 | width: 100%;
29 | }
30 |
31 | .spinner {
32 | animation: rotate 2s linear infinite;
33 | z-index: 2;
34 | }
35 |
36 | .spinner > .path {
37 | animation: dash 1.5s ease-in-out infinite;
38 | stroke-dasharray: 1,150;
39 | stroke-dashoffset: 0;
40 | stroke-linecap: round;
41 | stroke-width: 4px;
42 | }
43 |
--------------------------------------------------------------------------------
/app/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { MuiThemeProvider, getMuiTheme } from 'material-ui/styles';
4 | import { App } from './components';
5 | import injectTapEventPlugin from 'react-tap-event-plugin';
6 | injectTapEventPlugin();
7 |
8 | const app = document.querySelector('#app');
9 | const muiTheme = getMuiTheme({
10 | palette: {
11 | primary1Color: '#28A5D4',
12 | accent1Color: '#4170B7',
13 | borderColor: '#B5B5B7',
14 | disabledColor: '#CECECF',
15 | pickerHeaderColor: '#E8EAF6',
16 | clockCircleColor: '#E8EAF6',
17 | },
18 | appBar: {
19 | color: '#FFFFFF',
20 | textColor: '#000000'
21 | }
22 | });
23 |
24 | ReactDOM.render(
25 |
26 |
27 | ,
28 | app
29 | );
30 |
31 | setTimeout(() => {
32 | app.setAttribute('style', 'height:100%');
33 | }, 250)
34 |
--------------------------------------------------------------------------------
/api/tools.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import mkdirp from 'mkdirp-promise'
4 |
5 | export const initStore = (file, content) => new Promise((resolve, reject) => {
6 | mkdirp(path.dirname(file)).then(() => {
7 | fs.stat(file, (statErr) => {
8 | if (statErr === null) resolve(file)
9 | else {
10 | if (!content) reject('Content parameter is required')
11 | else fs.writeFile(file, content, (writeErr) => {
12 | if (writeErr) reject(writeErr)
13 | else resolve(file)
14 | })
15 | }
16 | })
17 | })
18 | })
19 |
20 | export const readStore = (file) => new Promise((resolve, reject) => {
21 | fs.readFile(file, (err, data) => {
22 | if (err) reject(err)
23 | else resolve(JSON.parse(data))
24 | })
25 | })
26 |
27 | export const updateStore = (file, store) => new Promise((resolve, reject) => {
28 | fs.writeFile(file, JSON.stringify(store), (writeErr) => {
29 | if (writeErr) reject(writeErr)
30 | else resolve(file)
31 | })
32 | })
33 |
34 | export const craftBot = (botProp) => new Promise((resolve, reject) => {
35 |
36 | })
--------------------------------------------------------------------------------
/app/components/App/App.jsx:
--------------------------------------------------------------------------------
1 | import $ from './App.css';
2 | import React from 'react';
3 | import { Paper, RaisedButton } from 'material-ui';
4 | import { CircularProgress, Telegraph } from '../../components';
5 | import { VelocityComponent } from 'velocity-react';
6 |
7 | export default class App extends React.Component {
8 | state = {
9 | loaded: false
10 | }
11 |
12 | onLoad = (loaded) => {
13 | this.setState({ loaded })
14 | }
15 |
16 | render() {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
30 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const path = require('path')
3 |
4 | module.exports = {
5 | devServer: {
6 | historyApiFallback: true,
7 | hot: true,
8 | inline: true,
9 | progress: true,
10 | publicPath: '/',
11 | contentBase: './app',
12 | host: '0.0.0.0',
13 | port: 8081
14 | },
15 | entry: [
16 | 'webpack/hot/dev-server',
17 | 'webpack-dev-server/client?http://127.0.0.1:8081',
18 | 'babel-polyfill',
19 | path.resolve(__dirname, 'app/main.jsx'),
20 | ],
21 | output: {
22 | path: path.resolve(__dirname, './dist'),
23 | publicPath: '/dist',
24 | filename: 'app.js'
25 | },
26 | module: {
27 | loaders:[
28 | {
29 | test: /\.css$/,
30 | include: path.resolve(__dirname, './app'),
31 | loaders: [
32 | 'style-loader',
33 | 'css-loader?modules&localIdentName=[hash:base64:8]',
34 | 'postcss-loader'
35 | ]
36 | },
37 | {
38 | test: /\.js[x]?$/,
39 | include: path.resolve(__dirname, './app'),
40 | exclude: /node_modules/,
41 | loader: 'babel-loader'
42 | },
43 | {
44 | test: /\.json$/,
45 | loader: 'json-loader'
46 | }
47 | ]
48 | },
49 | resolve: {
50 | extensions: ['', '.js', '.jsx', '.css'],
51 | },
52 | plugins: [
53 | new webpack.HotModuleReplacementPlugin()
54 | ],
55 | postcss: function(bundler) {
56 | return [
57 | require('postcss-import')({ addDependencyTo: bundler }),
58 | require('postcss-mixins')(),
59 | require('postcss-nested')(),
60 | require('postcss-cssnext')({ browsers: '> 1%, last 3 versions' })
61 | ]
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/app/components/Conversation/Conversation.css:
--------------------------------------------------------------------------------
1 | @import '../App/Mixins';
2 |
3 | .root {
4 | position: relative;
5 | width: 100%;
6 | }
7 |
8 | .paperToolbar {
9 | align-items: center;
10 | box-shadow: 0 0 4px rgba(0, 0, 0, .15);
11 | box-sizing: border-box;
12 | display: flex;
13 | width: 100%;
14 | }
15 |
16 | .header, .messages, .footer {
17 | position: absolute;
18 | }
19 |
20 | .header {
21 | height: 64px;
22 | padding: 0 24px;
23 | top: 0;
24 |
25 | div {
26 | width: 100%;
27 |
28 | span, small {
29 | display: inline-block;
30 | }
31 | span {
32 | font-size: 14pt;
33 | max-width: 100%;
34 | overflow: hidden;
35 | text-overflow: ellipsis;
36 | white-space: nowrap;
37 | @mixin sameHeight 28px;
38 | }
39 | small {
40 | font-size: 10pt;
41 | @mixin sameHeight 20px;
42 | }
43 | }
44 | }
45 |
46 | .messages {
47 | bottom: 54px;
48 | left: 0;
49 | overflow-x: hidden;
50 | right: 0;
51 | top: 64px;
52 |
53 | .messageHolder {
54 | display: flex;
55 | padding: 12px 16px;
56 |
57 | .avatar {
58 | margin-right: 24px;
59 | }
60 | .messageContentContainer {
61 | flex: 1;
62 | word-break: break-word;
63 |
64 | .messageContent {
65 | background: #ddd;
66 | border-radius: 4px;
67 | display: inline-block;
68 | padding: 8px 12px;
69 | }
70 | }
71 | }
72 | }
73 |
74 | .footer {
75 | bottom: 0;
76 | height: 54px;
77 | padding: 0 8px;
78 |
79 | .button {
80 | height: 24px;
81 | line-height: 24px;
82 | text-align: center;
83 | width: 24px;
84 | }
85 | .inputField {
86 | padding: 0 4px;
87 | width: 100%;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/webpack.prod.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const path = require('path')
3 | const CopyWebpackPlugin = require('copy-webpack-plugin')
4 |
5 | module.exports = {
6 | devtool: 'hidden-source-map',
7 | entry: [
8 | 'babel-polyfill',
9 | path.resolve(__dirname, 'app/main.jsx'),
10 | ],
11 | output: {
12 | path: path.resolve(__dirname, './dist'),
13 | publicPath: '/dist',
14 | filename: 'app.js'
15 | },
16 | module: {
17 | loaders:[
18 | {
19 | test: /\.css$/,
20 | include: path.resolve(__dirname, './app'),
21 | loaders: [
22 | 'style-loader',
23 | 'css-loader?modules&localIdentName=[hash:base64:8]',
24 | 'postcss-loader'
25 | ]
26 | },
27 | {
28 | test: /\.js[x]?$/,
29 | include: path.resolve(__dirname, './app'),
30 | exclude: /node_modules/,
31 | loader: 'babel-loader'
32 | },
33 | {
34 | test: /\.json$/,
35 | loader: 'json-loader'
36 | }
37 | ]
38 | },
39 | resolve: {
40 | extensions: ['', '.js', '.jsx', '.css'],
41 | },
42 | plugins: [
43 | new webpack.optimize.DedupePlugin(),
44 | new webpack.optimize.UglifyJsPlugin({
45 | compress: {
46 | warnings: false
47 | }
48 | }),
49 | new webpack.optimize.AggressiveMergingPlugin(),
50 | new CopyWebpackPlugin([
51 | { from: './app/index.html', to: 'index.html' },
52 | { from: './app/main.css', to: 'main.css' }
53 | ]),
54 | ],
55 | postcss: function(bundler) {
56 | return [
57 | require('postcss-import')({ addDependencyTo: bundler }),
58 | require('postcss-mixins')(),
59 | require('postcss-nested')(),
60 | require('postcss-cssnext')({ browsers: '> 1%, last 3 versions' }),
61 | require('cssnano')({
62 | autoprefixer: false
63 | })
64 | ]
65 | }
66 | };
67 |
--------------------------------------------------------------------------------
/app/components/AddBotDialog/AddBotDialog.jsx:
--------------------------------------------------------------------------------
1 | import 'isomorphic-fetch';
2 | import React from 'react';
3 | import {
4 | Dialog,
5 | FlatButton,
6 | Snackbar,
7 | TextField,
8 | } from 'material-ui';
9 |
10 | export default class AddBotDialog extends React.Component {
11 | componentWillReceiveProps() {
12 | /* reset when component was shown / hidden */
13 | this.setState({
14 | disabled: true,
15 | error: null,
16 | message: null,
17 | token: '',
18 | })
19 | }
20 |
21 | handleChange = (e) => {
22 | const token = e.target.value
23 | this.setState({ token })
24 | if (null !== token.match(/^\d{9}\:[a-zA-Z0-9-_]{35}$/)) {
25 | this.setState({
26 | disabled: false,
27 | error: null,
28 | message: null,
29 | })
30 | } else {
31 | this.setState({
32 | disabled: true,
33 | error: 'Bot Token is not valid',
34 | message: null,
35 | })
36 | }
37 | }
38 |
39 | async handleSubmit() {
40 | this.setState({
41 | disabled: true,
42 | message: null,
43 | })
44 | let result = await fetch(`/parking`, {
45 | method: 'POST',
46 | headers: {
47 | 'accept': 'application/json',
48 | 'content-type': 'application/json',
49 | },
50 | body: JSON.stringify({
51 | token: this.state.token,
52 | }),
53 | })
54 | result = await result.json()
55 | if (result) {
56 | this.props.onClose()
57 | } else {
58 | this.setState({
59 | disabled: false,
60 | message: 'An error occurred'
61 | })
62 | }
63 | }
64 |
65 | render() {
66 | return this.props.open ? (
67 |
68 | ,
73 | ,
77 | ]}
78 | contentStyle={{
79 | width: '480px',
80 | }}
81 | modal={false}
82 | open={true}>
83 | New Bot
84 |
85 |
91 |
92 |
100 |
101 | ) : null;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "telegraph",
3 | "version": "1.0.0",
4 | "description": "telegraph - web ui for telegram bots",
5 | "homepage": "https://git.io/telegraph",
6 | "author": "nodegin",
7 | "license": "MIT",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/nodegin/telegraph.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/nodegin/telegraph/issues"
14 | },
15 | "main": "app/main.jsx",
16 | "scripts": {
17 | "lint": "eslint '**/*.@(js|jsx)'",
18 | "test": "echo \"Error: no test specified\" && exit 1",
19 | "start:api": "NODE_ENV=development nodemon -e js --watch api api/server.js --exec babel-node",
20 | "start:app": "webpack-dev-server --config webpack.dev.config.js --devtool eval --progress --hot --colors --content-base app",
21 | "start": "concurrently --kill-others 'npm run start:api' 'npm run start:app'",
22 | "bundle": "NODE_ENV=production webpack -p --config webpack.prod.config.js",
23 | "deploy": "npm run bundle && NODE_ENV=production babel-node api/server.js",
24 | "validate": "npm ls"
25 | },
26 | "dependencies": {
27 | "express": "4.x",
28 | "body-parser": "1.x",
29 | "proxy-middleware": "0.x",
30 | "socket.io": "1.x",
31 | "react": "15.x",
32 | "react-dom": "15.x",
33 | "react-tap-event-plugin": "1.x",
34 | "classnames": "2.x",
35 | "velocity-react": "1.x",
36 | "normalize.css": "4.x",
37 | "material-ui": "0.x",
38 | "mkdirp-promise": "2.x",
39 | "node-telegram-bot-api": "0.x",
40 | "monkey-patches-node-telegram-bot-api": "0.x"
41 | },
42 | "devDependencies": {
43 | "nodemon": "1.x",
44 | "babel-runtime": "6.x",
45 | "babel-polyfill": "6.x",
46 | "babel-cli": "6.x",
47 | "babel-core": "6.x",
48 | "babel-eslint": "6.x",
49 | "babel-loader": "6.x",
50 | "babel-preset-stage-0-promises": "1.x",
51 | "babel-preset-es2015": "6.x",
52 | "babel-preset-react": "6.x",
53 | "copy-webpack-plugin": "3.x",
54 | "css-loader": "0.x",
55 | "eslint": "2.x",
56 | "eslint-config-airbnb": "9.x",
57 | "eslint-plugin-jsx-a11y": "1.x",
58 | "eslint-plugin-react": "5.x",
59 | "eslint-plugin-import": "1.x",
60 | "postcss": "5.x",
61 | "postcss-loader": "0.x",
62 | "postcss-import": "8.x",
63 | "postcss-cssnext": "2.x",
64 | "postcss-mixins": "4.x",
65 | "postcss-nested": "1.x",
66 | "cssnano": "3.x",
67 | "style-loader": "0.x",
68 | "css-loader": "0.x",
69 | "json-loader": "0.x",
70 | "webpack": "1.x",
71 | "webpack-dev-server": "1.x",
72 | "concurrently": "2.x"
73 | },
74 | "babel": {
75 | "presets": ["react", "es2015", "stage-0-promises"],
76 | "env": {
77 | "build": {
78 | "optional": ["optimisation", "minification"]
79 | }
80 | }
81 | },
82 | "eslintConfig": {
83 | "parser": "babel-eslint",
84 | "extends": "airbnb",
85 | "env": {
86 | "browser": true
87 | },
88 | "rules": {
89 | "semi": [2, "never"],
90 | "no-confusing-arrow": 0,
91 | "react/jsx-quotes": 0,
92 | "jsx-quotes": [2, "prefer-double"]
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/app/components/Conversation/Conversation.jsx:
--------------------------------------------------------------------------------
1 | import $ from './Conversation.css'
2 | import classNames from 'classnames'
3 | import React from 'react'
4 | import ReactDOM from 'react-dom'
5 | import { Avatar, IconButton, TextField } from 'material-ui'
6 |
7 | class Messages extends React.Component {
8 | componentDidUpdate() {
9 | /* Scroll to bottom automatically */
10 | const node = ReactDOM.findDOMNode(this)
11 | node.scrollTop = node.scrollHeight
12 | }
13 | render() {
14 | return (
15 |
16 | {this.props.children}
17 |
18 | )
19 | }
20 | }
21 |
22 | export default class Conversation extends React.Component {
23 |
24 | state = {
25 | chatContent: '',
26 | botMessages: {},
27 | }
28 |
29 | botMessages = {}
30 |
31 | handleEnter = (e) => {
32 | if (e.keyCode === 13) {
33 | this.sendMessage()
34 | }
35 | }
36 |
37 | handleChange = e => this.setState({ chatContent: e.target.value })
38 |
39 | sendMessage = () => {
40 | this.props.socket.emit('sendText', {
41 | bot: this.props.bot.id,
42 | chat: this.props.chat.id,
43 | text: this.state.chatContent,
44 | })
45 | if (!this.botMessages[this.props.chat.id]) {
46 | this.botMessages[this.props.chat.id] = []
47 | }
48 | this.botMessages[this.props.chat.id].push({
49 | chat: {
50 | id: this.props.chat.id,
51 | title: this.props.chat.title,
52 | type: this.props.chat.type,
53 | },
54 | date: +new Date,
55 | from: {
56 | first_name: this.props.bot.name,
57 | id: this.props.bot.id,
58 | username: this.props.bot.username,
59 | },
60 | message_id: -1,
61 | text: this.state.chatContent,
62 | })
63 | this.setState({ chatContent: '' })
64 | }
65 |
66 | render() {
67 | return (
68 |
69 |
76 |
77 | {
78 | this.props.messages.filter((msg) => {
79 | return String(msg.chat.id) === this.props.chat.id
80 | }).concat(
81 | this.botMessages[this.props.chat.id] ? this.botMessages[this.props.chat.id] : []
82 | ).sort((a, b) => {
83 | if (a.date < b.date)
84 | return -1
85 | if (a.date > b.date)
86 | return 1
87 | return 0
88 | }).map((msg) => {
89 | let content = null
90 | if (msg.text) {
91 | content = {msg.text}
92 | }
93 | return (
94 |
102 | )
103 | })
104 | }
105 |
106 |
120 |
121 | );
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/app/components/Telegraph/Telegraph.jsx:
--------------------------------------------------------------------------------
1 | import $ from './Telegraph.css'
2 | import React from 'react'
3 | import {
4 | Avatar,
5 | FloatingActionButton,
6 | FontIcon,
7 | List,
8 | ListItem,
9 | Subheader,
10 | } from 'material-ui'
11 | import { AddBotDialog, Conversation } from '../../components'
12 | import io from 'socket.io-client'
13 |
14 | export default class Telegraph extends React.Component {
15 | state = {
16 | addBotFormOpen: false,
17 | newBotToken: '',
18 | activeChat: null,
19 | bots: {},
20 | chats: {},
21 | messages: {},
22 | }
23 |
24 | socket = io(undefined, { path: '/_' })
25 |
26 | async componentDidMount() {
27 | this.props.onLoad(true)
28 | this.socket.on('syncStore', (store) => this.setState(store))
29 | this.socket.on('message', (data) => {
30 | /* Force use local time */
31 | data.msg.date = +new Date
32 | let messages = this.state.messages[data.bot]
33 | if (!messages) {
34 | messages = []
35 | }
36 | messages.push(data.msg)
37 | /* GC old messages > 1000 */
38 | if (messages.length > 1) {
39 | messages = messages.slice(Math.max(messages.length - 1000, 0))
40 | }
41 | this.setState({
42 | messages: {
43 | ...this.state.messages,
44 | [data.bot]: messages
45 | }
46 | })
47 | })
48 | }
49 |
50 | toggleAddBotDialog = () => {
51 | this.setState({
52 | addBotFormOpen: !this.state.addBotFormOpen
53 | })
54 | }
55 |
56 | openChat(botId, chatId) {
57 | let activeChat = [botId, chatId]
58 | if (this.state.activeChat !== null) {
59 | const [activeBotId, activeChatId] = this.state.activeChat
60 | /* close the conversation on toggle */
61 | if (activeBotId === botId && activeChatId === chatId) {
62 | activeChat = null
63 | }
64 | }
65 | this.setState({ activeChat })
66 | }
67 |
68 | render() {
69 | const listItems = []
70 | for (let botId in this.state.bots) {
71 | listItems.push({this.state.bots[botId].name})
72 | if (this.state.bots[botId].chats.length < 1) {
73 | listItems.push(No group was found for this bot.)
74 | }
75 | for (let chatId of this.state.bots[botId].chats) {
76 | listItems.push(
77 | }
79 | primaryText={this.state.chats[chatId].title}
80 | onTouchTap={this.openChat.bind(this, botId, chatId)} />
81 | )
82 | }
83 | }
84 | return (
85 |
86 |
87 |
88 | {
89 | listItems.length < 1 ? (
90 |
91 | No bot was found
92 |
93 | ) : (
94 |
{listItems}
95 | )
96 | }
97 |
98 |
99 |
102 |
103 |
104 |
105 |
106 | {
107 | this.state.activeChat !== null ? (
108 |
117 | ) : (
118 |
119 | No group selected
120 |
121 | )
122 | }
123 |
126 |
127 | );
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/api/server.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import express from 'express'
4 | import http from 'http'
5 | import bodyParser from 'body-parser'
6 | import proxy from 'proxy-middleware'
7 | import socket from 'socket.io'
8 | import {
9 | initStore,
10 | readStore,
11 | updateStore,
12 | craftBot
13 | } from './tools'
14 | import telegramBot from 'node-telegram-bot-api'
15 |
16 | require('monkey-patches-node-telegram-bot-api')(telegramBot, {
17 | stopPolling: true,
18 | sendVenue: true,
19 | answerCallbackQuery: true,
20 | editMessageText: true,
21 | })
22 |
23 | const app = express()
24 | const server = http.createServer(app)
25 | const io = socket(server, { path: '/_' })
26 |
27 | server.listen(8080, () => {
28 | console.log('api server running on 8080')
29 | })
30 |
31 | /* Setup middlewares */
32 | app.use(bodyParser.json())
33 |
34 | /* Initialize application */
35 | async function init() {
36 |
37 | const storeFile = path.resolve(__dirname, './store.json')
38 | /* Initialize the store if not exists */
39 | await initStore(storeFile, JSON.stringify({
40 | bots: {},
41 | chats: {},
42 | }))
43 | let Store = await readStore(storeFile)
44 | let Chain = Promise.resolve()
45 | let Bots = {}
46 |
47 | /* Routes */
48 |
49 | io.on('connection', (socket) => {
50 | socket.emit('syncStore', Store)
51 | socket.on('sendText', (data) => {
52 | Bots[data.bot].sendMessage(data.chat, data.text)
53 | })
54 | })
55 |
56 | app.post('/parking', async (req, res) => {
57 | if (!req.body.token) {
58 | res.send(false)
59 | return
60 | }
61 | const id = req.body.token.split(':')[0]
62 | let chats = []
63 | if (Store.bots[id]) {
64 | chats = Store.bots[id].chats
65 | }
66 | let bot = new telegramBot(req.body.token, {
67 | polling: {
68 | interval: 50,
69 | }
70 | })
71 | /* Timeout the getMe() action after 2.5 seconds */
72 | const timeout = setTimeout(() => {
73 | bot.stopPolling()
74 | res.send(false)
75 | }, 2500)
76 | const info = await bot.getMe()
77 | if (info) {
78 | /* Response received, unset the timeout */
79 | clearTimeout(timeout)
80 | bot.stopPolling()
81 | bot = null
82 | Store.bots[id] = {
83 | id,
84 | name: info.first_name,
85 | username: info.username,
86 | token: req.body.token,
87 | chats,
88 | }
89 | Chain = Chain.then(() => updateStore(storeFile, Store))
90 | await Chain
91 | io.emit('syncStore', Store)
92 | res.send(true)
93 | }
94 | })
95 |
96 | /* Must be under routes */
97 | if (process.env.NODE_ENV !== 'production') {
98 | app.use('/', proxy('http://127.0.0.1:8081/'))
99 |
100 | app.get('/', (req, res) => {
101 | res.sendFile(path.resolve(__dirname, '../app/index.html'))
102 | })
103 | } else {
104 | app.use(express.static(path.resolve(__dirname, '../dist')))
105 | }
106 |
107 | for (let botId in Store.bots) {
108 | const props = Store.bots[botId]
109 | Bots[botId] = new telegramBot(props.token, {
110 | polling: {
111 | interval: 99,
112 | timeout: 0,
113 | }
114 | })
115 | Bots[botId].on('new_chat_participant', async (msg) => {
116 | const chatId = String(msg.chat.id)
117 | const memId = String(msg.new_chat_member.id)
118 | /* If Bot was added to group */
119 | if (memId === botId) {
120 | /* Check if Bot already added to group */
121 | if (Store.bots[botId].chats.indexOf(chatId) < 0) {
122 | Store.bots[botId].chats.push(chatId)
123 | }
124 | Store.chats[chatId] = {
125 | id: chatId,
126 | title: msg.chat.title,
127 | type: msg.chat.type,
128 | }
129 | /* Update the store */
130 | Chain = Chain.then(() => updateStore(storeFile, Store))
131 | await Chain
132 | io.emit('syncStore', Store)
133 | }
134 | })
135 | Bots[botId].on('left_chat_participant', async (msg) => {
136 | const chatId = String(msg.chat.id)
137 | const memId = String(msg.left_chat_member.id)
138 | /* Save remove from store */
139 | if (memId === botId) {
140 | Store.bots[botId].chats = Store.bots[botId].chats.filter((id) => id !== chatId)
141 | /* Check whatever if the group id is not belonging to any Bot */
142 | let useless = true
143 | for (let _id in Store.bots) {
144 | if (useless) {
145 | useless = Store.bots[_id].chats.indexOf(chatId) < 0
146 | }
147 | }
148 | /* Perform Garbage Collection for the useless group */
149 | if (useless) {
150 | Store.chats = Object.keys(Store.chats)
151 | .filter(id => id !== chatId)
152 | .reduce((result, current) => {
153 | result[current] = Store.chats[current]
154 | return result
155 | }, {})
156 | }
157 | /* Update the store */
158 | Chain = Chain.then(() => updateStore(storeFile, Store))
159 | await Chain
160 | io.emit('syncStore', Store)
161 | }
162 | })
163 | Bots[botId].on('message', async (msg) => {
164 | /* Ignore since catched above */
165 | if (msg.new_chat_participant || msg.left_chat_participant) {
166 | return
167 | }
168 | /* Fill missing group */
169 | const chatId = String(msg.chat.id)
170 | if (Store.bots[botId].chats.indexOf(chatId) < 0) {
171 | if (Store.bots[botId].chats.indexOf(chatId) < 0) {
172 | Store.bots[botId].chats.push(chatId)
173 | }
174 | Store.chats[chatId] = {
175 | id: chatId,
176 | title: msg.chat.title,
177 | type: msg.chat.type,
178 | }
179 | Chain = Chain.then(() => updateStore(storeFile, Store))
180 | await Chain
181 | io.emit('syncStore', Store)
182 | }
183 | io.emit('message', { bot: botId, msg })
184 | })
185 | }
186 | }
187 |
188 | init()
189 |
--------------------------------------------------------------------------------