|
@@ -1,104 +1,437 @@
|
|
|
+const { Block } = require('../db/models')
|
|
|
|
|
|
-const addBlock = async (api, io, header, status) => {
|
|
|
- const id = Number(header.number)
|
|
|
- const exists = await Block.findByPk(id)
|
|
|
- if (exists) return
|
|
|
- const timestamp = (await api.query.timestamp.now()).toNumber()
|
|
|
- const last = await Block.findByPk(id - 1)
|
|
|
-
|
|
|
- const blocktime = last ? timestamp - last.timestamp : 6000
|
|
|
- const author = header.author?.toString()
|
|
|
-
|
|
|
- const block = await Block.create({ id,
|
|
|
- timestamp,
|
|
|
- blocktime,
|
|
|
- author
|
|
|
- })
|
|
|
- console.log(
|
|
|
- '[Joystream] block',
|
|
|
- block.id,
|
|
|
- block.blocktime,
|
|
|
- block.author
|
|
|
- )
|
|
|
- io.emit('block', block)
|
|
|
-
|
|
|
- update()
|
|
|
-}
|
|
|
-
|
|
|
-const processEvents = (blockHash) => {
|
|
|
- const blockEvents = api.query.system.events.at(
|
|
|
- blockHash
|
|
|
- ) as Vec<EventRecord>
|
|
|
- let transfers = blockEvents.filter((event) => {
|
|
|
- return event.section == 'balances' && event.method == 'Transfer'
|
|
|
- })
|
|
|
- let validatorRewards = blockEvents.filter((event) => {
|
|
|
- return event.section == 'staking' && event.method == 'Reward'
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const update = () => {
|
|
|
-const id = header.number.toNumber();
|
|
|
- if (blocks.find((b) => b.id === id)) return;
|
|
|
- const timestamp = (await api.query.timestamp.now()).toNumber();
|
|
|
- const duration = timestamp - lastBlock.timestamp;
|
|
|
- const block: Block = { id, timestamp, duration };
|
|
|
- blocks = blocks.concat(block);
|
|
|
- this.setState({ blocks, loading: false });
|
|
|
- this.save("block", id);
|
|
|
- this.save("now", timestamp);
|
|
|
-
|
|
|
- const proposalCount = await get.proposalCount(api);
|
|
|
- if (proposalCount > this.state.proposalCount) {
|
|
|
- this.fetchProposal(api, proposalCount);
|
|
|
- this.setState({ proposalCount });
|
|
|
- }
|
|
|
-
|
|
|
- const currentChannel = await get.currentChannelId(api);
|
|
|
- if (currentChannel > lastChannel)
|
|
|
- lastChannel = await this.fetchChannels(api, currentChannel);
|
|
|
-
|
|
|
- const currentCategory = await get.currentCategoryId(api);
|
|
|
- if (currentCategory > lastCategory)
|
|
|
- lastCategory = await this.fetchCategories(api, currentCategory);
|
|
|
-
|
|
|
- const currentPost = await get.currentPostId(api);
|
|
|
- if (currentPost > lastPost)
|
|
|
- lastPost = await this.fetchPosts(api, currentPost);
|
|
|
-
|
|
|
- const currentThread = await get.currentThreadId(api);
|
|
|
- if (currentThread > lastThread)
|
|
|
- lastThread = await this.fetchThreads(api, currentThread);
|
|
|
-
|
|
|
- const postCount = await api.query.proposalsDiscussion.postCount();
|
|
|
- this.setState({ proposalComments: Number(postCount) });
|
|
|
-
|
|
|
- lastBlock = block;
|
|
|
-
|
|
|
- // validators
|
|
|
- const currentEra = Number(await api.query.staking.currentEra());
|
|
|
- if (currentEra > era) {
|
|
|
- era = currentEra;
|
|
|
- this.fetchStakes(api, era, this.state.validators);
|
|
|
- this.save("era", era);
|
|
|
- this.fetchLastReward(api, era - 1);
|
|
|
- } else if (this.state.lastReward === 0)
|
|
|
- this.fetchLastReward(api, currentEra);
|
|
|
-
|
|
|
- this.fetchEraRewardPoints(api, Number(era));
|
|
|
-
|
|
|
- // check election stage
|
|
|
- if (id < termEndsAt || id < stageEndsAt) return;
|
|
|
- const json = stage.toJSON();
|
|
|
- const key = Object.keys(json)[0];
|
|
|
- stageEndsAt = json[key];
|
|
|
- //console.log(id, stageEndsAt, json, key);
|
|
|
-
|
|
|
- termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
|
|
|
- round = Number((await api.query.councilElection.round()).toJSON());
|
|
|
- stage = await api.query.councilElection.stage();
|
|
|
- councilElection = { termEndsAt, stage: stage.toJSON(), round };
|
|
|
- this.setState({ councilElection });
|
|
|
+const get = require('./lib/getters')
|
|
|
+const axios = require('axios')
|
|
|
+const moment = require('moment')
|
|
|
|
|
|
+import { VoteKind } from '@joystream/types/proposals'
|
|
|
+import {
|
|
|
+ Api,
|
|
|
+ Handles,
|
|
|
+ IState,
|
|
|
+ Member,
|
|
|
+ Category,
|
|
|
+ Channel,
|
|
|
+ Post,
|
|
|
+ Seat,
|
|
|
+ Thread,
|
|
|
+ ProposalDetail,
|
|
|
+ Status,
|
|
|
+} from '../types'
|
|
|
+
|
|
|
+const addBlock = async (
|
|
|
+ api: Api,
|
|
|
+ io: any,
|
|
|
+ header: { number: number; author: string },
|
|
|
+ status: Status
|
|
|
+): Promise<Status> => {
|
|
|
+ const id = +header.number
|
|
|
+ const exists = await Block.findByPk(id)
|
|
|
+ if (exists) return status
|
|
|
+
|
|
|
+ const timestamp = (await api.query.timestamp.now()).toNumber()
|
|
|
+
|
|
|
+ const last = await Block.findByPk(id - 1)
|
|
|
+
|
|
|
+ const blocktime = last ? timestamp - last.timestamp : 6000
|
|
|
+ const author = header.author?.toString()
|
|
|
+
|
|
|
+ const block = await Block.create({ id, timestamp, blocktime, author })
|
|
|
+ console.log('[Joystream] block', block.id, block.blocktime, block.author)
|
|
|
+ io.emit('block', block)
|
|
|
+ updateAll(api, io, status)
|
|
|
+ return status
|
|
|
+}
|
|
|
+
|
|
|
+const processEvents = (api: Api, blockHash) => {
|
|
|
+ const blockEvents = api.query.system.events.at(blockHash)
|
|
|
+ // TODO as Vec<EventRecord>
|
|
|
+ let transfers = blockEvents.filter((event) => {
|
|
|
+ return event.section == 'balances' && event.method == 'Transfer'
|
|
|
+ })
|
|
|
+ let validatorRewards = blockEvents.filter((event) => {
|
|
|
+ return event.section == 'staking' && event.method == 'Reward'
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// from frontend
|
|
|
+const updateAll = async (api: Api, io: any, status: any) => {
|
|
|
+ const proposalCount = await get.proposalCount(api)
|
|
|
+ if (proposalCount > status.proposalCount) {
|
|
|
+ fetchProposal(api, proposalCount)
|
|
|
+ status.proposalCount = proposalCount
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentChannel = await get.currentChannelId(api)
|
|
|
+ if (currentChannel > status.lastChannel)
|
|
|
+ status.lastChannel = await fetchChannels(api, currentChannel)
|
|
|
+
|
|
|
+ const currentCategory = await get.currentCategoryId(api)
|
|
|
+ if (currentCategory > status.lastCategory)
|
|
|
+ status.lastCategory = await fetchCategories(api, currentCategory)
|
|
|
+
|
|
|
+ const currentPost = await get.currentPostId(api)
|
|
|
+ if (currentPost > status.lastPost)
|
|
|
+ status.lastPost = await fetchPosts(api, currentPost)
|
|
|
+
|
|
|
+ const currentThread = await get.currentThreadId(api)
|
|
|
+ if (currentThread > status.lastThread)
|
|
|
+ status.lastThread = await fetchThreads(api, currentThread)
|
|
|
+
|
|
|
+ const postCount = await api.query.proposalsDiscussion.postCount()
|
|
|
+ // TODO save proposalComments: Number(postCount)
|
|
|
+
|
|
|
+ const currentEra = Number(await api.query.staking.currentEra())
|
|
|
+ if (currentEra > status.era) {
|
|
|
+ status.era = currentEra
|
|
|
+ fetchStakes(api, status.era, status.validators)
|
|
|
+ fetchLastReward(api, status.era - 1)
|
|
|
+ } else if (status.lastReward === 0) fetchLastReward(api, currentEra)
|
|
|
+
|
|
|
+ fetchEraRewardPoints(api, Number(status.era))
|
|
|
+}
|
|
|
+
|
|
|
+const fetchLastReward = async (api: Api, era: number) => {
|
|
|
+ const lastReward = Number(await api.query.staking.erasValidatorReward(era))
|
|
|
+ console.debug(`last reward`, era, lastReward)
|
|
|
+ if (lastReward > 0) {
|
|
|
+ } // TODO save lastReward
|
|
|
+ else fetchLastReward(api, era - 1)
|
|
|
+}
|
|
|
+
|
|
|
+const fetchTokenomics = async () => {
|
|
|
+ console.debug(`Updating tokenomics`)
|
|
|
+ const { data } = await axios.get('https://status.joystream.org/status')
|
|
|
+ if (!data) return
|
|
|
+ // TODO save 'tokenomics', data
|
|
|
+}
|
|
|
+
|
|
|
+const fetchChannels = async (api: Api, lastId: number) => {
|
|
|
+ const channels = [] // TOOD await Channel.findAll()
|
|
|
+ for (let id = lastId; id > 0; id--) {
|
|
|
+ if (channels.find((c) => c.id === id)) continue
|
|
|
+ console.debug(`Fetching channel ${id}`)
|
|
|
+ const data = await api.query.contentWorkingGroup.channelById(id)
|
|
|
+
|
|
|
+ const handle = String(data.handle)
|
|
|
+ const title = String(data.title)
|
|
|
+ const description = String(data.description)
|
|
|
+ const avatar = String(data.avatar)
|
|
|
+ const banner = String(data.banner)
|
|
|
+ const content = String(data.content)
|
|
|
+ const ownerId = Number(data.owner)
|
|
|
+ const accountId = String(data.role_account)
|
|
|
+ const publicationStatus =
|
|
|
+ data.publication_status === 'Public' ? true : false
|
|
|
+ const curation = String(data.curation_status)
|
|
|
+ const createdAt = data.created
|
|
|
+ const principal = Number(data.principal_id)
|
|
|
+
|
|
|
+ const channel = {
|
|
|
+ id,
|
|
|
+ handle,
|
|
|
+ title,
|
|
|
+ description,
|
|
|
+ avatar,
|
|
|
+ banner,
|
|
|
+ content,
|
|
|
+ ownerId,
|
|
|
+ accountId,
|
|
|
+ publicationStatus,
|
|
|
+ curation,
|
|
|
+ createdAt,
|
|
|
+ principal,
|
|
|
+ }
|
|
|
+ // TODO Channel.create(channel)
|
|
|
+ }
|
|
|
+ return lastId
|
|
|
+}
|
|
|
+
|
|
|
+const fetchCategories = async (api: Api, lastId: number) => {
|
|
|
+ const categories = [] // TODO await Category.findAll()
|
|
|
+ for (let id = lastId; id > 0; id--) {
|
|
|
+ if (categories.find((c) => c.id === id)) continue
|
|
|
+ console.debug(`fetching category ${id}`)
|
|
|
+ const data = await api.query.forum.categoryById(id)
|
|
|
+
|
|
|
+ const threadId = Number(data.thread_id)
|
|
|
+ const title = String(data.title)
|
|
|
+ const description = String(data.description)
|
|
|
+ const createdAt = Number(data.created_at.block)
|
|
|
+ const deleted = data.deleted
|
|
|
+ const archived = data.archived
|
|
|
+ const subcategories = Number(data.num_direct_subcategories)
|
|
|
+ const moderatedThreads = Number(data.num_direct_moderated_threads)
|
|
|
+ const unmoderatedThreads = Number(data.num_direct_unmoderated_threads)
|
|
|
+ const position = Number(data.position_in_parent_category)
|
|
|
+ const moderatorId = String(data.moderator_id)
|
|
|
+
|
|
|
+ const category = {
|
|
|
+ id,
|
|
|
+ threadId,
|
|
|
+ title,
|
|
|
+ description,
|
|
|
+ createdAt,
|
|
|
+ deleted,
|
|
|
+ archived,
|
|
|
+ subcategories,
|
|
|
+ moderatedThreads,
|
|
|
+ unmoderatedThreads,
|
|
|
+ position,
|
|
|
+ moderatorId,
|
|
|
+ }
|
|
|
+ //TODO Category.create(
|
|
|
+ }
|
|
|
+ return lastId
|
|
|
+}
|
|
|
+
|
|
|
+const fetchPosts = async (api: Api, lastId: number) => {
|
|
|
+ const posts = [] // TODO Post.findAll()
|
|
|
+ for (let id = lastId; id > 0; id--) {
|
|
|
+ if (posts.find((p) => p.id === id)) continue
|
|
|
+ console.debug(`fetching post ${id}`)
|
|
|
+ const data = await api.query.forum.postById(id)
|
|
|
+
|
|
|
+ const threadId = Number(data.thread_id)
|
|
|
+ const text = data.current_text
|
|
|
+ //const moderation = data.moderation;
|
|
|
+ //const history = data.text_change_history;
|
|
|
+ //const createdAt = moment(data.created_at);
|
|
|
+ const createdAt = data.created_at
|
|
|
+ const authorId = String(data.author_id)
|
|
|
+
|
|
|
+ // TODO Post.create({ id, threadId, text, authorId, createdAt })
|
|
|
+ }
|
|
|
+ return lastId
|
|
|
+}
|
|
|
+
|
|
|
+const fetchThreads = async (api: Api, lastId: number) => {
|
|
|
+ const threads = [] //TODO Thread.findAll()
|
|
|
+ for (let id = lastId; id > 0; id--) {
|
|
|
+ if (threads.find((t) => t.id === id)) continue
|
|
|
+ console.debug(`fetching thread ${id}`)
|
|
|
+ const data = await api.query.forum.threadById(id)
|
|
|
+
|
|
|
+ const title = String(data.title)
|
|
|
+ const categoryId = Number(data.category_id)
|
|
|
+ const nrInCategory = Number(data.nr_in_category)
|
|
|
+ const moderation = data.moderation
|
|
|
+ const createdAt = String(data.created_at.block)
|
|
|
+ const authorId = String(data.author_id)
|
|
|
+
|
|
|
+ const thread = {
|
|
|
+ id,
|
|
|
+ title,
|
|
|
+ categoryId,
|
|
|
+ nrInCategory,
|
|
|
+ moderation,
|
|
|
+ createdAt,
|
|
|
+ authorId,
|
|
|
+ }
|
|
|
+ // TODO Thread.create(
|
|
|
+ }
|
|
|
+ return lastId
|
|
|
+}
|
|
|
+
|
|
|
+const fetchCouncils = async (api: Api, status: any) => {
|
|
|
+ let councils = [] // await Council.findAll()
|
|
|
+ const cycle = 201600
|
|
|
+
|
|
|
+ for (let round = 0; round < status.round; round++) {
|
|
|
+ const block = 57601 + round * cycle
|
|
|
+ if (councils[round] || block > status.block) continue
|
|
|
+
|
|
|
+ console.debug(`Fetching council at block ${block}`)
|
|
|
+ const blockHash = await api.rpc.chain.getBlockHash(block)
|
|
|
+ if (!blockHash) continue
|
|
|
+
|
|
|
+ // TODO Council.create(await api.query.council.activeCouncil.at(blockHash))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const fetchProposals = async (api: Api) => {
|
|
|
+ const proposalCount = await get.proposalCount(api)
|
|
|
+ for (let i = proposalCount; i > 0; i--) fetchProposal(api, i)
|
|
|
}
|
|
|
+
|
|
|
+const fetchProposal = async (api: Api, id: number) => {
|
|
|
+ const exists = null // TODO await Proposa.findByPk(id)
|
|
|
+
|
|
|
+ if (exists && exists.stage === 'Finalized')
|
|
|
+ if (exists.votesByAccount && exists.votesByAccount.length) return
|
|
|
+ else return fetchVotesPerProposal(api, exists)
|
|
|
+
|
|
|
+ console.debug(`Fetching proposal ${id}`)
|
|
|
+ const proposal = await get.proposalDetail(api, id)
|
|
|
+ //TODO Proposal.create(proposal)
|
|
|
+ fetchVotesPerProposal(api, proposal)
|
|
|
+}
|
|
|
+
|
|
|
+const fetchVotesPerProposal = async (api: Api, proposal: ProposalDetail) => {
|
|
|
+ const { votesByAccount } = proposal
|
|
|
+ const proposals = [] // TODO await Proposal.findAll()
|
|
|
+ const councils = [] // TODO await Council.findAll()
|
|
|
+
|
|
|
+ if (votesByAccount && votesByAccount.length) return
|
|
|
+
|
|
|
+ console.debug(`Fetching proposal votes (${proposal.id})`)
|
|
|
+ let members = []
|
|
|
+ councils.map((seats) =>
|
|
|
+ seats.forEach(async (seat: Seat) => {
|
|
|
+ if (members.find((member) => member.account === seat.member)) return
|
|
|
+ const member = null // TODO await Member.findOne({ account: seat.member })
|
|
|
+ member && members.push(member)
|
|
|
+ })
|
|
|
+ )
|
|
|
+
|
|
|
+ const { id } = proposal
|
|
|
+ proposal.votesByAccount = await Promise.all(
|
|
|
+ members.map(async (member) => {
|
|
|
+ const vote = await fetchVoteByProposalByVoter(api, id, member.id)
|
|
|
+ return { vote, handle: member.handle }
|
|
|
+ })
|
|
|
+ )
|
|
|
+ // TODO save proposal.votesByAccount
|
|
|
+}
|
|
|
+
|
|
|
+const fetchVoteByProposalByVoter = async (
|
|
|
+ api: Api,
|
|
|
+ proposalId: number,
|
|
|
+ voterId: number
|
|
|
+): Promise<string> => {
|
|
|
+ console.debug(`Fetching vote by ${voterId} for proposal ${proposalId}`)
|
|
|
+ const vote: VoteKind = await api.query.proposalsEngine.voteExistsByProposalByVoter(
|
|
|
+ proposalId,
|
|
|
+ voterId
|
|
|
+ )
|
|
|
+ const hasVoted: number = (
|
|
|
+ await api.query.proposalsEngine.voteExistsByProposalByVoter.size(
|
|
|
+ proposalId,
|
|
|
+ voterId
|
|
|
+ )
|
|
|
+ ).toNumber()
|
|
|
+
|
|
|
+ return hasVoted ? String(vote) : ''
|
|
|
+}
|
|
|
+
|
|
|
+// nominators, validators
|
|
|
+
|
|
|
+const fetchNominators = async (api: Api) => {
|
|
|
+ const nominatorEntries = await api.query.staking.nominators.entries()
|
|
|
+ const nominators = nominatorEntries.map((n: any) => String(n[0].toHuman()))
|
|
|
+ // TODO save nominators
|
|
|
+}
|
|
|
+
|
|
|
+const fetchValidators = async (api: Api) => {
|
|
|
+ // session.disabledValidators: Vec<u32>
|
|
|
+ // TODO check online: imOnline.keys
|
|
|
+ // imOnline.authoredBlocks: 2
|
|
|
+ // TODO session.currentIndex: 17,081
|
|
|
+ const stashes = await api.derive.staking.stashes()
|
|
|
+ // TODO save stashes
|
|
|
+
|
|
|
+ const validatorEntries = await api.query.session.validators()
|
|
|
+ const validators = await validatorEntries.map((v: any) => String(v))
|
|
|
+ // TODO save validators
|
|
|
+}
|
|
|
+
|
|
|
+const fetchStakes = async (api: Api, era: number, validators: string[]) => {
|
|
|
+ // TODO staking.bondedEras: Vec<(EraIndex,SessionIndex)>
|
|
|
+ console.debug(`fetching stakes`)
|
|
|
+ const stashes = [] // TODO Stash.findAll()
|
|
|
+ if (!stashes) return
|
|
|
+ stashes.forEach(async (validator: string) => {
|
|
|
+ try {
|
|
|
+ const prefs = await api.query.staking.erasValidatorPrefs(era, validator)
|
|
|
+ const commission = Number(prefs.commission) / 10000000
|
|
|
+
|
|
|
+ const data = await api.query.staking.erasStakers(era, validator)
|
|
|
+ let { total, own, others } = data.toJSON()
|
|
|
+ //let { stakes = {} } = [] // TODO fetchStakes()
|
|
|
+ // { total, own, others, commission }
|
|
|
+ // TODO save stakes
|
|
|
+ } catch (e) {
|
|
|
+ console.warn(`Failed to fetch stakes for ${validator} in era ${era}`, e)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const fetchEraRewardPoints = async (api: Api, era: number) => {
|
|
|
+ const data = await api.query.staking.erasRewardPoints(era)
|
|
|
+ // TODO save rewardPoints
|
|
|
+}
|
|
|
+
|
|
|
+// accounts
|
|
|
+const fetchMembers = async (api: Api, lastId: number) => {
|
|
|
+ for (let id = lastId; id > 0; id--) {
|
|
|
+ fetchMember(api, id)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const fetchMemberByAccount = async (
|
|
|
+ api: Api,
|
|
|
+ account: string
|
|
|
+): Promise<Member> => {
|
|
|
+ const exists = null // TODO await Member.findOne({account}
|
|
|
+ if (exists) return exists
|
|
|
+
|
|
|
+ const id = await get.memberIdByAccount(api, account)
|
|
|
+ if (!id)
|
|
|
+ return { id: -1, handle: `unknown`, account, about: ``, registeredAt: 0 }
|
|
|
+ // TODO return member
|
|
|
+}
|
|
|
+
|
|
|
+const fetchMember = async (api: Api, id: number): Promise<Member> => {
|
|
|
+ const exists = null // TODO await Member.findOne({id}
|
|
|
+ if (exists) return exists
|
|
|
+
|
|
|
+ console.debug(`Fetching member ${id}`)
|
|
|
+ const membership = await get.membership(api, id)
|
|
|
+
|
|
|
+ const handle = String(membership.handle)
|
|
|
+ const account = String(membership.root_account)
|
|
|
+ const about = String(membership.about)
|
|
|
+ const registeredAt = Number(membership.registered_at_block)
|
|
|
+ const member = null // TODO await Member.create({ id, handle, account, registeredAt, about })
|
|
|
+ return member
|
|
|
+}
|
|
|
+
|
|
|
+const fetchReports = () => {
|
|
|
+ const domain = `https://raw.githubusercontent.com/Joystream/community-repo/master/council-reports`
|
|
|
+ const apiBase = `https://api.github.com/repos/joystream/community-repo/contents/council-reports`
|
|
|
+
|
|
|
+ const urls: { [key: string]: string } = {
|
|
|
+ alexandria: `${apiBase}/alexandria-testnet`,
|
|
|
+ archive: `${apiBase}/archived-reports`,
|
|
|
+ template: `${domain}/templates/council_report_template_v1.md`,
|
|
|
+ }
|
|
|
+
|
|
|
+ ;['alexandria', 'archive'].map((folder) => fetchGithubDir(urls[folder]))
|
|
|
+
|
|
|
+ fetchGithubFile(urls.template)
|
|
|
+}
|
|
|
+
|
|
|
+const fetchGithubFile = async (url: string): Promise<string> => {
|
|
|
+ const { data } = await axios.get(url)
|
|
|
+ return data
|
|
|
+}
|
|
|
+
|
|
|
+const fetchGithubDir = async (url: string) => {
|
|
|
+ const { data } = await axios.get(url)
|
|
|
+ data.forEach(
|
|
|
+ async (o: {
|
|
|
+ name: string
|
|
|
+ type: string
|
|
|
+ url: string
|
|
|
+ download_url: string
|
|
|
+ }) => {
|
|
|
+ const match = o.name.match(/^(.+)\.md$/)
|
|
|
+ const name = match ? match[1] : o.name
|
|
|
+ if (o.type === 'file') {
|
|
|
+ // TODO save await fetchGithubFile(o.download_url)
|
|
|
+ } else fetchGithubDir(o.url)
|
|
|
+ }
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = { addBlock }
|