├── .gitignore
├── LICENSE
├── README.md
├── babel.config.js
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
├── index.html
├── robots.txt
├── sitemap-0.xml
├── sitemap.xml
└── static
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── banner.png
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── logo.svg
│ ├── mstile-150x150.png
│ ├── safari-pinned-tab.svg
│ ├── site.webmanifest
│ └── wallet
│ ├── anchor.png
│ ├── metamask.png
│ ├── scatter.png
│ ├── telos.png
│ └── wombat.png
├── src
├── App.vue
├── assets
│ └── telegram.js
├── bots
│ ├── discord.js
│ ├── staking1.js
│ ├── test.js
│ └── twitter.js
├── components
│ ├── AboutUsCard.vue
│ ├── AppAlertBar.vue
│ ├── AppBar.vue
│ ├── AppNav.vue
│ ├── AppNavRight.vue
│ ├── ApproveTransfersCard.vue
│ ├── AssetCard.vue
│ ├── BarChart.js
│ ├── BrowsePageLayout.vue
│ ├── CommunityCard.vue
│ ├── ConnectWalletBtn.vue
│ ├── ConnectWalletCard.vue
│ ├── Countdown.vue
│ ├── FullScreenDialog.vue
│ ├── ImageViewer.vue
│ ├── LandingInfo.vue
│ ├── LineChart.js
│ ├── LoginCard.vue
│ ├── MarkdownEditor
│ │ ├── ImageUploadCard.vue
│ │ ├── InsertLinkCard.vue
│ │ ├── index.vue
│ │ └── nodes
│ │ │ ├── Hashtag.js
│ │ │ ├── Link2.js
│ │ │ └── Mention.js
│ ├── NotificationsButton.vue
│ ├── PostBrowser.vue
│ ├── PostCard.vue
│ ├── PostCardActions.vue
│ ├── PostDisplaySelect.vue
│ ├── PostReplyCard.vue
│ ├── PostScrollCard.vue
│ ├── PostScroller.vue
│ ├── PostSortSelect.vue
│ ├── PostSubmitter
│ │ ├── PayWall.vue
│ │ └── index.vue
│ ├── PostThreadLink.vue
│ ├── PostTips.vue
│ ├── PublicKeyIcon.vue
│ ├── RecentPostsCard.vue
│ ├── SendTipCard.vue
│ ├── SignupCard.vue
│ ├── SocialMediasCard.vue
│ ├── TagIcon.vue
│ ├── TagLink.vue
│ ├── ThreadBrowser.vue
│ ├── TokenIcon.vue
│ ├── TransactionBrowser.vue
│ ├── TransactionCard.vue
│ ├── TransactionLink.vue
│ ├── TransactionScroller.vue
│ ├── TransactionSubmitText.vue
│ ├── TrendingCard.vue
│ ├── UserAssetSelect.vue
│ ├── UserProfileCard.vue
│ └── UserProfileLink.vue
├── main.js
├── manager
│ └── index.js
├── mixins
│ ├── safari.js
│ ├── shortTime.js
│ ├── submitPost.js
│ ├── threadLink.js
│ └── userActions.js
├── novusphere-js
│ ├── discussions
│ │ ├── AccountSearchQuery.js
│ │ ├── DirectMsgSearchQuery.js
│ │ ├── Post.js
│ │ ├── PostSearchQuery.js
│ │ ├── api.js
│ │ ├── gateway.js
│ │ └── index.js
│ ├── index.js
│ ├── uid
│ │ ├── TransactionSearchQuery.js
│ │ ├── bch.js
│ │ ├── bufferwriter.js
│ │ ├── eos.js
│ │ ├── eth.js
│ │ ├── index.js
│ │ ├── newdex.js
│ │ ├── workers
│ │ │ └── ecc.js
│ │ └── xnation.js
│ └── utility
│ │ ├── index.js
│ │ └── lock.js
├── pages
│ ├── BlankPage.vue
│ ├── BrowseFeedPage.vue
│ ├── BrowseSearchPage.vue
│ ├── BrowseTagPostsPage.vue
│ ├── BrowseThreadPage.vue
│ ├── BrowseTrendingPostsPage.vue
│ ├── ClosePage.vue
│ ├── HomePage.vue
│ ├── LandingPage.vue
│ ├── LogOutPage.vue
│ ├── MainLayoutPage.vue
│ ├── MissingPage.vue
│ ├── RecoverPage.vue
│ ├── SubmitPostPage.vue
│ ├── UserProfilePage.vue
│ ├── discover
│ │ ├── DiscoverCommunityPage.vue
│ │ ├── DiscoverPage.vue
│ │ └── DiscoverUserPage.vue
│ ├── notifications
│ │ ├── BrowsePostNotificationsPage.vue
│ │ ├── BrowseTrxNotificationsPage.vue
│ │ └── NotificationsPage.vue
│ ├── settings
│ │ ├── BrowseModeratedPostsPage.vue
│ │ ├── BrowseWatchedThreadsPage.vue
│ │ ├── ContentSettingsPage.vue
│ │ ├── KeysSettingsPage.vue
│ │ └── SettingsPage.vue
│ ├── tests
│ │ ├── AirdropPage.vue
│ │ ├── AnalyticsPage.vue
│ │ ├── MessengerPage.vue
│ │ ├── TestEditorPage.vue
│ │ ├── TestsPage.vue
│ │ └── posts
│ │ │ ├── TestBrowsePostsPage.vue
│ │ │ └── posts.js
│ └── wallet
│ │ ├── EOSAccountCreatePage.vue
│ │ ├── StakingPage.vue
│ │ ├── WalletAssetsPage.vue
│ │ ├── WalletDepositPage.vue
│ │ ├── WalletPage.vue
│ │ ├── WalletSwapPage.vue
│ │ └── WalletWithdrawPage.vue
├── plugins
│ ├── router.js
│ ├── vuetify.js
│ └── vuex.js
├── server
│ ├── babel.config.json
│ ├── controllers
│ │ ├── AccountController.js
│ │ ├── BlockchainController.js
│ │ ├── DataController.js
│ │ ├── ModerationController.js
│ │ ├── SearchController.js
│ │ └── UploadController.js
│ ├── events
│ │ └── index.js
│ ├── gateways
│ │ └── index.js
│ ├── helpers
│ │ └── index.js
│ ├── index.js
│ ├── mongo
│ │ ├── config.js
│ │ └── index.js
│ ├── routes.js
│ ├── services
│ │ ├── EOSContractService.js
│ │ ├── analytics.js
│ │ ├── discussions.js
│ │ └── index.js
│ ├── site.js
│ └── sitemap.js
├── utility.js
└── watcher
│ ├── dfuse.js
│ ├── greymass.js
│ └── index.js
└── vue.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | /config/*
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Novusphere
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true
4 | }
5 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "discussions-vue",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint",
9 | "manager-alpha": "babel-node --max-old-space-size=4096 --config-file ./src/server/babel.config.json ./src/manager/ --server=server-alpha",
10 | "manager-beta": "babel-node --max-old-space-size=4096 --config-file ./src/server/babel.config.json ./src/manager/ --server=server-beta",
11 | "server-beta": "babel-node --max-old-space-size=4096 --config-file ./src/server/babel.config.json ./src/server/ --config=server-beta",
12 | "server-alpha": "babel-node --max-old-space-size=4096 --config-file ./src/server/babel.config.json ./src/server/ --config=server-alpha",
13 | "server-mode1": "babel-node --max-old-space-size=4096 --config-file ./src/server/babel.config.json ./src/server/ --config=server-mode1",
14 | "sitemap": "babel-node --config-file ./src/server/babel.config.json ./src/server/sitemap.js --config=server-alpha",
15 | "staking1-bot": "babel-node --config-file ./src/server/babel.config.json ./src/bots/staking1.js",
16 | "discord-bot": "babel-node --config-file ./src/server/babel.config.json ./src/bots/discord.js",
17 | "twitter-bot": "babel-node --config-file ./src/server/babel.config.json ./src/bots/twitter.js",
18 | "test": "babel-node --config-file ./src/server/babel.config.json ./src/bots/test.js",
19 | "watcher": "babel-node --max-old-space-size=4096 --config-file ./src/server/babel.config.json ./src/watcher/"
20 | },
21 | "dependencies": {
22 | "@decorators/di": "^1.0.2",
23 | "@decorators/express": "^2.4.0",
24 | "@decorators/socket": "^3.2.0",
25 | "@dfuse/client": "^0.3.17",
26 | "@hapi/joi": "^17.1.1",
27 | "aes-js": "^3.1.2",
28 | "axios": "^0.21.1",
29 | "big-integer": "^1.6.48",
30 | "bip32": "^2.0.6",
31 | "bip39": "^3.0.3",
32 | "bs58": "^4.0.1",
33 | "chart.js": "^2.9.4",
34 | "core-js": "^3.8.0",
35 | "cors": "^2.8.5",
36 | "date-fns": "^2.16.1",
37 | "discord.js": "^12.5.1",
38 | "eos-transit": "^4.0.7",
39 | "eos-transit-anchorlink-provider": "^4.0.7",
40 | "eos-transit-scatter-provider": "^4.0.7",
41 | "eosjs": "^20.0.3",
42 | "eosjs-ecc": "^4.0.7",
43 | "express": "^4.17.1",
44 | "express-session": "^1.17.1",
45 | "identicon.js": "^2.3.3",
46 | "jsdom": "^16.4.0",
47 | "lodash": "^4.17.20",
48 | "mongodb": "^3.6.3",
49 | "multer": "^1.4.2",
50 | "node-fetch": "^2.6.1",
51 | "passport": "^0.4.1",
52 | "passport-reddit": "^0.2.4",
53 | "passport-twitter": "^1.0.4",
54 | "rendertron-middleware": "^0.1.5",
55 | "sanitize-html": "^1.27.5",
56 | "serialize-javascript": "^3.1.0",
57 | "showdown": "^1.9.1",
58 | "socket.io": "^2.3.0",
59 | "socket.io-client": "^2.3.1",
60 | "threads": "^1.6.3",
61 | "tiptap": "^1.30.0",
62 | "tiptap-commands": "^1.15.0",
63 | "tiptap-extensions": "^1.33.2",
64 | "turndown": "^6.0.0",
65 | "twitter": "^1.7.1",
66 | "uuidv4": "^6.2.5",
67 | "vue": "^2.6.12",
68 | "vue-chartjs": "^3.5.1",
69 | "vue-clipboard2": "^0.3.1",
70 | "vue-gtag": "^1.10.0",
71 | "vue-infinite-loading": "^2.4.5",
72 | "vue-router": "^3.4.9",
73 | "vue-router-sitemap": "0.0.4",
74 | "vuetify": "^2.3.19",
75 | "vuex": "^3.6.0",
76 | "ws": "^7.4.0",
77 | "yargs": "^15.4.1"
78 | },
79 | "devDependencies": {
80 | "@babel/cli": "^7.12.8",
81 | "@babel/core": "^7.12.9",
82 | "@babel/node": "^7.12.6",
83 | "@babel/plugin-proposal-decorators": "^7.12.1",
84 | "@babel/plugin-transform-modules-commonjs": "^7.12.1",
85 | "@babel/preset-env": "^7.12.7",
86 | "@vue/cli-plugin-babel": "^4.4.6",
87 | "@vue/cli-plugin-eslint": "^4.4.6",
88 | "@vue/cli-service": "^4.5.9",
89 | "babel-eslint": "^10.1.0",
90 | "babel-plugin-module-resolver": "^4.0.0",
91 | "babel-plugin-root-import": "^6.6.0",
92 | "eslint": "^6.7.2",
93 | "eslint-plugin-vue": "^6.2.2",
94 | "node-sass": "^4.14.1",
95 | "sass": "^1.29.0",
96 | "sass-loader": "^8.0.2",
97 | "threads-plugin": "^1.3.3",
98 | "vue-cli-plugin-vuetify": "^2.0.7",
99 | "vue-template-compiler": "^2.6.12",
100 | "vuetify-loader": "^1.6.0"
101 | },
102 | "eslintConfig": {
103 | "root": true,
104 | "env": {
105 | "node": true
106 | },
107 | "extends": [
108 | "plugin:vue/essential",
109 | "eslint:recommended"
110 | ],
111 | "parserOptions": {
112 | "parser": "babel-eslint"
113 | },
114 | "rules": {}
115 | },
116 | "browserslist": [
117 | "> 1%",
118 | "last 2 versions",
119 | "not dead"
120 | ]
121 | }
122 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Discussions
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | We're sorry but Discussions doesn't work properly without JavaScript enabled.
51 | Please enable it to continue.
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/robots.txt
--------------------------------------------------------------------------------
/public/sitemap-0.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://discussions.app/home
4 | https://discussions.app/404
5 | https://discussions.app/submit
6 | https://discussions.app/logout
7 | https://discussions.app/feed
8 | https://discussions.app/search
9 | https://discussions.app/tag/all
10 | https://discussions.app/notifications/posts
11 | https://discussions.app/notifications/trx
12 | https://discussions.app/discover/community
13 | https://discussions.app/discover/user
14 | https://discussions.app/wallet/assets
15 | https://discussions.app/wallet/withdraw
16 | https://discussions.app/wallet/deposit
17 | https://discussions.app/wallet/eos-account
18 | https://discussions.app/settings/content
19 | https://discussions.app/settings/watched
20 | https://discussions.app/settings/keys
21 | https://discussions.app/tag/test
22 | https://discussions.app/tag/voice
23 | https://discussions.app/tag/boid
24 | https://discussions.app/tag/mpt
25 | https://discussions.app/tag/puml
26 | https://discussions.app/tag/bbt
27 | https://discussions.app/tag/atmos
28 | https://discussions.app/tag/banano
29 | https://discussions.app/tag/eos
30 | https://discussions.app/tag/help
31 | https://discussions.app/tag/tlos
32 | https://discussions.app/tag/hive
33 | https://discussions.app/tag/steemit
34 | https://discussions.app/tag/steem
35 | https://discussions.app/tag/twitter
36 | https://discussions.app/tag/reddit
37 | https://discussions.app/tag/telegram
38 | https://discussions.app/tag/pixeos
39 | https://discussions.app/tag/govrn
40 | https://discussions.app/tag/binance
41 | https://discussions.app/tag/coronavirus
42 | https://discussions.app/tag/newdex
43 | https://discussions.app/tag/nco
44 | https://discussions.app/tag/covidfree_earth
45 | https://discussions.app/tag/bos
46 | https://discussions.app/tag/krown
47 | https://discussions.app/tag/dash
48 | https://discussions.app/tag/vigor
49 | https://discussions.app/tag/wax
50 | https://discussions.app/tag/hongkong
51 | https://discussions.app/tag/defi
52 | https://discussions.app/tag/eu
53 | https://discussions.app/tag/china
54 | https://discussions.app/tag/bitcoin
55 | https://discussions.app/tag/eth
56 | https://discussions.app/tag/ethereum
57 | https://discussions.app/tag/coinbase
58 | https://discussions.app/tag/telos
59 | https://discussions.app/tag/sec
60 | https://discussions.app/tag/instagram
61 | https://discussions.app/tag/facebook
62 | https://discussions.app/tag/fio
63 | https://discussions.app/tag/anchor
64 | https://discussions.app/tag/tiktok
65 | https://discussions.app/tag/youtube
66 | https://discussions.app/tag/dapp
67 | https://discussions.app/tag/ultra
68 |
--------------------------------------------------------------------------------
/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | https://discussions.app/sitemap-0.xml
5 |
6 |
--------------------------------------------------------------------------------
/public/static/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/static/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/static/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/static/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/banner.png
--------------------------------------------------------------------------------
/public/static/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #00aba9
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/favicon-16x16.png
--------------------------------------------------------------------------------
/public/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/favicon-32x32.png
--------------------------------------------------------------------------------
/public/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/favicon.ico
--------------------------------------------------------------------------------
/public/static/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/public/static/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/mstile-150x150.png
--------------------------------------------------------------------------------
/public/static/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/static/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/static/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#000000",
17 | "background_color": "#000000",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/public/static/wallet/anchor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/wallet/anchor.png
--------------------------------------------------------------------------------
/public/static/wallet/metamask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/wallet/metamask.png
--------------------------------------------------------------------------------
/public/static/wallet/scatter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/wallet/scatter.png
--------------------------------------------------------------------------------
/public/static/wallet/telos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/wallet/telos.png
--------------------------------------------------------------------------------
/public/static/wallet/wombat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novusphere/discussions-vue/dc7cd08bbcb73bac02cab31d82b6a72d92e3e73c/public/static/wallet/wombat.png
--------------------------------------------------------------------------------
/src/bots/staking1.js:
--------------------------------------------------------------------------------
1 | import { getConfig, sleep } from "@/novusphere-js/utility";
2 | //import { setAPIHost } from "@/novusphere-js/discussions/api";
3 | import { getActiveWallets, getAsset } from "@/novusphere-js/uid";
4 | import eos from "@/novusphere-js/uid/eos";
5 |
6 | const fetch = require('node-fetch');
7 | global.fetch = fetch;
8 |
9 | (async function () {
10 | try {
11 | //setAPIHost("http://localhost:8008");
12 |
13 | const ONE_HOUR = 1 * 60 * 60 * 1000;
14 |
15 | const config = await getConfig('staking1', {
16 | key: ''
17 | });
18 |
19 | if (!config.key) throw new Error(`Staking 1.0 key is not configured`);
20 |
21 | for (; ;) {
22 |
23 | let assetMap = {};
24 | const activeWallets = await getActiveWallets();
25 | for (const uidw of activeWallets) {
26 | const asset = await getAsset('ATMOS', uidw);
27 | assetMap[uidw] = asset;
28 | await sleep(100);
29 | }
30 |
31 | let total = Object.values(assetMap).map(a => parseFloat(a)).reduce((a, b) => a + b);
32 | const payouts = Object.keys(assetMap)
33 | .map(key => ({ publicKey: key, balance: assetMap[key] }))
34 | .map(r => ({
35 | key: r.publicKey,
36 | amount: ((parseFloat(r.balance) / total) * 2740)
37 | }))
38 | .filter(r => r.amount >= 0.001)
39 | .map(r => ({
40 | from: `atmosstakerw`,
41 | to: `nsuidcntract`,
42 | quantity: `${r.amount.toFixed(3)} ATMOS`,
43 | memo: r.key,
44 | }));
45 |
46 | const actions = payouts.map(p => ({
47 | account: `novusphereio`,
48 | name: `transfer`,
49 | data: p,
50 | authorization: [{
51 | actor: `atmosstakerw`,
52 | permission: 'active',
53 | }]
54 | }));
55 |
56 | const api = await eos.getAPI("https://api.eosn.io", [config.key]);
57 | const tx = await api.transact({ actions: actions },
58 | {
59 | blocksBehind: 3,
60 | expireSeconds: 30,
61 | });
62 |
63 | console.log(actions.length);
64 | console.log(tx);
65 |
66 | for (let n = 0; n < 24; n++) {
67 | console.log(`Resting... ${new Date()}`);
68 | await sleep(ONE_HOUR);
69 | }
70 | }
71 | }
72 | catch (ex) {
73 | console.log(ex);
74 | }
75 |
76 | })();
--------------------------------------------------------------------------------
/src/bots/test.js:
--------------------------------------------------------------------------------
1 | import fetch from "node-fetch";
2 | import { getConfig } from "@/novusphere-js/utility";
3 | import { getMarketCaps } from "@/novusphere-js/uid";
4 |
5 | (async function () {
6 | const config = await getConfig(`test`);
7 |
8 | console.log((await getMarketCaps())['ATMOS']);
9 |
10 | })();
--------------------------------------------------------------------------------
/src/components/AboutUsCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | mdi-github
9 |
10 |
11 | mdi-twitter
12 |
13 |
14 | mdi-telegram
15 |
16 |
17 | mdi-discord
18 |
19 |
20 |
21 | Privacy Policy
26 | User Agreement
31 | Build {{ version }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/AppAlertBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
78 |
--------------------------------------------------------------------------------
/src/components/AppNavRight.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/components/ApproveTransfersCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Transfer
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{ t.total }}
15 |
16 |
17 | arrow_right_alt
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | {{ closeText }}
45 |
46 | Submit
47 |
48 |
49 |
50 |
51 |
52 |
100 |
101 |
--------------------------------------------------------------------------------
/src/components/AssetCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ symbol }}
7 |
8 | {{ quantity }}
9 | {{ price }} {{ symbol }}/USD
12 |
13 | {{ change24 > 0 ? "+" : "" }}{{ change24 }}%
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/components/BarChart.js:
--------------------------------------------------------------------------------
1 | import { Bar, mixins } from 'vue-chartjs';
2 | const { reactiveProp } = mixins;
3 |
4 | export default {
5 | extends: Bar,
6 | mixins: [reactiveProp],
7 | props: ['options'],
8 | mounted() {
9 | // this.chartData is created in the mixin.
10 | // If you want to pass options please create a local options object
11 | this.renderChart(this.chartData, this.options);
12 | }
13 | }
--------------------------------------------------------------------------------
/src/components/BrowsePageLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
44 |
45 |
50 |
--------------------------------------------------------------------------------
/src/components/CommunityCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ community.members }} members
11 |
12 |
13 |
14 |
19 |
20 |
21 |
26 | person_add Join
27 |
28 |
29 | person_remove
30 | Leave
31 |
32 | View
38 |
39 |
40 |
41 |
42 |
43 |
44 |
70 |
71 |
--------------------------------------------------------------------------------
/src/components/ConnectWalletBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 | Connect Wallet
9 |
10 |
11 | $emit('start-connect')"
17 | @connected="connected"
18 | @error="(args) => $emit('error', args)"
19 | />
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/components/Countdown.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ timeLeft }}
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/components/FullScreenDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
67 |
68 |
--------------------------------------------------------------------------------
/src/components/ImageViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | close Close
7 |
8 |
9 |
10 |
15 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
76 |
77 |
--------------------------------------------------------------------------------
/src/components/LandingInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{ reveal ? 'mdi-close-circle-outline' : 'mdi-plus-circle-outline' }}
14 | More
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 | {{ reveal ? 'mdi-close-circle-outline' : 'mdi-plus-circle-outline' }}
30 | More
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
57 |
58 |
--------------------------------------------------------------------------------
/src/components/LineChart.js:
--------------------------------------------------------------------------------
1 | import { Line, mixins } from 'vue-chartjs'
2 | const { reactiveProp } = mixins
3 |
4 | export default {
5 | extends: Line,
6 | mixins: [reactiveProp],
7 | props: ['options'],
8 | mounted () {
9 | // this.chartData is created in the mixin.
10 | // If you want to pass options please create a local options object
11 | this.renderChart(this.chartData, this.options)
12 | }
13 | }
--------------------------------------------------------------------------------
/src/components/MarkdownEditor/ImageUploadCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Image Upload
4 |
5 |
6 |
7 | {{ error }}
8 |
9 |
10 | Upload
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/components/MarkdownEditor/InsertLinkCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Insert Link
4 |
5 |
6 | Insert
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/components/MarkdownEditor/nodes/Hashtag.js:
--------------------------------------------------------------------------------
1 | // Modified from: https://github.com/scrumpy/tiptap/blob/a6f4e896dc5723cb807e213966d137e487240631/packages/tiptap-extensions/src/nodes/Mention.js
2 |
3 | import { Node, Mark } from 'tiptap';
4 | import { replaceText, pasteRule } from 'tiptap-commands';
5 | import { Suggestions } from "tiptap-extensions";
6 |
7 | export class HashtagPaste extends Mark {
8 | get name() {
9 | return 'hashtagpaste'
10 | }
11 |
12 | get schema() {
13 | return {
14 | attrs: {
15 | href: {}
16 | },
17 | group: 'inline',
18 | inline: true,
19 | selectable: false,
20 | atom: true,
21 | toDOM: node => ['a', { href: `${node.attrs.href}`, target: `_blank` }, ``,],
22 | parseDOM: [],
23 | }
24 | }
25 |
26 | pasteRules({ type }) {
27 | return [
28 | pasteRule(
29 | /#[a-zA-Z0-9]+/gi,
30 | type,
31 | (match) => ({ href: `/tag/${match.substring(1)}`, tag: '' }),
32 | ),
33 | ]
34 | }
35 | }
36 |
37 | export class Hashtag extends Node {
38 | get name() {
39 | return 'hashtag'
40 | }
41 |
42 | get defaultOptions() {
43 | return {
44 | matcher: {
45 | char: '#',
46 | allowSpaces: false,
47 | startOfLine: false,
48 | }
49 | }
50 | }
51 |
52 | get schema() {
53 | return {
54 | attrs: {
55 | tag: {},
56 | href: {}
57 | },
58 | group: 'inline',
59 | inline: true,
60 | selectable: false,
61 | atom: true,
62 | toDOM: node => ['a', { href: `${node.attrs.href}`, target: `_blank`, }, `${this.options.matcher.char}${node.attrs.tag}`],
63 | parseDOM: [], // they will be parsed as a link, which is ok
64 | }
65 | }
66 |
67 | commands({ schema }) {
68 | return attrs => replaceText(null, schema.nodes[this.name], attrs)
69 | }
70 |
71 | get plugins() {
72 | return [
73 | Suggestions({
74 | command: ({ range, attrs, schema }) => replaceText(range, schema.nodes[this.name], attrs),
75 | appendText: ' ',
76 | matcher: this.options.matcher,
77 | items: this.options.items,
78 | onEnter: this.options.onEnter,
79 | onChange: this.options.onChange,
80 | onExit: this.options.onExit,
81 | onKeyDown: this.options.onKeyDown,
82 | onFilter: this.options.onFilter
83 | }),
84 | ]
85 | }
86 | }
--------------------------------------------------------------------------------
/src/components/MarkdownEditor/nodes/Link2.js:
--------------------------------------------------------------------------------
1 | import { pasteRule } from 'tiptap-commands';
2 | import {
3 | Link,
4 | } from "tiptap-extensions";
5 |
6 | function encodeURI2(str) {
7 | str = decodeURI(str);
8 | str = encodeURI(str);
9 | str = str.replace(/\(/g, "%28");
10 | str = str.replace(/\)/g, "%29");
11 | return str;
12 | }
13 |
14 | export default class Link2 extends Link {
15 |
16 | pasteRules({ type }) {
17 | return [
18 | pasteRule(
19 | /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b([-a-zA-Z0-9@:%_+.~#?&//=!()]*)/gi,
20 | type,
21 | url => ({ href: encodeURI2(url) }),
22 | ),
23 | ]
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/src/components/MarkdownEditor/nodes/Mention.js:
--------------------------------------------------------------------------------
1 | // Modified from: https://github.com/scrumpy/tiptap/blob/a6f4e896dc5723cb807e213966d137e487240631/packages/tiptap-extensions/src/nodes/Mention.js
2 |
3 | import { Node, Mark } from 'tiptap'
4 | import { replaceText, pasteRule } from 'tiptap-commands'
5 | import { Suggestions } from "tiptap-extensions";
6 |
7 | export class MentionPaste extends Mark {
8 | constructor(options) {
9 | super(options);
10 |
11 | this.allSuggestions = this.options.onFilter(undefined, '');
12 | this.pasteRegex = new RegExp(`@(${this.allSuggestions.map(s => s.displayName).join('|')})`, 'gi');
13 | }
14 |
15 | get name() {
16 | return 'mentionpaste'
17 | }
18 |
19 | get schema() {
20 | return {
21 | attrs: {
22 | href: {}
23 | },
24 | group: 'inline',
25 | inline: true,
26 | selectable: false,
27 | atom: true,
28 | toDOM: node => ['a', { href: `${node.attrs.href}`, target: `_blank` }, ``,],
29 | parseDOM: [],
30 | }
31 | }
32 |
33 | pasteRules({ type }) {
34 | return this.allSuggestions.length == 0 ? [] : [
35 | pasteRule(
36 | this.pasteRegex,
37 | type,
38 | (match) => {
39 | const [{ pub, displayName }] = this.options.onFilter(undefined, match.substring(1));
40 | return ({ href: `/u/${encodeURIComponent(displayName)}-${pub}` });
41 | },
42 | ),
43 | ]
44 | }
45 | }
46 |
47 | export class Mention extends Node {
48 | get name() {
49 | return 'mention'
50 | }
51 |
52 | get defaultOptions() {
53 | return {
54 | matcher: {
55 | char: '@',
56 | allowSpaces: false,
57 | startOfLine: false,
58 | }
59 | }
60 | }
61 |
62 | get schema() {
63 | return {
64 | attrs: {
65 | name: {},
66 | href: {},
67 | },
68 | group: 'inline',
69 | inline: true,
70 | selectable: false,
71 | atom: true,
72 | toDOM: node => ['a', { href: `${node.attrs.href}`, target: `_blank` }, `${this.options.matcher.char}${node.attrs.name}`],
73 | parseDOM: [], // they will be parsed as a link, which is ok
74 | }
75 | }
76 |
77 | commands({ schema }) {
78 | return attrs => replaceText(null, schema.nodes[this.name], attrs)
79 | }
80 |
81 | get plugins() {
82 | return [
83 | Suggestions({
84 | command: ({ range, attrs, schema }) => replaceText(range, schema.nodes[this.name], attrs),
85 | appendText: ' ',
86 | matcher: this.options.matcher,
87 | items: this.options.items,
88 | onEnter: this.options.onEnter,
89 | onChange: this.options.onChange,
90 | onExit: this.options.onExit,
91 | onKeyDown: this.options.onKeyDown,
92 | onFilter: this.options.onFilter
93 | }),
94 | ]
95 | }
96 | }
--------------------------------------------------------------------------------
/src/components/NotificationsButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ notificationCount }}
6 |
7 | notifications
8 |
9 |
10 | notifications
11 |
12 |
13 |
14 | {{ notificationCount }}
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/PostBrowser.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/PostDisplaySelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ view.icon }}
7 | {{ view.text }}
8 |
9 |
10 |
11 |
12 |
13 | {{ v.icon }}
14 | {{ v.text }}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
60 |
--------------------------------------------------------------------------------
/src/components/PostReplyCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
21 |
22 |
23 |
31 |
32 |
33 |
42 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/components/PostScrollCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/PostScroller.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | No more posts available
18 | No posts results were found
19 |
20 |
21 |
22 |
23 |
24 |
25 |
74 |
--------------------------------------------------------------------------------
/src/components/PostSortSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ valueProxy }}
7 | arrow_drop_down
8 |
9 |
10 |
11 |
12 | {{ item }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
61 |
--------------------------------------------------------------------------------
/src/components/PostThreadLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/PostTips.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ formatTip(tip) }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
78 |
79 |
--------------------------------------------------------------------------------
/src/components/PublicKeyIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
58 |
--------------------------------------------------------------------------------
/src/components/RecentPostsCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | chat New Replies
5 |
6 |
7 |
8 |
9 | {{ p.title }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
54 |
--------------------------------------------------------------------------------
/src/components/SocialMediasCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | mdi-twitter
7 | @{{ twitter.username }}
12 |
13 | clear
14 |
15 |
16 |
17 | mdi-twitter
18 | Authenticate
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/TagIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
38 |
--------------------------------------------------------------------------------
/src/components/TagLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/TokenIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
42 |
--------------------------------------------------------------------------------
/src/components/TransactionBrowser.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | History - {{ filter }}
11 |
12 |
13 |
14 |
15 |
16 | {{ v }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/components/TransactionLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/TransactionScroller.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | No more transactions available
13 | No transaction results were found
14 |
15 |
16 |
17 |
18 |
41 |
--------------------------------------------------------------------------------
/src/components/TransactionSubmitText.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/components/TrendingCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | trending_up Trending
5 |
6 |
7 |
8 |
9 | {{ tag }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
36 |
--------------------------------------------------------------------------------
/src/components/UserAssetSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
17 | {{ !noAmount ? item.asset : item.asset.split(" ")[1] }}
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/UserProfileCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ displayName }}
10 |
11 | {{ extendedInfo.followers }} followers
14 |
15 |
16 |
17 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
41 | person_add
42 |
43 |
51 | person_remove
52 |
53 |
61 | attach_money
62 |
63 |
75 | block
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/src/components/UserProfileLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | // Primarily written and maintained by [bed8c66e8c0c903cf91946f9f625fd9de3b8556972141ee60694faa19d1812da]
2 |
3 | import Vue from 'vue';
4 | import vuetify from './plugins/vuetify';
5 | import router from './plugins/router';
6 | import store from './plugins/vuex';
7 | import App from './App.vue';
8 |
9 | import * as axios from 'axios';
10 | window._axios = axios;
11 |
12 | import VueClipboard from 'vue-clipboard2'
13 | Vue.use(VueClipboard);
14 |
15 | import InfiniteLoading from 'vue-infinite-loading';
16 | Vue.use(InfiniteLoading, { /* options */ });
17 |
18 | import VueGtag from "vue-gtag";
19 | Vue.use(VueGtag, {
20 | config: { id: "UA-178655433-1" },
21 | enabled: (window.localStorage["analytics"] != "disabled")
22 | }, router);
23 |
24 | Vue.config.productionTip = false;
25 |
26 | window.$vue = new Vue({
27 | vuetify,
28 | router,
29 | store,
30 | render: h => h(App)
31 | }).$mount('#app');
--------------------------------------------------------------------------------
/src/manager/index.js:
--------------------------------------------------------------------------------
1 | import { argv } from 'yargs';
2 | import { spawn } from 'child_process';
3 | import { sleep } from "@/novusphere-js/utility";
4 |
5 |
6 | (async function () {
7 |
8 | const queue = [
9 | { name: `w`, cmd: `npm run watcher` },
10 | { name: `s`, cmd: `npm run ${argv.server}` }
11 | ];
12 |
13 | // TO-DO: automatically restart watcher every 1h and server every 24h
14 |
15 | for (; ;) {
16 |
17 | if (queue.length == 0) {
18 | await sleep(10000);
19 | continue;
20 | }
21 |
22 | const { name, cmd } = queue.pop();
23 | const p = spawn(cmd, { shell: true, detached: true });
24 |
25 | p.on('exit', function (code, signal) {
26 | console.log(`[${name}] process exited with code ${code} and signal ${signal}`);
27 | queue.push({ name, cmd });
28 | });
29 |
30 | p.stdout.on('data', (data) => {
31 |
32 | if (data.indexOf('Terminate batch job') > -1) {
33 | console.log(new Date());
34 | p.kill();
35 | }
36 |
37 | console.log(`[${name}] ${data}`);
38 | });
39 |
40 | p.stderr.on('data', (data) => {
41 | console.error(`[${name}] ${data}`);
42 | });
43 |
44 | await sleep(1000);
45 | }
46 |
47 |
48 |
49 |
50 | })();
--------------------------------------------------------------------------------
/src/mixins/safari.js:
--------------------------------------------------------------------------------
1 | export const safariMixin = {
2 | computed: {
3 | isSafari() {
4 | if (
5 | navigator.userAgent.match(/safari/i) &&
6 | !navigator.userAgent.match(/chrome/i)
7 | ) {
8 | return true;
9 | }
10 | return false;
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/mixins/shortTime.js:
--------------------------------------------------------------------------------
1 | import { formatDistance } from "date-fns";
2 |
3 | export const shortTimeMixin = {
4 | methods: {
5 | shortTime(t) {
6 | if (!this.$vuetify.breakpoint.mobile)
7 | return formatDistance(t, new Date(), { addSuffix: true });
8 | else {
9 | const delta = Date.now() - t;
10 | const second = 1000;
11 | const minute = second * 60;
12 | const hour = minute * 60;
13 | const day = hour * 24;
14 |
15 | let unit = (u, s) => {
16 | const n = Math.max(1, Math.ceil(delta / u));
17 | return `${n}${s}`;
18 | };
19 |
20 | if (delta < minute) return unit(second, `s`);
21 | else if (delta < hour) return unit(minute, `m`);
22 | else if (delta < day) return unit(hour, `h`);
23 | else return unit(day, `d`);
24 | }
25 | },
26 | }
27 | }
--------------------------------------------------------------------------------
/src/mixins/submitPost.js:
--------------------------------------------------------------------------------
1 | import { createArtificalTips } from "@/novusphere-js/uid";
2 |
3 | export const submitPostMixin = {
4 | methods: {
5 | async submitPost({ post, transferActions }) {
6 | if (post.edit) {
7 | const p = this.tree[post.parentUuid].post;
8 | if (p) {
9 | p.content = post.content;
10 | p.title = post.title;
11 | }
12 | } else {
13 | if (this.tree[post.uuid]) return;
14 |
15 | const reply = { post, replies: [] };
16 | this.tree[post.uuid] = reply;
17 | this.tree[post.parentUuid].replies.unshift(reply);
18 |
19 | if (transferActions && transferActions.length > 0) {
20 | const parent = this.tree[post.parentUuid];
21 | if (parent) {
22 | transferActions = transferActions.filter(ta => ta.recipientPublicKey == parent.post.uidw); // filter only tips for the parent post
23 | if (transferActions.length > 0) {
24 | let artificalTips = await createArtificalTips(
25 | this.keys.wallet.pub,
26 | post.transaction,
27 | transferActions
28 | );
29 | parent.post.tips.push(...artificalTips);
30 | }
31 | }
32 | }
33 | }
34 | },
35 | }
36 | }
--------------------------------------------------------------------------------
/src/mixins/threadLink.js:
--------------------------------------------------------------------------------
1 | export const threadLinkMixin = {
2 | computed: {
3 | link() {
4 | if (!this.post) return '';
5 | return this.getThreadLink(this.post);
6 | }
7 | },
8 | methods: {
9 | openThreadDialog(post) {
10 | const sub = post.sub;
11 | let title = undefined, referenceId = undefined, referenceId2 = undefined;
12 |
13 | if (post.op) {
14 | title = post.op.getSnakeCaseTitle();
15 | referenceId = post.op.getEncodedId();
16 | referenceId2 = post.getEncodedId();
17 | }
18 | else {
19 | title = post.getSnakeCaseTitle();
20 | referenceId = post.getEncodedId();
21 | referenceId2 = undefined;
22 | }
23 |
24 | this.$store.commit("setThreadDialogOpen", {
25 | value: true,
26 | sub,
27 | referenceId,
28 | title,
29 | referenceId2,
30 | });
31 | },
32 | getThreadLink(post) {
33 | let link = `/tag/${post.sub}`;
34 | if (post.op && post.transaction != post.op.transaction) {
35 | link += `/${post.op.getEncodedId()}/${post.op.getSnakeCaseTitle()}/${post.getEncodedId()}`;
36 | } else {
37 | link += `/${post.getEncodedId()}/${post.getSnakeCaseTitle()}`;
38 | }
39 | return link;
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/mixins/userActions.js:
--------------------------------------------------------------------------------
1 | import { mapState, mapGetters } from "vuex";
2 | import {
3 | blockUser, unblockUser,
4 | followUser, unfollowUser,
5 | subscribeTag, unsubscribeTag, orientTag,
6 | searchPostsByNotifications }
7 | from "@/novusphere-js/discussions/api";
8 |
9 | export const userActionsMixin = {
10 | computed: {
11 | ...mapGetters(["isLoggedIn"]),
12 | ...mapState({
13 | keys: (state) => state.keys,
14 | delegatedMods: (state) => state.delegatedMods,
15 | followingUsers: (state) => state.followingUsers,
16 | blockedUsers: (state) => state.blockedUsers,
17 | notificationCount: (state) => state.notificationCount,
18 | lastSeenNotificationsTime: (state) => state.lastSeenNotificationsTime,
19 | watchedThreads: (state) => state.watchedThreads,
20 | limitMentions: (state) => state.limitMentions
21 | })
22 | },
23 | methods: {
24 | searchPostsByNotifications(time) {
25 | let keyFilter = undefined;
26 |
27 | if (this.limitMentions) {
28 | keyFilter = [...this.followingUsers.map(u => u.pub), ...this.delegatedMods.map(u => u.pub)];
29 | }
30 |
31 | const cursor = searchPostsByNotifications(
32 | this.keys.arbitrary.pub,
33 | (time == undefined) ? this.lastSeenNotificationsTime : time,
34 | this.watchedThreads,
35 | keyFilter
36 | );
37 |
38 | return cursor;
39 | },
40 | openLoginDialog() {
41 | this.$store.commit('setLoginDialogOpen', true);
42 | },
43 | async blockUser({ displayName, pub }) {
44 | console.log(`blockUser`);
45 | if (!this.isLoggedIn) return this.openLoginDialog();
46 |
47 | if (pub == this.keys.arbitrary.pub) return; // self block disallowed
48 | if (this.blockedUsers.find(u => u.pub == pub)) return;
49 |
50 | const nameTime = Date.now();
51 | this.$store.commit('blockUser', {
52 | displayName, pub, nameTime,
53 | beforeSaveCallback: async () => await blockUser(this.keys.identity.key, { displayName, pub, nameTime })
54 | });
55 | },
56 | async unblockUser(pub) {
57 | console.log(`unblockUser`);
58 | if (!this.isLoggedIn) return this.openLoginDialog();
59 |
60 | this.$store.commit('unblockUser', {
61 | pub,
62 | beforeSaveCallback: async () => await unblockUser(this.keys.identity.key, pub)
63 | });
64 | },
65 | async followUser({ displayName, pub, uidw }) {
66 | if (!this.isLoggedIn) return this.openLoginDialog();
67 |
68 | if (pub == this.keys.arbitrary.pub) return; // self follow disallowed
69 | if (this.followingUsers.find(u => u.pub == pub)) return;
70 |
71 | const nameTime = Date.now();
72 | this.$store.commit('followUser', {
73 | displayName, pub, uidw, nameTime,
74 | beforeSaveCallback: async () => await followUser(this.keys.identity.key, { displayName, pub, uidw, nameTime })
75 | });
76 | },
77 | async unfollowUser(pub) {
78 | if (!this.isLoggedIn) return this.openLoginDialog();
79 |
80 | this.$store.commit('unfollowUser', {
81 | pub,
82 | beforeSaveCallback: async () => await unfollowUser(this.keys.identity.key, pub)
83 | });
84 | },
85 | async subscribeTag(tag) {
86 | if (!this.isLoggedIn) return this.openLoginDialog();
87 |
88 | tag = tag.toLowerCase();
89 | this.$store.commit("subscribeTag", {
90 | tag,
91 | beforeSaveCallback: async () => await subscribeTag(this.keys.identity.key, tag)
92 | });
93 | },
94 | async unsubscribeTag(tag) {
95 | if (!this.isLoggedIn) return this.openLoginDialog();
96 |
97 | tag = tag.toLowerCase();
98 | this.$store.commit("unsubscribeTag", {
99 | tag,
100 | beforeSaveCallback: async () => await unsubscribeTag(this.keys.identity.key, tag)
101 | });
102 | },
103 | async orientTag(tag, up) {
104 | if (!this.isLoggedIn) return;
105 |
106 | tag = tag.toLowerCase();
107 | this.$store.commit("orientTag", {
108 | tag,
109 | up,
110 | beforeSaveCallback: async () => await orientTag(this.keys.identity.key, tag, up)
111 | });
112 | }
113 | }
114 | }
--------------------------------------------------------------------------------
/src/novusphere-js/discussions/AccountSearchQuery.js:
--------------------------------------------------------------------------------
1 | import { apiRequest } from "./api";
2 |
3 | export class AccountSearchQuery {
4 | constructor(searchQuery) {
5 | this.setFrom(searchQuery);
6 | }
7 |
8 | //
9 | // Set from another search query
10 | //
11 | setFrom(searchQuery) {
12 | this.id = searchQuery.id;
13 | this.pipeline = searchQuery.pipeline;
14 | this.count = searchQuery.count;
15 | this.limit = searchQuery.limit;
16 | }
17 |
18 | //
19 | // Resets the search query
20 | //
21 | reset() {
22 | const searchQuery = {
23 | pipeline: this.pipeline,
24 | limit: this.limit,
25 | };
26 |
27 | this.setFrom(searchQuery);
28 | }
29 |
30 | //
31 | // Checks whether this search query has more data
32 | // Note: if next() hasn't been called yet, hasMore() will always return false
33 | //
34 | hasMore() {
35 | return (this.id != 0);
36 | }
37 |
38 | async next() {
39 | const payload = await this.nextRaw();
40 | const result = payload.map(dbo => dbo);
41 | return result;
42 | }
43 |
44 | async nextRaw() {
45 | const queryObject = {
46 | id: this.id,
47 | pipeline: this.pipeline,
48 | count: this.count,
49 | limit: this.limit,
50 | };
51 |
52 | try {
53 | const startTime = Date.now();
54 |
55 | const { id, count, limit, accounts } = await apiRequest(`/v1/api/search/accounts`, queryObject);
56 |
57 | this.id = id;
58 | this.count = count;
59 | this.limit = limit;
60 |
61 | const deltaTime = Date.now() - startTime;
62 | if (deltaTime > 1000)
63 | console.proxyLog(`AccountSearchQuery took ${deltaTime}ms to return results`, this.pipeline);
64 |
65 | return accounts;
66 | }
67 | catch (ex) {
68 | console.error(`Trx Search query error`, this, ex);
69 | throw (ex);
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/src/novusphere-js/discussions/DirectMsgSearchQuery.js:
--------------------------------------------------------------------------------
1 | import { apiRequest } from "./api";
2 |
3 | export class DirectMsgSearchQuery {
4 | constructor(key, searchQuery) {
5 | this.key = key;
6 | this.setFrom(searchQuery);
7 | }
8 |
9 | //
10 | // Set from another search query
11 | //
12 | setFrom(searchQuery) {
13 | this.id = searchQuery.id;
14 | this.friendPublicKey = searchQuery.friendPublicKey;
15 | this.count = searchQuery.count;
16 | this.limit = searchQuery.limit;
17 | }
18 |
19 | //
20 | // Resets the search query
21 | //
22 | reset() {
23 | const searchQuery = {
24 | friendPublicKey: this.friendPublicKey,
25 | limit: this.limit,
26 | };
27 |
28 | this.setFrom(searchQuery);
29 | }
30 |
31 | //
32 | // Checks whether this search query has more data
33 | // Note: if next() hasn't been called yet, hasMore() will always return false
34 | //
35 | hasMore() {
36 | return (this.id != 0);
37 | }
38 |
39 | async next() {
40 | const payload = await this.nextRaw();
41 | const result = payload.map(dbo => dbo);
42 | return result;
43 | }
44 |
45 | async nextRaw() {
46 | const queryObject = {
47 | id: this.id,
48 | friendPublicKey: this.friendPublicKey,
49 | count: this.count,
50 | limit: this.limit,
51 | };
52 |
53 | try {
54 | const startTime = Date.now();
55 |
56 | const { id, count, limit, msgs } = await apiRequest(`/v1/api/search/directmsgs`, queryObject, { key: this.key });
57 |
58 | this.id = id;
59 | this.count = count;
60 | this.limit = limit;
61 |
62 | const deltaTime = Date.now() - startTime;
63 | if (deltaTime > 1000)
64 | console.proxyLog(`DirectMsgSearchQuery took ${deltaTime}ms to return results`, this.pipeline);
65 |
66 | return msgs;
67 | }
68 | catch (ex) {
69 | console.error(`Trx Search query error`, this, ex);
70 | throw (ex);
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/src/novusphere-js/discussions/PostSearchQuery.js:
--------------------------------------------------------------------------------
1 | import Joi from "@hapi/joi";
2 | import { Post } from "./Post";
3 | import { apiRequest } from "./api";
4 |
5 | const schema = Joi.object({
6 | id: Joi.number().default(0),
7 | pipeline: Joi.array().items(Joi.object()).required(),
8 | count: Joi.number().default(0),
9 | limit: Joi.number().default(20),
10 | sort: Joi.string(),
11 | votePublicKey: Joi.string(),
12 | includeOpeningPost: Joi.boolean().default(false),
13 | moderatorKeys: Joi.array().items(Joi.string())
14 | });
15 |
16 | export class PostSearchQuery {
17 | //
18 | // Creates a search query, see schema for details
19 | //
20 | constructor(searchQuery) {
21 | this.setFrom(searchQuery);
22 | }
23 |
24 | //
25 | // Set from another search query
26 | //
27 | setFrom(searchQuery) {
28 | searchQuery = schema.validate(searchQuery).value;
29 |
30 | this.id = searchQuery.id;
31 | this.pipeline = searchQuery.pipeline;
32 | this.count = searchQuery.count;
33 | this.limit = searchQuery.limit;
34 | this.sort = searchQuery.sort;
35 | this.includeOpeningPost = searchQuery.includeOpeningPost;
36 | this.votePublicKey = searchQuery.votePublicKey;
37 | this.moderatorKeys = searchQuery.moderatorKeys;
38 | }
39 |
40 | //
41 | // Resets the search query
42 | //
43 | reset() {
44 | const searchQuery = {
45 | pipeline: this.pipeline,
46 | limit: this.limit,
47 | sort: this.sort,
48 | includeOpeningPost: this.includeOpeningPost,
49 | votePublicKey: this.votePublicKey,
50 | moderatorKeys: this.moderatorKeys
51 | };
52 |
53 | this.setFrom(searchQuery);
54 | }
55 |
56 | //
57 | // Checks whether this search query has more data
58 | // Note: if next() hasn't been called yet, hasMore() will always return false
59 | //
60 | hasMore() {
61 | return (this.id != 0);
62 | }
63 |
64 | //
65 | // Directly returns the payload from a search
66 | //
67 | async next() {
68 | const payload = await this.nextRaw();
69 | const result = payload.map(dbo => Post.fromDbObject(dbo));
70 | //console.log(result[0].transaction + ' - ' + result[0].title);
71 | //console.log(JSON.stringify(queryObject));
72 | return result;
73 | }
74 |
75 | //
76 | // Returns a Post[] of next posts for the query asynchronously
77 | //
78 | async nextRaw() {
79 | const queryObject = {
80 | id: this.id,
81 | pipeline: this.pipeline,
82 | count: this.count,
83 | limit: this.limit,
84 | sort: this.sort,
85 | votePublicKey: this.votePublicKey,
86 | includeOpeningPost: this.includeOpeningPost,
87 | moderatorKeys: this.moderatorKeys
88 | };
89 |
90 | try {
91 | const startTime = Date.now();
92 |
93 | const { id, count, limit, posts } = await apiRequest(`/v1/api/search/posts`, queryObject);
94 |
95 | this.id = id;
96 | this.count = count;
97 | this.limit = limit;
98 |
99 | const deltaTime = Date.now() - startTime;
100 | if (deltaTime > 1000)
101 | console.proxyLog(`PostSearchQuery took ${deltaTime}ms to return results`, this.pipeline);
102 |
103 | return posts;
104 | }
105 | catch (ex) {
106 | console.error(`Search query error`, this, ex);
107 | throw (ex);
108 | }
109 | }
110 | }
--------------------------------------------------------------------------------
/src/novusphere-js/discussions/gateway.js:
--------------------------------------------------------------------------------
1 | import io from 'socket.io-client';
2 | import { getAPIHost, createSignedBody } from './api';
3 | import { waitFor } from "../utility";
4 | import { Aes } from 'eosjs-ecc';
5 | import Long from 'long';
6 |
7 | window._Aes = Aes;
8 | window._Long = Long;
9 | window._Buffer = Buffer;
10 |
11 | let _callbacks = {};
12 | let _lastGatewayId = 1;
13 | let $socket = undefined; // NOTE: WARNING: HMR can cause multiple sockets
14 | const $state = {
15 | identityKey: ''
16 | }
17 |
18 | async function subscribeAccount(identityKey) {
19 | const subscription = await gatewaySend('subscribeAccount', {}, { key: identityKey });
20 | if (subscription) {
21 | $state.identityKey = identityKey;
22 | }
23 | return subscription;
24 | }
25 |
26 | async function decryptDirectMessage(arbitraryKey, friendPublicKey, message, nonce, checksum) {
27 | const encryptedBuffer = Buffer.from(message, "hex");
28 | //console.log('decrypt', arbitraryKey, nonce.toString(), encryptedBuffer.toString('hex'), checksum, friendPublicKey);
29 | return Aes.decrypt(arbitraryKey, friendPublicKey, Long.fromString(nonce), encryptedBuffer, checksum).toString('utf8');
30 | }
31 |
32 | async function sendDirectMessage(arbitraryKey, friendPublicKey, textMessage) {
33 | const { nonce, message, checksum } = Aes.encrypt(arbitraryKey, friendPublicKey, textMessage);
34 | const nonceStr = nonce.toString();
35 | const messageStr = message.toString('hex');
36 | //console.log('encrypt', arbitraryKey, nonceStr, messageStr, checksum, friendPublicKey);
37 | const dm = await gatewaySend('sendDirectMessage', { nonce: nonceStr, message: messageStr, checksum, friendPublicKey }, { key: arbitraryKey });
38 | return dm;
39 | }
40 |
41 | function gatewaySend(method, body, { key, domain }) {
42 | return new Promise((resolve, reject) => {
43 |
44 | (async function () {
45 | const socket = await getSocket();
46 |
47 | if (key) {
48 | body = await createSignedBody(key, domain, body);
49 | }
50 |
51 | const id = _lastGatewayId++;
52 | const timeout = setTimeout(() => reject(new Error('gatewaySend timeout')), 30000);
53 |
54 | _callbacks[id] = ({ payload, error, message }) => {
55 | clearTimeout(timeout);
56 | delete _callbacks[id];
57 |
58 | if (error) return reject(new Error(message));
59 | return resolve(payload);
60 | };
61 |
62 | socket.emit('api', { id, method, data: body });
63 | })();
64 |
65 | });
66 | }
67 |
68 | async function getSocket() {
69 | if (!$socket) {
70 | const host = await getAPIHost();
71 | const socket = io(host, {});
72 |
73 | socket.on('apiResponse', ({ id, payload, error, message }) => {
74 | const callback = _callbacks[id];
75 | if (callback) callback({ payload, error, message });
76 | });
77 |
78 | socket.on('accountChange', (e) => {
79 |
80 | const event = new CustomEvent('accountChange', { detail: e });
81 | window.dispatchEvent(event);
82 |
83 | });
84 |
85 | socket.on('receiveDirectMessage', (e) => {
86 |
87 | const detail = { ...e.payload };
88 | //console.log(detail);
89 |
90 | const event = new CustomEvent('receiveDirectMessage', { detail });
91 | window.dispatchEvent(event);
92 |
93 | });
94 |
95 | socket.on('connect', () => {
96 | if ($state.identityKey) {
97 | // resubscribe
98 | subscribeAccount($state.identityKey);
99 | }
100 | });
101 |
102 | $socket = socket;
103 | }
104 |
105 | await waitFor(() => $socket && $socket.connected, 500);
106 | return $socket;
107 | }
108 |
109 | export {
110 | decryptDirectMessage,
111 | sendDirectMessage,
112 | subscribeAccount
113 | }
--------------------------------------------------------------------------------
/src/novusphere-js/discussions/index.js:
--------------------------------------------------------------------------------
1 | export { Post } from './Post';
2 |
3 | import * as api from './api';
4 | import * as gateway from './gateway';
5 |
6 | export {
7 | api,
8 | gateway
9 | }
--------------------------------------------------------------------------------
/src/novusphere-js/index.js:
--------------------------------------------------------------------------------
1 | import * as discussions from './discussions';
2 | import * as utility from './utility';
3 | import * as uid from './uid';
4 |
5 |
6 | export {
7 | discussions,
8 | utility,
9 | uid
10 | }
--------------------------------------------------------------------------------
/src/novusphere-js/uid/TransactionSearchQuery.js:
--------------------------------------------------------------------------------
1 | import { apiRequest } from "../discussions/api";
2 |
3 | export class TransactionSearchQuery {
4 | constructor(searchQuery) {
5 | this.setFrom(searchQuery);
6 | }
7 |
8 | //
9 | // Set from another search query
10 | //
11 | setFrom(searchQuery) {
12 | this.id = searchQuery.id;
13 | this.pipeline = searchQuery.pipeline;
14 | this.count = searchQuery.count;
15 | this.limit = searchQuery.limit;
16 | }
17 |
18 | //
19 | // Resets the search query
20 | //
21 | reset() {
22 | const searchQuery = {
23 | pipeline: this.pipeline,
24 | limit: this.limit,
25 | };
26 |
27 | this.setFrom(searchQuery);
28 | }
29 |
30 | //
31 | // Checks whether this search query has more data
32 | // Note: if next() hasn't been called yet, hasMore() will always return false
33 | //
34 | hasMore() {
35 | return (this.id != 0);
36 | }
37 |
38 | async next() {
39 | const payload = await this.nextRaw();
40 | const result = payload.map(dbo => dbo);
41 | return result;
42 | }
43 |
44 | async nextRaw() {
45 | const queryObject = {
46 | id: this.id,
47 | pipeline: this.pipeline,
48 | count: this.count,
49 | limit: this.limit,
50 | };
51 |
52 | try {
53 | const startTime = Date.now();
54 |
55 | const { id, count, limit, trxs } = await apiRequest(`/v1/api/search/uid`, queryObject);
56 |
57 | this.id = id;
58 | this.count = count;
59 | this.limit = limit;
60 |
61 | const deltaTime = Date.now() - startTime;
62 | if (deltaTime > 1000)
63 | console.proxyLog(`TransactionSearchQuery took ${deltaTime}ms to return results`, this.pipeline);
64 |
65 | return trxs;
66 | }
67 | catch (ex) {
68 | console.error(`Trx Search query error`, this, ex);
69 | throw (ex);
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/src/novusphere-js/uid/bch.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | }
--------------------------------------------------------------------------------
/src/novusphere-js/uid/bufferwriter.js:
--------------------------------------------------------------------------------
1 | import * as base58 from 'bs58'
2 |
3 | export default class BufferWriter {
4 | static fromBuffer(buffer) {
5 | let bw = new BufferWriter(buffer.length);
6 | return bw.write(buffer);
7 | }
8 |
9 | constructor(size = 256) {
10 | this.offset = 0;
11 | this.size = size;
12 | this.buffer = new Buffer(this.size);
13 | }
14 |
15 | _resizeForIncomingWrite(writeSize) {
16 | if (this.offset + writeSize >= this.size) {
17 | let buffer = new Buffer(this.size * 2);
18 | this.buffer.copy(buffer, 0, 0, buffer.length);
19 | this.buffer = buffer;
20 | this.size = buffer.length;
21 | }
22 | }
23 |
24 | skip(amount) {
25 | this._resizeForIncomingWrite(amount);
26 | this.offset += amount;
27 | return this;
28 | }
29 |
30 | write(buffer, length) {
31 | length = length || buffer.length;
32 |
33 | this._resizeForIncomingWrite(length);
34 |
35 | buffer.copy(this.buffer, this.offset, 0, length);
36 | this.offset += length;
37 | return this;
38 | }
39 |
40 | writeByte(num) {
41 | this._resizeForIncomingWrite(1);
42 | this.buffer[this.offset] = (num & 0xFF);
43 | this.offset++;
44 | return this;
45 | }
46 |
47 | writeUInt64(num) {
48 | let buffer = Buffer.alloc(8);
49 | buffer.writeUIntLE(num, 0, 6);
50 | return this.write(buffer);
51 | }
52 |
53 | writeAsset(asset) {
54 | const [balance] = asset.split(' ');
55 | return this.writeUInt64(parseInt(balance.replace('.', '')));
56 | }
57 |
58 | writePublicKey(publicKey, prefix = '') {
59 | if (prefix && publicKey.indexOf(prefix) != 0) throw new Error(`Public key ${publicKey} does not start with expected prefix "${prefix}"`);
60 | const key = base58.decode(publicKey.substring(prefix.length));
61 | return this.write(key, 33);
62 | }
63 |
64 | writeString(str) {
65 | return this.write(Buffer.from(str));
66 | }
67 |
68 | writeQuantity(quantity) {
69 | return this.writeUInt64(parseInt(quantity.replace('.', '')));
70 | }
71 |
72 | toBuffer() {
73 | let buffer = new Buffer(this.offset);
74 | this.buffer.copy(buffer, 0, 0, this.offset);
75 | return buffer;
76 | }
77 | }
--------------------------------------------------------------------------------
/src/novusphere-js/uid/eos.js:
--------------------------------------------------------------------------------
1 | import { Api, JsonRpc } from 'eosjs'
2 | import { JsSignatureProvider } from 'eosjs/dist/eosjs-jssig';
3 | import { initAccessContext } from 'eos-transit';
4 | import scatter from 'eos-transit-scatter-provider';
5 | import anchor from 'eos-transit-anchorlink-provider';
6 |
7 | const GREYMASS_EOS_RPC = 'https://eos.greymass.com';
8 | const EOSCAFE_EOS_RPC = 'https://eos.eoscafeblock.com';
9 | const EOSNATION_EOS_RPC = 'https://api.eosn.io';
10 | const DEFAULT_EOS_RPC = GREYMASS_EOS_RPC;
11 | const DEFAULT_TELOS_RPC = 'https://telos.greymass.com';
12 |
13 | const ACCESS_CONTEXT_OPTIONS = {
14 | appName: 'Discussions',
15 | network: {
16 | protocol: '',
17 | host: '',
18 | port: 443,
19 | chainId: 'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906',
20 | },
21 | walletProviders: [
22 | anchor(`discussions`),
23 | scatter(),
24 | scatter()
25 | ]
26 | }
27 |
28 | function getWalletNames() {
29 | // NOTE: this should match [ACCESS_CONTEXT_OPTIONS.walletProviders] indexes
30 | return [`anchor`, `scatter`, `wombat`];
31 | }
32 |
33 | function makeNetwork(rpc) {
34 | const network = {
35 | ...ACCESS_CONTEXT_OPTIONS.network,
36 | protocol: rpc.substring(0, rpc.indexOf(':')),
37 | host: rpc.substring(rpc.indexOf(':') + 3)
38 | };
39 | return network;
40 | }
41 |
42 | async function connectWallet(name, chain) {
43 |
44 | let network = makeNetwork(DEFAULT_EOS_RPC);
45 |
46 | if (chain == 'telos') {
47 | network = makeNetwork(DEFAULT_TELOS_RPC);
48 | network.chainId = '4667b205c6838ef70ff7988f6e8257e8be0e1284a2f59699054a018f743b1d11';
49 | }
50 |
51 |
52 | const accessContextOptions = {
53 | ...ACCESS_CONTEXT_OPTIONS,
54 | network
55 | }
56 |
57 | const accessContext = initAccessContext(accessContextOptions);
58 |
59 | const walletProviders = accessContext.getWalletProviders();
60 | const selectedProvider = walletProviders[getWalletNames().findIndex(wn => wn == name)];
61 |
62 | const wallet = accessContext.initWallet(selectedProvider);
63 | await wallet.connect();
64 | if (name == 'scatter') {
65 | await wallet.logout();
66 | }
67 | await wallet.login();
68 | return wallet;
69 | }
70 |
71 | async function getAPI(rpcEndpoint, keys = [], rpcConfig = {}) {
72 | rpcEndpoint = rpcEndpoint || DEFAULT_EOS_RPC;
73 | const signatureProvider = new JsSignatureProvider(keys);
74 | const jsonRpc = new JsonRpc(rpcEndpoint, rpcConfig);
75 | const api = new Api({
76 | rpc: jsonRpc,
77 | signatureProvider,
78 | textDecoder: new TextDecoder(),
79 | textEncoder: new TextEncoder(),
80 | });
81 | return api;
82 | }
83 |
84 | async function getAccount(name, { rpcEndpoint, rpcConfig } = {}) {
85 | const api = await getAPI(rpcEndpoint, undefined, rpcConfig);
86 | try {
87 | const account = await api.rpc.get_account(name);
88 | return account;
89 | }
90 | catch (ex) {
91 | return undefined;
92 | }
93 | }
94 |
95 | export default {
96 | DEFAULT_EOS_RPC,
97 | DEFAULT_TELOS_RPC,
98 | GREYMASS_EOS_RPC,
99 | EOSNATION_EOS_RPC,
100 | EOSCAFE_EOS_RPC,
101 | getWalletNames,
102 | getAPI,
103 | connectWallet,
104 | getAccount
105 | }
--------------------------------------------------------------------------------
/src/novusphere-js/uid/eth.js:
--------------------------------------------------------------------------------
1 | import { sleep, waitFor } from "@/novusphere-js/utility";
2 |
3 | class MetamaskWallet {
4 | constructor() {
5 | this.provider = {
6 | id: `metamask`
7 | };
8 | }
9 | async connect() {
10 | if (typeof window.ethereum == "undefined" || typeof window.web3 == "undefined") {
11 | throw new Error(`Unable to connect to metamask`);
12 | }
13 |
14 | this.ethereum = window.ethereum;
15 | this.web3 = window.web3;
16 |
17 | this.ethereum.enable(); // we do not await this since older versions do not throw ex when canceling
18 | await waitFor(async () => this.web3.eth.accounts.length > 0, 500, 25000,
19 | `Connecting to metamask has taken too long, try again`);
20 | }
21 | async login() {
22 | await sleep(1000);
23 | const account = this.web3.eth.accounts[0];
24 | console.log(`eth login ${account}`);
25 | this.auth = {
26 | accountName: '',
27 | permission: '',
28 | publicKey: account
29 | }
30 | }
31 | async logout() {
32 | this.ethereum = null;
33 | this.web3 = null;
34 | }
35 | signArbitrary(msg) {
36 | if (!this.auth || !this.auth.publicKey)
37 | throw new Error(`Not logged into Metamask`);
38 |
39 | return new Promise((resolve, reject) => this.web3.currentProvider.sendAsync({
40 | method: 'personal_sign',
41 | params: [msg, this.auth.publicKey],
42 | from: this.auth.publicKey,
43 | }, function (err, result) {
44 | if (err) reject(err);
45 | else resolve(result.result);
46 | }));
47 | }
48 | }
49 |
50 | function getWalletNames() {
51 | return [`metamask`];
52 | }
53 |
54 | async function connectWallet(name) {
55 | if (name == `metamask`) {
56 | const wallet = new MetamaskWallet();
57 | await wallet.connect();
58 | await wallet.login();
59 | return wallet;
60 | }
61 | return undefined;
62 | }
63 |
64 | export default {
65 | MetamaskWallet,
66 | connectWallet,
67 | getWalletNames
68 | };
--------------------------------------------------------------------------------
/src/novusphere-js/uid/workers/ecc.js:
--------------------------------------------------------------------------------
1 | import ecc from "eosjs-ecc";
2 | import { expose } from "threads/worker";
3 |
4 | expose({
5 | sign: ecc.sign,
6 | signHash: ecc.signHash
7 | })
8 |
--------------------------------------------------------------------------------
/src/novusphere-js/utility/lock.js:
--------------------------------------------------------------------------------
1 | import { waitFor } from "@/novusphere-js/utility";
2 |
3 | export default class Lock {
4 | Lock() {
5 | this.isLocked = false;
6 | }
7 | async lock(asyncTask) {
8 | await waitFor(() => !this.isLocked);
9 | this.isLocked = true;
10 | try {
11 | await asyncTask();
12 | }
13 | finally {
14 | this.isLocked = false;
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/src/pages/BlankPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/pages/BrowseFeedPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | It looks like you're not subscribed to any tags, or following any
8 | users! Click the button below to discover some reccomended communities
9 | and users.
11 |
12 | Discover
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/pages/BrowseSearchPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/pages/BrowseTagPostsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/pages/BrowseThreadPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/pages/BrowseTrendingPostsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/pages/ClosePage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/pages/HomePage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/pages/LogOutPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/pages/MainLayoutPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/pages/MissingPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
404
4 |
Page not found
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/pages/RecoverPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Recover
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/pages/discover/DiscoverCommunityPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
35 |
36 |
--------------------------------------------------------------------------------
/src/pages/discover/DiscoverPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Communities
6 | Users
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/pages/discover/DiscoverUserPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/pages/notifications/BrowsePostNotificationsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/pages/notifications/BrowseTrxNotificationsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/pages/notifications/NotificationsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Posts
6 | Tips
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/pages/settings/BrowseModeratedPostsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/pages/settings/BrowseWatchedThreadsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/pages/settings/KeysSettingsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | content_copy
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | content_copy
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
37 |
38 |
39 |
40 | Reveal Private Keys
41 |
42 |
43 |
44 | Enter your password to reveal your private keys, which are used to make posts, manage your account and transfer tokens.
45 | Please be cautious when accessing this page in areas where others can easily see your screen.
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/pages/settings/SettingsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Settings
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Content
17 | Watched
18 | Pinned
19 | Spam
20 | NSFW
21 | Keys
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/pages/tests/TestEditorPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Editor
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Log
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/pages/tests/TestsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Build:
8 | {{ build }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Clear
18 |
19 |
20 |
21 |
22 |
23 |
24 | Browse
25 |
26 |
27 |
28 | All
29 |
30 |
31 | {{ p.name }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Misc
40 |
41 |
42 |
43 | Airdrop
44 |
45 |
46 | Analytics
47 |
48 |
49 | Editor
50 |
51 |
52 | Dialog Thread
53 |
54 |
55 | Test Trx
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/pages/tests/posts/TestBrowsePostsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/pages/tests/posts/posts.js:
--------------------------------------------------------------------------------
1 | const testPosts = [
2 | { name: 'No Embed', transaction: "ab25c7aa416c67991d13f441da98440a82c568a53c40d3cd286931e0c2c4595c" },
3 | { name: 'Telegram', transaction: "e1f11da63f697baf2979b522785379e17f8d38ac39a102c72b617902a820f286" },
4 | { name: 'Twitter', transaction: "8cdd6819d421238bcfa070ad6500d284a9e16c55dbe4662bb34bdd7970d97bae" },
5 | { name: 'Youtube', transaction: "8df76a2c8fdfdb3e2d339aa4451420289cfd0382fc7c931ccfee2eb640e5a807" },
6 | { name: 'Instagram', transaction: "59c14291ea28fa91f5112a441e9acaffa0032b716a491e8e8de9bc7f7bb0db6f" },
7 | { name: 'Soundcloud', transaction: "ddc5607f78c1c8857bef10c72da1dd2106e8e25059f6593248dd92ca238e2672" },
8 | { name: 'Long', transaction: "9c7b051405f1ccc6d19639c3240ed27ca4ac8fe9bb0007806b852c16f823b2f5" },
9 | { name: 'Tipped', transaction: "d8b54c5a74da9d0027eb1a6dacd61a0e6cead3bc99c80bb54344ab9b1063d4e0" },
10 | { name: 'NSFW', transaction: "1e15faaa03f625ea2aefb0f918df49ccd113b55fd0e8edd5f9df0223637f06ce" },
11 | ];
12 |
13 | export {
14 | testPosts
15 | }
--------------------------------------------------------------------------------
/src/pages/wallet/WalletAssetsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Price feed provided by EOSGO
5 |
6 |
11 | updateShow(i, !zero)" />
12 |
13 |
14 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/pages/wallet/WalletPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{ keys.wallet.pub }}
12 |
13 |
14 |
15 | content_copy
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | Assets
25 | Deposit
26 | Withdraw
27 | Swap
28 | Stake
29 | EOSIO Account
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
59 |
60 |
--------------------------------------------------------------------------------
/src/plugins/vuetify.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuetify from 'vuetify/lib';
3 |
4 | Vue.use(Vuetify);
5 |
6 | export default new Vuetify({
7 | options: {
8 | customProperties: true
9 | },
10 | theme: {
11 | dark: true,
12 | themes: {
13 | light: {
14 | primary: '#079e99',
15 | secondary: '#ffffff',
16 | accent: '#49C193',
17 | error: '#b71c1c',
18 | background: '#ecf0f1'
19 | },
20 | dark: {
21 | primary: '#079e99',
22 | secondary: '#000000',
23 | accent: '#49C193',
24 | error: '#b71c1c',
25 | background: '#000000'
26 | },
27 | },
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/src/server/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | [
4 | "@babel/plugin-proposal-decorators",
5 | {
6 | "legacy": true
7 | }
8 | ],
9 | [
10 | "babel-plugin-root-import",
11 | {
12 | "root": "./src/",
13 | "rootPathPrefix": "@/"
14 | }
15 | ]
16 | ],
17 | "presets": [
18 | "@babel/preset-env"
19 | ]
20 | }
--------------------------------------------------------------------------------
/src/server/controllers/ModerationController.js:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Get } from '@decorators/express';
2 | import { Api } from "../helpers";
3 | import { config, getDatabase } from "../mongo";
4 |
5 | export default @Controller('/moderation') class ModerationController {
6 | constructor() {
7 | }
8 |
9 | @Api()
10 | @Get('/test')
11 | async test(req, res) {
12 | return res.success([]);
13 | }
14 |
15 | /*@Api()
16 | @Post('/posts/:tag')
17 | async posts(req, res) {
18 | const { domain, mods, tag, tags, thread } = req.unpack();
19 | if (!domain) throw new Error(`Field domain is unspecified`);
20 | if (!mods || !Array.isArray(mods)) throw new Error(`Field mods must be specified`);
21 |
22 | let db = await getDatabase();
23 |
24 |
25 | const matchPosts = {};
26 |
27 | if (thread) {
28 | matchPosts["$expr"] = { $eq: ["$post.uuid", "$post.threadUuid"] };
29 | }
30 |
31 | if (tags && tags.length > 0) {
32 | matchPosts["post.tags"] = { $in: tags };
33 | }
34 |
35 | let result = await db.collection(config.table.moderation)
36 | .aggregate([{
37 | $match: {
38 | domain: domain,
39 | pub: { $in: mods },
40 | tags: tag
41 | },
42 | },
43 | {
44 | $lookup:
45 | {
46 | from: config.table.posts,
47 | let: { uuid: "$uuid" },
48 | pipeline: [
49 | {
50 | $match: {
51 | $expr: {
52 | $eq: ["$uuid", "$$uuid"]
53 | }
54 | }
55 | }
56 | ],
57 | as: "post"
58 | }
59 | },
60 | ...(Object.keys(matchPosts).length > 0 ? [{ $match: matchPosts }] : []),
61 | {
62 | $project:
63 | {
64 | mod: "$pub",
65 | uuid: "$uuid",
66 | tags: "$tags",
67 | post: { "$arrayElemAt": ["$post", 0] }
68 | }
69 | },
70 | {
71 | $project:
72 | {
73 | mod: "$pub",
74 | uuid: "$uuid",
75 | threadUuid: "$post.threadUuid",
76 | transaction: "$post.transaction",
77 | createdAt: "$post.createdAt"
78 | }
79 | }
80 | ])
81 | .toArray();
82 |
83 | return res.success(result);
84 | }*/
85 |
86 | @Api()
87 | @Post('/settags')
88 | async setTags(req, res) {
89 |
90 | const { pub, domain, data: { uuid, tags } } = req.unpackAuthenticated({ tags: [] });
91 |
92 | if (!uuid) throw new Error(`Uuid must be specified`);
93 | if (!Array.isArray(tags)) throw new Error(`Tags must be an array`);
94 |
95 | let db = await getDatabase();
96 |
97 | await db.collection(config.table.moderation).updateOne({
98 | pub: pub,
99 | domain: domain,
100 | uuid: uuid
101 | }, {
102 | $setOnInsert: {
103 | pub: pub,
104 | domain: domain,
105 | uuid: uuid
106 | },
107 | $set: {
108 | tags: tags
109 | }
110 | }, {
111 | upsert: true
112 | });
113 |
114 | return res.success({
115 | uuid,
116 | tags
117 | });
118 | }
119 | }
--------------------------------------------------------------------------------
/src/server/controllers/UploadController.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import { Controller, Post, Get, All } from '@decorators/express';
3 | import { Api } from "../helpers";
4 | import siteConfig from "../site";
5 | import fs from 'fs';
6 |
7 | const multer = require('multer');
8 | const mime = require('mime');
9 | const path = require('path');
10 |
11 | export default @Controller('/upload') class UploadController {
12 | constructor() {
13 | const storage = multer.diskStorage({
14 | destination: (req, file, cb) => {
15 | cb(null, path.join('public', 'uploads'))
16 | },
17 | filename: (req, file, cb) => {
18 | // TO-DO: change to hash?
19 | const filename = `${Date.now()}.${mime.getExtension(file.mimetype)}`;
20 | cb(null, filename);
21 | }
22 | });
23 |
24 | const upload = multer({
25 | storage: storage,
26 | fileFilter: function (req, file, cb) {
27 | const imageRx = /\.(jpg|jpeg|png|gif)$/i;
28 | const ext = `.${mime.getExtension(file.mimetype)}`;
29 | if (!ext.match(imageRx)) {
30 | return cb(new Error('Only image files are allowed!'));
31 | }
32 | cb(null, true);
33 | },
34 | limits: {
35 | files: 1, // allow only 1 file per request
36 | fileSize: (siteConfig.uploadLimit || 1) * 1024 * 1024, // (max file size)
37 | },
38 | });
39 |
40 | this.uploadSingle = upload.single('image');
41 | }
42 |
43 | @All('/file/:filename')
44 | async getFile(req, res) {
45 | const [filename] = req.params.filename.match(/[a-z0-9]+/i);
46 | const mimeType = mime.getType(req.params.filename);
47 | const ext = mime.getExtension(mimeType);
48 |
49 | fs.readFile(path.join('public', 'uploads', `${filename}.${ext}`), function (err, data) {
50 | if (err) {
51 | res.writeHead(404);
52 | return res.end(JSON.stringify({ error: `File not found` }));
53 | }
54 | res.setHeader('Content-Type', mimeType);
55 | res.writeHead(200);
56 | return res.end(data);
57 | });
58 | }
59 |
60 | @Api()
61 | @Post('/')
62 | async uploadFile(req, res) {
63 | const filename = await (new Promise((resolve, reject) => {
64 | this.uploadSingle(req, res, (err) => {
65 | if (err) return reject(err);
66 | const file = req.file;
67 | if (file) return resolve(file.filename);
68 | return reject(new Error(`An unexpected error occured while uploading`));
69 | });
70 | }));
71 |
72 | return res.success({
73 | filename
74 | })
75 | }
76 | }
--------------------------------------------------------------------------------
/src/server/events/index.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require('events');
2 | EventEmitter.defaultMaxListeners = 1000;
3 |
4 | const accountEvent = new EventEmitter();
5 |
6 | export {
7 | accountEvent
8 | }
--------------------------------------------------------------------------------
/src/server/mongo/config.js:
--------------------------------------------------------------------------------
1 | import { getConfig } from "@/novusphere-js/utility";
2 |
3 | //
4 | // NOTE:
5 | // create /config/mongo.json and put your "connection" field there
6 | //
7 |
8 | let config = {
9 | "connection": "mongodb://localhost:27017",
10 | "database": "discussions2",
11 | "contract": {
12 | "discussions": "discussionsx",
13 | "uid": "nsuidcntract"
14 | },
15 | "table": {
16 | "discussions": "discussionsx",
17 | "uid": "discussionsx", // -- make the same table as discussions contract 10/21/2020
18 | "analytics": "analytics",
19 | "state": "state",
20 | "posts": "posts",
21 | "votes": "votes",
22 | "accounts": "accounts",
23 | "moderation": "moderation",
24 | "directmsgs": "directmsgs",
25 | "oauths": "oauths"
26 | },
27 | "index": {
28 | "analytics": {
29 | "type": 1,
30 | "time": 1
31 | },
32 | "discussionsx": {
33 | "transaction": 1,
34 | "name": 1,
35 | "time": 1,
36 | "data.relayer": 1,
37 | "data.from": 1,
38 | "data.to": 1,
39 | "data.memo": 1, // unfortunately, we need an index on memos for depositing...
40 | },
41 | "accounts": {
42 | "pub": 1,
43 | "domain": 1,
44 | "data.publicKeys.arbitrary": 1,
45 | "data.publicKeys.wallet": 1,
46 | "followingUsers.pub": 1,
47 | "subscribedTags": 1
48 | },
49 | "state": {
50 | "name": 1
51 | },
52 | "votes": {
53 | "pub": 1,
54 | "uuid": 1
55 | },
56 | "directmsgs": {
57 | "time": 1,
58 | "senderPublicKey": 1,
59 | "friendPublicKey": 1,
60 | },
61 | "posts": {
62 | "id": -1,
63 | "block": -1,
64 | "transaction": 1,
65 | "chain": 1,
66 | "createdAt": -1,
67 | "poster": 1,
68 | "pub": 1,
69 | "sub": 1,
70 | "tags": 1,
71 | "threadUuid": 1,
72 | "uuid": 1,
73 | "mentions": 1,
74 | "$text": ["searchMeta", "content"]
75 | },
76 | "moderation": {
77 | "pub": 1,
78 | "uuid": 1,
79 | "tags": 1,
80 | }
81 | }
82 | }
83 |
84 | Object.assign(config, getConfig(`mongo`));
85 |
86 | export default config;
--------------------------------------------------------------------------------
/src/server/mongo/index.js:
--------------------------------------------------------------------------------
1 | import { MongoClient } from 'mongodb';
2 | import config from './config';
3 | import { getFromCache } from "@/novusphere-js/utility";
4 |
5 | let cache = {};
6 |
7 | async function getMongo() {
8 | return getFromCache(cache, 'mongo', async () => {
9 | console.log(`Trying to connect to ${config.connection}`);
10 | const mongo = await MongoClient.connect(config.connection, { useNewUrlParser: true, useUnifiedTopology: true });
11 |
12 | // set up the indexes
13 | const database = await mongo.db(config.database);
14 | for (let collection in config.index) {
15 | let indexes = config.index[collection];
16 | for (let name in indexes) {
17 | let action = {};
18 | if (name == "$text") {
19 | indexes[name].forEach(field => action[field] = "text");
20 | }
21 | else {
22 | action[name] = indexes[name];
23 | }
24 |
25 | console.log(`Index ${collection} ${JSON.stringify(action)}`);
26 | await database.collection(collection).createIndex(action);
27 | }
28 | }
29 |
30 | return mongo;
31 | });
32 | }
33 |
34 | async function getDatabase(name) {
35 | name = name || config.database;
36 |
37 | return getFromCache(cache, `database_${name}`, async () => {
38 | const mongo = await getMongo();
39 | const database = await mongo.db(name);
40 | return database;
41 | });
42 | }
43 |
44 | async function getCollection(name) {
45 | const nameData = name.split('::');
46 | if (nameData.length > 1) {
47 | let database = await getDatabase(nameData[0]);
48 | return database.collection(nameData[1]);
49 | }
50 | else {
51 | let database = await getDatabase();
52 | return database.collection(nameData[0]);
53 | }
54 | }
55 |
56 | async function connectDatabase() {
57 | try {
58 | await getDatabase();
59 | return true;
60 | }
61 | catch (ex) {
62 | console.error(ex);
63 | return false;
64 | }
65 | }
66 |
67 | export {
68 | config,
69 | getDatabase,
70 | getCollection,
71 | connectDatabase
72 | }
--------------------------------------------------------------------------------
/src/server/services/EOSContractService.js:
--------------------------------------------------------------------------------
1 | import { config, getDatabase, getCollection } from "../mongo";
2 | import { sleep } from "@/novusphere-js/utility";
3 |
4 | export default class EOSContractService {
5 | constructor(table, contract, chain, childService) {
6 | this.dispatch = {};
7 | this.updates = {};
8 | this.table = table;
9 | this.chain = chain;
10 | this.childService = childService;
11 | this.DEFAULT_STATE = {
12 | name: contract,
13 | id: 0,
14 | block: 0,
15 | time: 0
16 | };
17 | }
18 |
19 | async getState() {
20 | const db = await getDatabase();
21 | let state = await db.collection(config.table.state)
22 | .findOne({ name: `${this.chain}::${this.DEFAULT_STATE.name}` });
23 |
24 | if (!state) {
25 | state = {};
26 | Object.assign(state, this.DEFAULT_STATE);
27 | }
28 |
29 | return state;
30 | }
31 |
32 | async updateState({ id, block, time }) {
33 | const name = `${this.chain}::${this.DEFAULT_STATE.name}`;
34 | const db = await getDatabase();
35 | await db.collection(config.table.state)
36 | .updateOne(
37 | { name },
38 | {
39 | $setOnInsert: { name },
40 | $set: { id, block, time }
41 | },
42 | { upsert: true });
43 | }
44 |
45 | sanitizeAction(action) {
46 | if (!action.data) action.data = {};
47 | if (action.data.metadata) {
48 | if (typeof (action.data.metadata) == "string") {
49 | try { action.data.metadata = JSON.parse(action.data.metadata); }
50 | catch (ex) { action.data.metadata = {}; }
51 | }
52 | }
53 | else {
54 | action.data.metadata = {};
55 | }
56 | return action;
57 | }
58 |
59 | pushUpdate(table, update) {
60 | let updates = this.updates[table];
61 | if (!updates) {
62 | updates = [];
63 | this.updates[table] = updates;
64 | }
65 | updates.push(update);
66 | }
67 |
68 | async commit() {
69 | const db = await getDatabase();
70 | for (const table in this.updates) {
71 | const updates = this.updates[table];
72 | if (updates && updates.length > 0) {
73 | await db.command({
74 | update: table,
75 | updates: updates
76 | });
77 | }
78 | }
79 | this.updates = {};
80 | }
81 |
82 | async getActionCollection() {
83 | const collection = await getCollection(this.table);
84 | return collection;
85 | }
86 |
87 | async migration() {
88 | }
89 |
90 | async tick() {
91 | const dispatch = this.dispatch;
92 | const state = await this.getState();
93 | console.log(`[${new Date().toLocaleTimeString()}] [${this.chain}::${this.DEFAULT_STATE.name}] state position = ${new Date(state.time).toLocaleString()}`);
94 |
95 | let actionCollection = await this.getActionCollection();
96 | let actions = (await actionCollection
97 | .find({
98 | id: { $gt: state.id },
99 | account: this.DEFAULT_STATE.name,
100 | chain: this.chain
101 | })
102 | .sort({ id: 1 })
103 | .limit(100)
104 | .toArray())
105 | .map(a => this.sanitizeAction(a));
106 |
107 | for (let i = 0; i < actions.length; i++) {
108 | const action = actions[i];
109 | const dispatcher = dispatch[action.name];
110 | if (dispatcher) {
111 | await dispatcher.apply(this, [action]);
112 | }
113 |
114 | if (i > 0 && actions[i].transaction != actions[i - 1].transaction) {
115 | await this.commit();
116 | }
117 | }
118 |
119 | await this.commit();
120 |
121 | if (actions.length > 0) {
122 | await this.updateState(actions[actions.length - 1]);
123 | }
124 | else {
125 | // child service can tick, since we're up to date here
126 | if (this.childService) {
127 | await this.childService.tick();
128 | }
129 | }
130 | }
131 |
132 | async start() {
133 | await this.migration();
134 | for (; ;) {
135 | await this.tick();
136 | await sleep(1000);
137 | }
138 | }
139 | }
--------------------------------------------------------------------------------
/src/server/services/index.js:
--------------------------------------------------------------------------------
1 | import discussions from "./discussions";
2 | import analytics from "./analytics";
3 | import { config } from "../mongo";
4 |
5 | function start() {
6 | discussions.start();
7 | analytics.start();
8 | }
9 |
10 | export default {
11 | start
12 | }
--------------------------------------------------------------------------------
/src/server/site.js:
--------------------------------------------------------------------------------
1 | export default {
2 | "port": 8008,
3 | "rendertron": "http://localhost:8010/render",
4 | "title": "Discussions",
5 | "description": "An open forum for discussions built on blockchain. Supporting and nurturing token communities of all kind",
6 | "image": "/static/banner.png",
7 | "url": "https://discussions.app",
8 | "domain": "discussions.app", // this is a context field for many of the database contexts
9 | "testerPublicKeys": [
10 | 'EOS5FcwE6haZZNNTR6zA3QcyAwJwJhk53s7UjZDch1c7QgydBWFSe', // xia256
11 | 'EOS66mZsNtdEVeFfxrxkZ9sZ5snwTPYmtRnEtHWhpyFovfnvDnCM5', // xia512
12 | 'EOS5epmzy9PGex6uS6r6UzcsyxYhsciwjMdrx1qbtF51hXhRjnYYH', // jacques
13 | 'EOS6sYMyMHzHhGtfwjCcZkRaw3YK5ws8xoD6ke2DNUmnHT3j1cpjV', // brain
14 | 'EOS7RWM4YvxcUEhZfHozf8XVajgvfh8wvohoJS9vx8Hg1K12PDx5o', // paul
15 | ],
16 | "relay": {
17 | "account": "",
18 | "key": "",
19 | "pub": "EOS7YrEup7dQ82v3wF3RjxXe7RG3yosz1SXVRjMwgT1fdHsgHZ8MJ"
20 | },
21 | "trustedRelay": [
22 | "EOS7YrEup7dQ82v3wF3RjxXe7RG3yosz1SXVRjMwgT1fdHsgHZ8MJ"
23 | ],
24 | "botUserAgents": [
25 | 'Baiduspider',
26 | 'bingbot',
27 | 'Discordbot',
28 | 'Embedly',
29 | 'facebookexternalhit',
30 | 'Googlebot',
31 | 'LinkedInBot',
32 | 'outbrain',
33 | 'pinterest',
34 | 'quora link preview',
35 | 'rogerbot',
36 | 'redditbot',
37 | 'showyoubot',
38 | 'Slackbot',
39 | 'TelegramBot',
40 | 'Twitterbot',
41 | 'vkShare',
42 | 'W3C_Validator',
43 | 'WhatsApp',
44 | ]
45 | }
--------------------------------------------------------------------------------
/src/server/sitemap.js:
--------------------------------------------------------------------------------
1 | import { getCommunities } from "@/novusphere-js/discussions/api";
2 | import { getConfig } from "@/novusphere-js/utility";
3 | import { argv } from 'yargs';
4 | import VueRouterSitemap from 'vue-router-sitemap';
5 | import path from 'path';
6 | import createRoutes from "./routes";
7 | import siteConfig from "./site";
8 |
9 | if (argv.config) {
10 | console.log(`Updating site settings from config: ${argv.config}`);
11 | Object.assign(siteConfig, getConfig(argv.config));
12 | }
13 |
14 | function unwindRoutes(routes, basePath = '') {
15 | let result = [];
16 |
17 | for (const { path, children } of routes) {
18 | if (!children) {
19 | const fp = `${basePath}${path}`;
20 | if (fp.indexOf(':') == -1 && fp.indexOf('/tests') != 0)
21 | result.push({ path: fp });
22 | }
23 | else {
24 | let p = path;
25 | if (p.lastIndexOf('/') != p.length - 1)
26 | p += '/';
27 | result.push(...unwindRoutes(children, `${basePath}${p}`));
28 | }
29 | }
30 |
31 | return result;
32 | }
33 |
34 | (async function () {
35 | const communities = await getCommunities();
36 |
37 | const routes = unwindRoutes(createRoutes());
38 | routes.push(...communities.map(comm => ({ path: `/tag/${comm.tag}` })));
39 | console.log(routes);
40 |
41 | const staticSitemap = path.resolve('./public/', 'sitemap.xml');
42 | new VueRouterSitemap({ options: { routes } })
43 | .build(siteConfig.url)
44 | .save(staticSitemap);
45 |
46 | console.log(`${staticSitemap} created`);
47 |
48 | })();
--------------------------------------------------------------------------------
/src/utility.js:
--------------------------------------------------------------------------------
1 | import { mapGetters, mapState } from "vuex";
2 | import { waitFor } from "@/novusphere-js/utility";
3 | import { encrypt, decrypt, brainKeyToKeys, isValidBrainKey, findInvalidBrainKeyWord } from "@/novusphere-js/uid";
4 |
5 | //
6 | // Wrap a Vue component export to watch for isLoggedIn and require it, otherwise redirect
7 | //
8 | function requireLoggedIn(component) {
9 | let created = component.created;
10 | let watch = component.watch;
11 | let computed = component.computed;
12 |
13 | component.created = async function () {
14 | if (this.needSyncAccount) {
15 | await waitFor(async () => !this.needSyncAccount, 100);
16 | }
17 | if (!this.isLoggedIn) this.$router.push(`/`);
18 | else if (created) created.apply(this, arguments);
19 | };
20 |
21 | component.computed = {
22 | ...computed,
23 | ...mapGetters(["isLoggedIn"]),
24 | ...mapState({
25 | needSyncAccount: state => state.needSyncAccount
26 | })
27 | };
28 |
29 | component.watch = {
30 | ...watch,
31 | isLoggedIn() {
32 | if (!this.isLoggedIn) this.$router.push(`/`);
33 | if (watch && watch.isLoggedIn) watch.isLoggedIn.apply(this, arguments);
34 | }
35 | };
36 |
37 | return component;
38 | }
39 |
40 | function displayNameRules(displayName) {
41 | return {
42 | displayNameRules() {
43 | const rules = [];
44 | if (this[displayName].length < 3) {
45 | rules.push(`Display name must be at least 3 characters`);
46 | }
47 |
48 | if (this[displayName].length > 16) {
49 | rules.push(`Display names can be at most 16 characters`);
50 | }
51 |
52 | const validNameRegex = /[a-zA-Z0-9_]+/g;
53 | const match = this[displayName].match(validNameRegex);
54 | if (!match || match[0] != this[displayName]) {
55 | rules.push(
56 | `Display names may only contain letters, numbers, underscores`
57 | );
58 | }
59 | return rules;
60 | }
61 | }
62 | }
63 |
64 | function passwordRules(password) {
65 | return {
66 | passwordRules() {
67 | const rules = [];
68 | if (this[password].length < 5) {
69 | rules.push(`Password must be at least 5 characters`);
70 | }
71 | return rules;
72 | }
73 | }
74 | }
75 |
76 | function passwordTesterRules(password, encryptedTest) {
77 | return {
78 | passwordTesterRules() {
79 | const rules = [];
80 | if (decrypt(this[encryptedTest], this[password]) != "test") {
81 | rules.push(`Password is incorrect`);
82 | }
83 | return rules;
84 | }
85 | }
86 | }
87 |
88 | function brainKeyRules(brainKey) {
89 | return {
90 | brainKeyRules() {
91 | const rules = [];
92 | const bkValue = this[brainKey];
93 |
94 | if (!isValidBrainKey(bkValue)) {
95 | const invalidWord = findInvalidBrainKeyWord(bkValue);
96 | if (invalidWord)
97 | rules.push(`Invalid brain key mnemonic - "${invalidWord}" may be invalid or spelled incorrectly`);
98 | else
99 | rules.push(`Invalid brain key mnemonic`);
100 | }
101 |
102 | return rules;
103 | }
104 | }
105 | }
106 |
107 | async function createLoginObject({ displayName, brainKey, password }) {
108 | const keys = await brainKeyToKeys(brainKey);
109 |
110 | return {
111 | encryptedBrainKey: encrypt(brainKey, password),
112 | encryptedTest: encrypt("test", password),
113 | displayName: displayName,
114 | keys: keys
115 | };
116 | }
117 |
118 | function getBuildVersion() {
119 | let build = '';
120 | if (window.__BUILD__) {
121 | build = `S-${new Date(window.__BUILD__).getTime().toString(16)}`;
122 | } else {
123 | build = `C-${new Date().getTime().toString(16)}`;
124 | }
125 | return build;
126 | }
127 |
128 | export {
129 | requireLoggedIn,
130 | displayNameRules,
131 | passwordRules,
132 | passwordTesterRules,
133 | brainKeyRules,
134 | createLoginObject,
135 | getBuildVersion
136 | }
--------------------------------------------------------------------------------
/src/watcher/greymass.js:
--------------------------------------------------------------------------------
1 | import { sleep } from "@/novusphere-js/utility";
2 | import axios from 'axios';
3 |
4 | export default class GreymassWatcher {
5 | constructor(endpoint, name) {
6 | this.name = name || 'greymass';
7 | this._endpoint = endpoint || `https://eos.greymass.com`;
8 | }
9 |
10 | async getPreviousAction(chain, account, collection) {
11 | return await collection
12 | .find({ account, chain })
13 | .sort({ position: -1 })
14 | .limit(1)
15 | .next()
16 | || { block: -1, position: -1 };
17 | }
18 |
19 | async startWatch(account, previousAction, onAction) {
20 | try {
21 | for (; ;) {
22 |
23 | const { data } = await axios.post(`${this._endpoint}/v1/history/get_actions`,
24 | JSON.stringify({
25 | account_name: account,
26 | pos: previousAction.position + 1,
27 | offset: 100
28 | }),
29 | {
30 | headers: { 'Content-Type': 'application/json' },
31 | timeout: 10000 // 10s timeout
32 | });
33 |
34 | const actions = data.actions.map((a, i) => {
35 | const { trx_id, act } = a.action_trace;
36 |
37 | let block_time = a.block_time;
38 | if (typeof block_time == 'string') {
39 | if (!block_time.endsWith('Z'))
40 | block_time = `${block_time}Z`; // UTC
41 | }
42 |
43 | return {
44 | id: Number(a.global_action_seq),
45 | position: a.account_action_seq,
46 | account: act.account,
47 | auth: act.authorization.map(auth => auth.actor),
48 | transaction: trx_id,
49 | block: a.block_num,
50 | time: new Date(block_time).getTime(),
51 | name: act.name,
52 | hexData: act.hex_data,
53 | data: act.data
54 | };
55 | });
56 |
57 | for (const action of actions) {
58 | if (onAction) {
59 | onAction(action);
60 | }
61 | if (!previousAction || action.position > previousAction.position) {
62 | previousAction = action;
63 | //console.log(`GM position set to ${account}@${previousAction.position}`);
64 | }
65 | }
66 |
67 | await sleep(2500);
68 | }
69 | }
70 | catch (ex) {
71 | console.error(`Greymass error for ${account}`, ex);
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/src/watcher/index.js:
--------------------------------------------------------------------------------
1 | import { getConfig, sleep } from "@/novusphere-js/utility";
2 | import DfuseWatcher from "./dfuse";
3 | import GreymassWatcher from "./greymass";
4 | import siteConfig from "../server/site";
5 | import { connectDatabase, getCollection, config } from "../server/mongo";
6 |
7 | async function startActionWriter(chain, contract, table, watcher) {
8 | try {
9 | const collection = await getCollection(table);
10 |
11 | let previousAction = await watcher.getPreviousAction(chain, contract, collection);
12 |
13 | let actions = [];
14 | watcher.startWatch(contract, previousAction, (action) => {
15 | try {
16 | // try to decode metadata json
17 | if (action.data && action.data.metadata) {
18 | action.data.metadata = JSON.parse(action.data.metadata);
19 | }
20 | }
21 | catch (ex) {
22 | // if we failed, nbd
23 | }
24 | actions.push(action);
25 | });
26 |
27 | for (; ;) {
28 | const consumedActions = [...actions];
29 | actions = [];
30 |
31 | if (consumedActions.length > 0) {
32 |
33 | const write = consumedActions.map(action => ({
34 | updateOne: {
35 | filter: { transaction: action.transaction, name: action.name, hexData: action.hexData },
36 | update: { $set: { ...action, chain } },
37 | upsert: true
38 | }
39 | }));
40 |
41 | await collection.bulkWrite(write);
42 |
43 | previousAction = consumedActions.reduce((a1, a2) => a1.block > a2.block ? a1 : a2, consumedActions[0]);
44 | console.log(`Consumed ${consumedActions.length} actions for ${contract}@${watcher.name} at ${new Date(previousAction.time).toLocaleString()}`);
45 | }
46 | else {
47 | console.log(`Idle for ${contract}@${watcher.name} at ${new Date(previousAction.time).toLocaleString()}, p=${previousAction.position}, id=${previousAction.transaction}`);
48 | }
49 |
50 | await sleep(1000);
51 | }
52 | }
53 | catch (ex) {
54 | console.error(`Error with watcher for ${contract}`, ex);
55 | }
56 | }
57 |
58 | (async function () {
59 | if (!await connectDatabase()) return;
60 |
61 | Object.assign(siteConfig, getConfig(`watcher`));
62 |
63 | const eosDfuse = new DfuseWatcher(); // eos nation, eos
64 | const eosGreymass = new GreymassWatcher('https://eos.greymass.com', 'eos');
65 | const telosGreymass = new GreymassWatcher('https://telos.greymass.com', 'telos');
66 |
67 | // dfuse
68 | startActionWriter('eos', config.contract.discussions, config.table.discussions, eosDfuse);
69 | startActionWriter('eos', config.contract.uid, config.table.uid, eosDfuse);
70 |
71 | // gm
72 | startActionWriter('eos', config.contract.discussions, config.table.discussions, eosGreymass);
73 | startActionWriter('eos', config.contract.uid, config.table.uid, eosGreymass);
74 |
75 | // gm - telos
76 | startActionWriter('telos', config.contract.discussions, config.table.discussions, telosGreymass);
77 | startActionWriter('telos', config.contract.uid, config.table.uid, telosGreymass);
78 |
79 | // kill process on timer expected to be restarted by manager
80 | if (siteConfig.exit) {
81 | setTimeout(() => process.exit(), siteConfig.exit);
82 | }
83 |
84 | })();
85 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const ThreadsPlugin = require('threads-plugin');
2 |
3 | module.exports = {
4 | "configureWebpack": {
5 | plugins: [
6 | new ThreadsPlugin()
7 | ]
8 | },
9 | "transpileDependencies": [
10 | "vuetify"
11 | ]
12 | }
--------------------------------------------------------------------------------