From 694dce682083e810f9ed40bad2cb647517eb2e41 Mon Sep 17 00:00:00 2001 From: PeterMPhillips Date: Thu, 21 May 2020 15:55:19 -0700 Subject: [PATCH 1/2] feat: update Discussions app to support 3Box threads --- apps/discussions/app/ipfs/index.js | 25 ++++++-- apps/discussions/app/script.js | 28 +++++++-- apps/discussions/app/state/index.js | 1 + apps/discussions/app/state/initialState.js | 3 +- apps/discussions/app/state/threads.js | 62 ++++++++++++++++++++ apps/discussions/arapp.json | 9 ++- apps/discussions/contracts/DiscussionApp.sol | 42 ++++++++++++- apps/discussions/package.json | 2 +- apps/discussions/public/meta/icon.svg | 11 +++- package.json | 2 +- templates/dev/contracts/BaseCache.sol | 2 +- templates/dev/contracts/BaseOEApps.sol | 5 +- templates/dev/contracts/DevTemplate.sol | 2 +- 13 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 apps/discussions/app/state/threads.js diff --git a/apps/discussions/app/ipfs/index.js b/apps/discussions/app/ipfs/index.js index a7500eb36..4d8710222 100644 --- a/apps/discussions/app/ipfs/index.js +++ b/apps/discussions/app/ipfs/index.js @@ -1,7 +1,22 @@ +import axios from 'axios' import ipfsClient from 'ipfs-http-client' -export const ipfs = ipfsClient({ - host: 'ipfs.autark.xyz', - port: '5001', - protocol: 'https', -}) +const environments = { + development: { host: 'localhost', port: '5001', protocol: 'http' }, + production: { host: 'ipfs.autark.xyz', port: '5001', protocol: 'https' }, + staging: { host: 'ipfs.autark.xyz', port: '5001', protocol: 'https' }, +} + +const config = environments[process.env.NODE_ENV] + +export const ipfs = ipfsClient(config) + +export const ipfsGet = async hash => { + const endpoint = `${config.protocol}://${config.host}:8080/ipfs/${hash}` + try { + const { data } = await axios.get(endpoint) + return data + } catch (err) { + console.error('Error getting data from IPFS', err) + } +} diff --git a/apps/discussions/app/script.js b/apps/discussions/app/script.js index 45bd92f0d..ef1566295 100644 --- a/apps/discussions/app/script.js +++ b/apps/discussions/app/script.js @@ -2,7 +2,14 @@ import '@babel/polyfill' import { of } from 'rxjs' import AragonApi from '@aragon/api' -import { handleHide, handlePost, handleRevise, initialState } from './state' +import { + updateThread, + deleteThread, + handleHide, + handlePost, + handleRevise, + initialState +} from './state' const INITIALIZATION_TRIGGER = Symbol('INITIALIZATION_TRIGGER') @@ -13,17 +20,28 @@ api.store( let newState switch (event.event) { case INITIALIZATION_TRIGGER: - newState = { ...initialState, syncing: false } + newState = { ...initialState, isSyncing: false } return newState case 'SYNC_STATUS_SYNCING': - newState = { ...initialState, syncing: true } + newState = { ...initialState, isSyncing: true } return newState case 'SYNC_STATUS_SYNCED': - newState = { ...initialState, syncing: false } + newState = { ...initialState, isSyncing: false } return newState case 'ACCOUNTS_TRIGGER': - newState = { ...initialState, syncing: false } + newState = { ...initialState, isSyncing: false } return newState + case 'UpdateThread': + return { + ...state, + threads: await updateThread(state, event.returnValues), + } + case 'DeleteThread': + const threads = await deleteThread(state, event.returnValues) + return { + ...state, + threads, + } case 'Post': newState = await handlePost(state, event) return newState diff --git a/apps/discussions/app/state/index.js b/apps/discussions/app/state/index.js index 47f38a278..d3cc80b84 100644 --- a/apps/discussions/app/state/index.js +++ b/apps/discussions/app/state/index.js @@ -1,2 +1,3 @@ export * from './initialState' export * from './updateState' +export * from './threads' diff --git a/apps/discussions/app/state/initialState.js b/apps/discussions/app/state/initialState.js index 84a0ec732..0d84cdfda 100644 --- a/apps/discussions/app/state/initialState.js +++ b/apps/discussions/app/state/initialState.js @@ -1,4 +1,5 @@ export const initialState = { + threads: [], discussions: {}, - syncing: true, + isSyncing: true, } diff --git a/apps/discussions/app/state/threads.js b/apps/discussions/app/state/threads.js new file mode 100644 index 000000000..437d4e316 --- /dev/null +++ b/apps/discussions/app/state/threads.js @@ -0,0 +1,62 @@ +import { ipfsGet } from '../ipfs' + +export const newThread = async (state, { thread, metadata }) => { + const { threads = [] } = state + const { + name, + title, + description, + creationDate, + context, + author, + } = await ipfsGet(metadata) + threads.push({ + address: thread, + name, + title, + description, + context, + creationDate: new Date(creationDate), + author, + }) + return threads +} + +export const editThread = async (state, { thread, metadata }) => { + const { threads = [] } = state + const { description } = await ipfsGet(metadata) + const index = threads.findIndex(t => t.address === thread) + if (index > -1) threads[index].description = description + return threads +} + +export const updateThread = async (state, { thread, metadata }) => { + const { threads = [] } = state + const { + name, + title, + description, + creationDate, + context, + author, + } = await ipfsGet(metadata) + const index = threads.findIndex(t => t.address === thread) + if (index > -1) threads[index].description = description + else { + threads.push({ + address: thread, + name, + title, + description, + context, + creationDate: new Date(creationDate), + author, + }) + } + return threads +} + +export const deleteThread = async (state, { thread }) => { + const { threads = [] } = state + return threads.filter(t => t.address !== thread) +} diff --git a/apps/discussions/arapp.json b/apps/discussions/arapp.json index e9e983577..a04e29e70 100644 --- a/apps/discussions/arapp.json +++ b/apps/discussions/arapp.json @@ -1,8 +1,13 @@ { "roles": [ { - "name": "Is doing nothing, just needed to technically be an AragonApp", - "id": "EMPTY_ROLE", + "name": "Register role", + "id": "REGISTER_ROLE", + "params": [] + }, + { + "name": "Moderator role", + "id": "MODERATOR_ROLE", "params": [] } ], diff --git a/apps/discussions/contracts/DiscussionApp.sol b/apps/discussions/contracts/DiscussionApp.sol index 7b69881b2..fdf4bc1cf 100644 --- a/apps/discussions/contracts/DiscussionApp.sol +++ b/apps/discussions/contracts/DiscussionApp.sol @@ -19,10 +19,16 @@ contract DiscussionApp is IForwarder, AragonApp { ); event Hide(address indexed author, uint256 discussionThreadId, uint256 postId, uint256 hiddenAt); event CreateDiscussionThread(uint256 actionId, bytes _evmScript); + event UpdateThread(string thread, string metadata); + event DeleteThread(string thread); - bytes32 public constant EMPTY_ROLE = keccak256("EMPTY_ROLE"); + bytes32 public constant REGISTER_ROLE = keccak256("REGISTER_ROLE"); + bytes32 public constant MODERATOR_ROLE = keccak256("MODERATOR_ROLE"); string private constant ERROR_CAN_NOT_FORWARD = "DISCUSSIONS_CAN_NOT_FORWARD"; + string private constant ERROR_ALREADY_EXISTS = "You cannot create an existing thread."; + string private constant ERROR_EDIT_NOT_AUTHOR = "You cannot edit a thread you did not author."; + string private constant ERROR_DELETE_NOT_AUTHOR = "You cannot delete a thread you did not author."; struct DiscussionPost { address author; @@ -38,6 +44,8 @@ contract DiscussionApp is IForwarder, AragonApp { mapping(uint256 => DiscussionPost[]) public discussionThreadPosts; + mapping(string => address) private threadAuthors; + function initialize() external onlyInit { discussionThreadId = 0; initialized(); @@ -96,6 +104,38 @@ contract DiscussionApp is IForwarder, AragonApp { ); } + /** + * @notice Create new thread at `thread` + * @param thread The address of the 3Box thread + * @param metadata The IPFS hash of the metadata for the thread + */ + function registerThread(string thread, string metadata) + auth(REGISTER_ROLE) external { + require(threadAuthors[thread] == 0, ERROR_ALREADY_EXISTS); + threadAuthors[thread] = msg.sender; + emit UpdateThread(thread, metadata); + } + + /** + * @notice Edit thread at `thread` + * @param thread The address of the 3Box thread + * @param newMetadata The IPFS hash of the new metadata for the thread + */ + function editThread(string thread, string newMetadata) + auth(REGISTER_ROLE) external { + require(threadAuthors[thread] == msg.sender, ERROR_EDIT_NOT_AUTHOR); + emit UpdateThread(thread, newMetadata); + } + + /** + * @notice Delete thread at `thread` + * @param thread The address of the 3Box thread + */ + function deleteThread(string thread) auth(REGISTER_ROLE) external { + require(threadAuthors[thread] == msg.sender, ERROR_DELETE_NOT_AUTHOR); + emit DeleteThread(thread); + } + // Forwarding fns /** diff --git a/apps/discussions/package.json b/apps/discussions/package.json index 6b53ae23f..2be9cb4ad 100644 --- a/apps/discussions/package.json +++ b/apps/discussions/package.json @@ -46,7 +46,7 @@ "clean:start": "aragon clean && rm -rf ~/.aragon && npm run start:http:template", "start": "npm run start:ipfs", "start:ipfs": "aragon run", - "start:http": "aragon run --http localhost:8001 --http-served-from ./dist", + "start:http": "NODE_ENV=development aragon run --http localhost:8001 --http-served-from ./dist", "start:ipfs:template": "npm run start:ipfs -- --template Template --template-init @ARAGON_ENS", "start:http:template": "rm -rf build && npm run start:http -- --template Template --template-init @ARAGON_ENS", "start:app": "cd app && npm start && cd ..", diff --git a/apps/discussions/public/meta/icon.svg b/apps/discussions/public/meta/icon.svg index 546d85afe..394a0917f 100644 --- a/apps/discussions/public/meta/icon.svg +++ b/apps/discussions/public/meta/icon.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + + + + diff --git a/package.json b/package.json index f639af030..c9d3db482 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "start:dev-template": "cd templates/dev && npm run publish:aragen && npm run start:template:aragen", "start:address": "cd apps/address-book && npm start", "start:allocations": "cd apps/allocations && npm start", - "start:dev": "DEV=true apps/shared/test-helpers/ganache-cli.sh", + "start:dev": "NODE_ENV=development DEV=true apps/shared/test-helpers/ganache-cli.sh", "start:no:client": "NO_CLIENT=true apps/shared/test-helpers/ganache-cli.sh", "start:dot": "cd apps/dot-voting && npm start", "start:projects": "cd apps/projects && npm start", diff --git a/templates/dev/contracts/BaseCache.sol b/templates/dev/contracts/BaseCache.sol index 67d5ec392..d0b32907a 100644 --- a/templates/dev/contracts/BaseCache.sol +++ b/templates/dev/contracts/BaseCache.sol @@ -23,7 +23,7 @@ contract BaseCache is BaseTemplate { ENS(_deployedSetupContracts[1]), MiniMeTokenFactory(_deployedSetupContracts[2]), IFIFSResolvingRegistrar(_deployedSetupContracts[3]) - ) {} + ) public {} function _cacheBase( ACL _acl, diff --git a/templates/dev/contracts/BaseOEApps.sol b/templates/dev/contracts/BaseOEApps.sol index b7638127d..bb65f8542 100644 --- a/templates/dev/contracts/BaseOEApps.sol +++ b/templates/dev/contracts/BaseOEApps.sol @@ -123,11 +123,12 @@ contract BaseOEApps is BaseCache, TokenCache { /* DISCUSSIONS */ function _installDiscussionsApp(Kernel _dao) internal returns (DiscussionApp) { - return DiscussionApp(_installNonDefaultApp(_dao, DISCUSSIONS_APP_ID)); + bytes memory initializeData = abi.encodeWithSelector(DiscussionApp(0).initialize.selector); + return DiscussionApp(_installNonDefaultApp(_dao, DISCUSSIONS_APP_ID, initializeData)); } function _createDiscussionsPermissions(ACL _acl, DiscussionApp _discussions, address _grantee, address _manager) internal { - _acl.createPermission(_grantee, _discussions, _discussions.EMPTY_ROLE(), _manager); + _acl.createPermission(_grantee, _discussions, _discussions.REGISTER_ROLE(), _manager); } /* PROJECTS */ diff --git a/templates/dev/contracts/DevTemplate.sol b/templates/dev/contracts/DevTemplate.sol index ebb0f9afd..9189b9fc2 100644 --- a/templates/dev/contracts/DevTemplate.sol +++ b/templates/dev/contracts/DevTemplate.sol @@ -160,7 +160,7 @@ contract DevTemplate is BaseOEApps { { if (_useDiscussions) { DiscussionApp discussions = _installDiscussionsApp(_dao); - _createDiscussionsPermissions(_acl, discussions, ANY_ENTITY, _voting); + _createDiscussionsPermissions(_acl, discussions, _tokenManager, _voting); } MiniMeToken token = _popTokenCache(msg.sender); From 723ff630ebcae492dcbcb70d847f6480269fb242 Mon Sep 17 00:00:00 2001 From: PeterMPhillips Date: Wed, 27 May 2020 14:13:48 -0700 Subject: [PATCH 2/2] fix: update discussions permissions --- apps/discussions/app/script.js | 13 ++--- apps/discussions/app/state/threads.js | 76 ++++++++++--------------- package.json | 2 +- templates/dev/contracts/BaseOEApps.sol | 5 +- templates/dev/contracts/DevTemplate.sol | 13 +++-- 5 files changed, 45 insertions(+), 64 deletions(-) diff --git a/apps/discussions/app/script.js b/apps/discussions/app/script.js index ef1566295..13a07ac77 100644 --- a/apps/discussions/app/script.js +++ b/apps/discussions/app/script.js @@ -32,16 +32,11 @@ api.store( newState = { ...initialState, isSyncing: false } return newState case 'UpdateThread': - return { - ...state, - threads: await updateThread(state, event.returnValues), - } + newState = await updateThread(state, event.returnValues) + return newState case 'DeleteThread': - const threads = await deleteThread(state, event.returnValues) - return { - ...state, - threads, - } + newState = await deleteThread(state, event.returnValues) + return newState case 'Post': newState = await handlePost(state, event) return newState diff --git a/apps/discussions/app/state/threads.js b/apps/discussions/app/state/threads.js index 437d4e316..d419cc89e 100644 --- a/apps/discussions/app/state/threads.js +++ b/apps/discussions/app/state/threads.js @@ -1,62 +1,44 @@ import { ipfsGet } from '../ipfs' -export const newThread = async (state, { thread, metadata }) => { - const { threads = [] } = state - const { - name, - title, - description, - creationDate, - context, - author, - } = await ipfsGet(metadata) - threads.push({ - address: thread, - name, - title, - description, - context, - creationDate: new Date(creationDate), - author, - }) - return threads -} - -export const editThread = async (state, { thread, metadata }) => { - const { threads = [] } = state - const { description } = await ipfsGet(metadata) - const index = threads.findIndex(t => t.address === thread) - if (index > -1) threads[index].description = description - return threads -} - export const updateThread = async (state, { thread, metadata }) => { const { threads = [] } = state - const { - name, - title, - description, - creationDate, - context, - author, - } = await ipfsGet(metadata) - const index = threads.findIndex(t => t.address === thread) - if (index > -1) threads[index].description = description - else { - threads.push({ - address: thread, + try { + const { name, title, description, + creationDate, context, - creationDate: new Date(creationDate), author, - }) + } = await ipfsGet(metadata) + const index = threads.findIndex(t => t.address === thread) + if (index > -1) threads[index].description = description + else { + threads.push({ + address: thread, + name, + title, + description, + context, + creationDate: new Date(creationDate), + author, + }) + } + return { + ...state, + threads + } + } catch (e) { + console.error(e) + return state } - return threads } export const deleteThread = async (state, { thread }) => { const { threads = [] } = state - return threads.filter(t => t.address !== thread) + const filteredThreads = threads.filter(t => t.address !== thread) + return { + ...state, + threads: filteredThreads + } } diff --git a/package.json b/package.json index c9d3db482..9689d78ed 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lerna": "^3.10.8", "truffle-privatekey-provider": "1.1.0", "wait-on": "^3.2.0", - "web3": "^1.2.4" + "web3": "1.2.7" }, "scripts": { "bootstrap": "lerna bootstrap --hoist", diff --git a/templates/dev/contracts/BaseOEApps.sol b/templates/dev/contracts/BaseOEApps.sol index bb65f8542..98e74983b 100644 --- a/templates/dev/contracts/BaseOEApps.sol +++ b/templates/dev/contracts/BaseOEApps.sol @@ -127,8 +127,9 @@ contract BaseOEApps is BaseCache, TokenCache { return DiscussionApp(_installNonDefaultApp(_dao, DISCUSSIONS_APP_ID, initializeData)); } - function _createDiscussionsPermissions(ACL _acl, DiscussionApp _discussions, address _grantee, address _manager) internal { - _acl.createPermission(_grantee, _discussions, _discussions.REGISTER_ROLE(), _manager); + function _createDiscussionsPermissions(ACL _acl, DiscussionApp _discussions, address _moderator, address _register, address _manager) internal { + _acl.createPermission(_moderator, _discussions, _discussions.MODERATOR_ROLE(), _manager); + _acl.createPermission(_register, _discussions, _discussions.REGISTER_ROLE(), _manager); } /* PROJECTS */ diff --git a/templates/dev/contracts/DevTemplate.sol b/templates/dev/contracts/DevTemplate.sol index 9189b9fc2..351cf33e1 100644 --- a/templates/dev/contracts/DevTemplate.sol +++ b/templates/dev/contracts/DevTemplate.sol @@ -158,11 +158,6 @@ contract DevTemplate is BaseOEApps { ) internal { - if (_useDiscussions) { - DiscussionApp discussions = _installDiscussionsApp(_dao); - _createDiscussionsPermissions(_acl, discussions, _tokenManager, _voting); - } - MiniMeToken token = _popTokenCache(msg.sender); AddressBook addressBook = _installAddressBookApp(_dao); Allocations allocations = _installAllocationsApp(_dao, _vault, _allocationsPeriod == 0 ? DEFAULT_PERIOD : _allocationsPeriod); @@ -170,6 +165,14 @@ contract DevTemplate is BaseOEApps { Projects projects = _installProjectsApp(_dao, _vault); Rewards rewards = _installRewardsApp(_dao, _vault); + if (_useDiscussions) { + DiscussionApp discussions = _installDiscussionsApp(_dao); + // _createDiscussionsPermissions(_acl, discussions, msg.sender, _tokenManager, _voting); // Stack too deep + _acl.createPermission(msg.sender, discussions, discussions.MODERATOR_ROLE(), _voting); + _acl.createPermission(_tokenManager, discussions, discussions.REGISTER_ROLE(), _voting); + } + + _setupOEPermissions( _acl, _tokenManager,