Merge branch 'main' into extend-subscriptions

pull/241/head
Andrey Dolgolev 2021-09-07 15:51:23 +03:00
commit 1f36d16585
8 zmienionych plików z 309 dodań i 131 usunięć

Wyświetl plik

@ -1,7 +1,9 @@
import json
import logging
from typing import Optional
from typing import Optional, Dict, Any
from enum import Enum
import uuid
import boto3 # type: ignore
from moonstreamdb.models import (
@ -14,6 +16,12 @@ from sqlalchemy.orm import Session
from . import data
from .reporter import reporter
from .settings import ETHERSCAN_SMARTCONTRACTS_BUCKET
from bugout.data import BugoutResource
from .settings import (
MOONSTREAM_APPLICATION_ID,
bugout_client as bc,
BUGOUT_REQUEST_TIMEOUT_SECONDS,
)
logger = logging.getLogger(__name__)
ETHERSCAN_SMARTCONTRACT_LABEL_NAME = "etherscan_smartcontract"
@ -144,3 +152,21 @@ def get_address_labels(
)
return addresses_response
def create_onboarding_resource(
token: uuid.UUID,
resource_data: Dict[str, Any] = {
"type": data.USER_ONBOARDING_STATE,
"steps": {"welcome": 0, "subscriptions": 0, "stream": 0,},
"is_complete": False,
},
) -> BugoutResource:
resource = bc.create_resource(
token=token,
application_id=MOONSTREAM_APPLICATION_ID,
resource_data=resource_data,
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
)
return resource

Wyświetl plik

@ -5,6 +5,8 @@ from typing import List, Optional, Dict, Any
from pydantic import BaseModel, Field
USER_ONBOARDING_STATE = "onboarding_state"
class SubscriptionTypeResourceData(BaseModel):
id: str
@ -196,3 +198,8 @@ class AddressLabelsResponse(BaseModel):
class AddressListLabelsResponse(BaseModel):
addresses: List[AddressLabelsResponse] = Field(default_factory=list)
class OnboardingState(BaseModel):
is_complete: bool
steps: Dict[str, int]

Wyświetl plik

@ -5,20 +5,30 @@ import logging
from typing import Any, Dict
import uuid
from bugout.data import BugoutToken, BugoutUser
from bugout.data import BugoutToken, BugoutUser, BugoutResource
from bugout.exceptions import BugoutResponseException
from fastapi import FastAPI, Form, Request
from fastapi import (
Body,
FastAPI,
Form,
Request,
)
from fastapi.middleware.cors import CORSMiddleware
from .. import data
from ..middleware import BroodAuthMiddleware, MoonstreamHTTPException
from ..settings import (
MOONSTREAM_APPLICATION_ID,
DOCS_TARGET_PATH,
ORIGINS,
DOCS_PATHS,
bugout_client as bc,
BUGOUT_REQUEST_TIMEOUT_SECONDS,
)
from ..version import MOONSTREAM_VERSION
from ..actions import create_onboarding_resource
logger = logging.getLogger(__name__)
@ -166,3 +176,121 @@ async def logout_handler(request: Request) -> uuid.UUID:
except Exception as e:
raise MoonstreamHTTPException(status_code=500, internal_error=e)
return token_id
@app.post("/onboarding", tags=["users"], response_model=data.OnboardingState)
async def set_onboarding_state(
request: Request, onboarding_data: data.OnboardingState = Body(...),
) -> data.OnboardingState:
token = request.state.token
try:
response = bc.list_resources(
token=token,
params={"type": data.USER_ONBOARDING_STATE},
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
)
resource_data = {"type": data.USER_ONBOARDING_STATE, **onboarding_data.dict()}
if response.resources:
resource = bc.update_resource(
token=token,
resource_id=str(response.resources[0].id),
resource_data={"update": resource_data, "drop_keys": []},
)
else:
resource = create_onboarding_resource(
token=token, resource_data=resource_data
)
except BugoutResponseException as e:
raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
except Exception as e:
raise MoonstreamHTTPException(status_code=500)
if (
resource.resource_data.get("is_complete") is None
or resource.resource_data.get("steps") is None
):
logger.error(
f"Resources did not return correct onboarding object. Resource id:{resource.id}"
)
raise MoonstreamHTTPException(status_code=500)
result = data.OnboardingState(
is_complete=resource.resource_data.get("is_complete", False),
steps=resource.resource_data.get("steps", {}),
)
return result
@app.get("/onboarding", tags=["users"], response_model=data.OnboardingState)
async def get_onboarding_state(request: Request) -> data.OnboardingState:
token = request.state.token
try:
response = bc.list_resources(
token=token,
params={"type": data.USER_ONBOARDING_STATE},
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
)
if response.resources:
resource = response.resources[0]
else:
resource = create_onboarding_resource(token=token)
except BugoutResponseException as e:
raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
except Exception as e:
raise MoonstreamHTTPException(status_code=500)
if (
resource.resource_data.get("is_complete") is None
or resource.resource_data.get("steps") is None
):
logger.error(
f"Resources did not return correct onboarding object. Resource id:{resource.id}"
)
raise MoonstreamHTTPException(status_code=500)
result = data.OnboardingState(
is_complete=resource.resource_data.get("is_complete", False),
steps=resource.resource_data.get("steps", {}),
)
return result
@app.delete("/onboarding", tags=["users"], response_model=data.OnboardingState)
async def delete_onboarding_state(request: Request) -> data.OnboardingState:
token = request.state.token
try:
response = bc.list_resources(
token=token,
params={"type": data.USER_ONBOARDING_STATE},
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
)
if not response.resources:
raise MoonstreamHTTPException(status_code=404, detail="not found")
if response.resources:
resource: BugoutResource = bc.delete_resource(
token=token,
resource_id=response.resources[0].id,
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
)
except BugoutResponseException as e:
raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
except Exception as e:
raise MoonstreamHTTPException(status_code=500)
if (
resource.resource_data.get("is_complete") is None
or resource.resource_data.get("steps") is None
):
logger.error(
f"Resources did not return correct onboarding object. Resource id:{resource.id}"
)
raise MoonstreamHTTPException(status_code=500)
result = data.OnboardingState(
is_complete=resource.resource_data.get("is_complete", False),
steps=resource.resource_data.get("steps", {}),
)
return result

Wyświetl plik

@ -1,24 +1,26 @@
import argparse
import codecs
import csv
from dataclasses import dataclass
from datetime import datetime
import json
import logging
import os
import sys
import time
from datetime import datetime
from typing import Any, List, Optional, Dict
from dataclasses import dataclass
import csv
import codecs
import json
import os
import boto3 # type: ignore
from moonstreamdb.db import yield_db_session_ctx
from moonstreamdb.models import EthereumAddress, EthereumLabel
import requests
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import text
from .version import MOONCRAWL_VERSION
from .settings import MOONSTREAM_ETHERSCAN_TOKEN
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
if MOONSTREAM_ETHERSCAN_TOKEN is None:
raise Exception("MOONSTREAM_ETHERSCAN_TOKEN environment variable must be set")
@ -66,21 +68,16 @@ def get_address_id(db_session: Session, contract_address: str) -> int:
if id is not None:
return id[0]
latest_address_id = (
db_session.query(EthereumAddress.id).order_by(text("id desc")).limit(1).one()
)[0]
id = latest_address_id + 1
smart_contract = EthereumAddress(
id=id,
address=contract_address,
)
try:
db_session.add(smart_contract)
db_session.commit()
except:
return smart_contract.id
except Exception as e:
db_session.rollback()
return id
raise e
def crawl_step(db_session: Session, contract: VerifiedSmartContract, crawl_url: str):
@ -112,22 +109,27 @@ def crawl_step(db_session: Session, contract: VerifiedSmartContract, crawl_url:
object_key = f"/etherscan/v1/{contract.address}.json"
push_to_bucket(contract_info, object_key)
eth_address_id = get_address_id(db_session, contract.address)
eth_label = EthereumLabel(
label=ETHERSCAN_SMARTCONTRACTS_LABEL_NAME,
address_id=eth_address_id,
label_data={
"object_uri": f"s3://{bucket}/{object_key}",
"name": contract.name,
"tx_hash": contract.tx_hash,
},
)
try:
db_session.add(eth_label)
db_session.commit()
except:
db_session.rollback()
eth_address_id = get_address_id(db_session, contract.address)
eth_label = EthereumLabel(
label=ETHERSCAN_SMARTCONTRACTS_LABEL_NAME,
address_id=eth_address_id,
label_data={
"object_uri": f"s3://{bucket}/{object_key}",
"name": contract.name,
"tx_hash": contract.tx_hash,
},
)
try:
db_session.add(eth_label)
db_session.commit()
except Exception as e:
db_session.rollback()
raise e
except Exception as e:
logger.error(
f"Failed to add addresss label ${contract.address} to database\n{str(e)}"
)
def crawl(

Wyświetl plik

@ -70,7 +70,7 @@ const Welcome = () => {
ui.setOnboardingStep(ui.onboardingStep + 1);
scrollRef?.current?.scrollIntoView();
} else {
ui.setisOnboardingComplete(true);
ui.setOnboardingComplete(true);
router.push("/stream");
}
};

Wyświetl plik

@ -115,12 +115,6 @@ const AppNavbar = () => {
</RouteButton>
))}
{USER_NAV_PATHES.map((item, idx) => {
console.log(
"item.path:",
item.path,
"pathname:",
router.nextRouter.pathname
);
return (
<RouteButton
key={`${idx}-${item.title}-navlink`}

Wyświetl plik

@ -1,10 +1,26 @@
import React, { useState, useContext, useEffect } from "react";
import React, { useState, useContext, useEffect, useCallback } from "react";
import { useBreakpointValue } from "@chakra-ui/react";
import { useStorage, useQuery, useRouter } from "../../hooks";
import UIContext from "./context";
import UserContext from "../UserProvider/context";
import ModalContext from "../ModalProvider/context";
import { v4 as uuid4 } from "uuid";
import { PreferencesService } from "../../services";
const onboardingSteps = [
{
step: "welcome",
description: "Basics of how Moonstream works",
},
{
step: "subscriptions",
description: "Learn how to subscribe to blockchain events",
},
{
step: "stream",
description: "Learn how to use your Moonstream to analyze blah blah blah",
},
];
const UIProvider = ({ children }) => {
const router = useRouter();
@ -53,14 +69,6 @@ const UIProvider = ({ children }) => {
}
}, [isAppView, user, isLoggingOut]);
useEffect(() => {
if (isInit && router.nextRouter.isReady) {
setAppReady(true);
} else {
setAppReady(false);
}
}, [isInit, router]);
useEffect(() => {
if (user && user.username) {
setLoggedIn(true);
@ -155,90 +163,109 @@ const UIProvider = ({ children }) => {
// *********** Onboarding state **********************
const onboardingSteps = [
{
step: "welcome",
description: "Basics of how Moonstream works",
},
{
step: "subscriptions",
description: "Learn how to subscribe to blockchain events",
},
{
step: "stream",
description: "Learn how to use your Moonstream to analyze blah blah blah",
},
];
const [onboardingState, setOnboardingState] = useState(false);
const [onboardingStep, setOnboardingStep] = useState();
const [onboardingStateInit, setOnboardingStateInit] = useState(false);
const [onboardingState, setOnboardingState] = useStorage(
window.localStorage,
"onboardingState",
{
welcome: 0,
subscriptions: 0,
stream: 0,
}
);
const [onboardingStep, setOnboardingStep] = useState(() => {
//First find onboarding step that was viewed once (resume where left)
// If none - find step that was never viewed
// if none - set onboarding to zero
let step = onboardingSteps.findIndex(
(event) => onboardingState[event.step] === 1
);
step =
step === -1
? onboardingSteps.findIndex(
(event) => onboardingState[event.step] === 0
)
: step;
if (step === -1 && isOnboardingComplete) return 0;
else if (step === -1 && !isOnboardingComplete) return 0;
else return step;
});
const [isOnboardingComplete, setisOnboardingComplete] = useStorage(
window.localStorage,
"isOnboardingComplete",
isLoggedIn ? true : false
const setOnboardingComplete = useCallback(
(newState) => {
setOnboardingState({ ...onboardingState, is_complete: newState });
},
[onboardingState]
);
useEffect(() => {
if (isLoggedIn && !isOnboardingComplete) {
//If onboarding state not exists - fetch it from backend
//If it exists but init is not set - set init true
//If it exists and is init -> post update to backend
if (!onboardingState && user) {
const currentOnboardingState = async () =>
PreferencesService.getOnboardingState().then((response) => {
return response.data;
});
currentOnboardingState().then((response) => {
setOnboardingState(response);
});
} else if (user && onboardingState && !onboardingStateInit) {
setOnboardingStateInit(true);
} else if (onboardingStateInit) {
PreferencesService.setOnboardingState(onboardingState);
}
// eslint-disable-next-line
}, [onboardingState, user]);
useEffect(() => {
//This will set step after state is fetched from backend
if (!Number.isInteger(onboardingStep) && onboardingState) {
const step = onboardingSteps.findIndex(
(event) => onboardingState[event.step] === 0
);
if (step === -1 && onboardingState["is_complete"])
setOnboardingStep(onboardingSteps.length - 1);
else if (step === -1 && !onboardingState["is_complete"])
return setOnboardingStep(0);
else setOnboardingStep(step);
}
}, [onboardingState, onboardingStep]);
useEffect(() => {
//redirect to welcome page if yet not completed onboarding
if (
isLoggedIn &&
isAppReady &&
onboardingState &&
!onboardingState?.is_complete
) {
router.replace("/welcome");
}
// eslint-disable-next-line
}, [isLoggedIn, isOnboardingComplete]);
}, [isLoggedIn, onboardingState?.is_complete, isAppReady]);
useEffect(() => {
if (
onboardingSteps.findIndex(
(event) => onboardingState[event.step] === 0
) === -1
) {
setisOnboardingComplete(true);
//This will set up onboarding complete once user finishes each view at least once
if (onboardingState?.steps && user && isAppReady) {
if (
onboardingSteps.findIndex(
(event) => onboardingState.steps[event.step] === 0
) === -1
) {
!onboardingState.is_complete && setOnboardingComplete(true);
}
}
//eslint-disable-next-line
}, [onboardingState]);
}, [onboardingState, user, isAppReady, setOnboardingComplete]);
useEffect(() => {
if (router.nextRouter.pathname === "/welcome") {
const newOnboardingState = {
//This will update onboardingState when step changes
if (
router.nextRouter.pathname === "/welcome" &&
isAppReady &&
user &&
Number.isInteger(onboardingStep) &&
onboardingState
) {
setOnboardingState({
...onboardingState,
[`${onboardingSteps[onboardingStep].step}`]:
onboardingState[onboardingSteps[onboardingStep].step] + 1,
};
setOnboardingState(newOnboardingState);
steps: {
...onboardingState.steps,
[`${onboardingSteps[onboardingStep].step}`]:
onboardingState.steps[onboardingSteps[onboardingStep].step] + 1,
},
});
}
// eslint-disable-next-line
}, [onboardingStep, router.nextRouter.pathname]);
// const ONBOARDING_STEP_NUM = steps.length;
}, [onboardingStep, router.nextRouter.pathname, user, isAppReady]);
// ********************************************************
useEffect(() => {
if (isInit && router.nextRouter.isReady && onboardingState) {
setAppReady(true);
} else {
setAppReady(false);
}
}, [isInit, router, onboardingState]);
return (
<UIContext.Provider
value={{
@ -268,8 +295,7 @@ const UIProvider = ({ children }) => {
isEntryDetailView,
onboardingStep,
setOnboardingStep,
isOnboardingComplete,
setisOnboardingComplete,
setOnboardingComplete,
onboardingSteps,
setOnboardingState,
onboardingState,

Wyświetl plik

@ -1,23 +1,18 @@
import { http } from "../utils";
const API = process.env.NEXT_PUBLIC_SIMIOTICS_JOURNALS_URL;
const PREFERENCES_API = `${API}/preferences`;
const API_URL = process.env.NEXT_PUBLIC_MOONSTREAM_API_URL;
export const PREFERENCES_URL = `${API_URL}/users`;
export const getDefaultJournal = () =>
export const getOnboardingState = () =>
http({
method: "GET",
url: `${PREFERENCES_API}/default_journal`,
url: `${PREFERENCES_URL}/onboarding`,
});
export const setDefaultJournal = (journalId) =>
http({
export const setOnboardingState = (data) => {
return http({
method: "POST",
url: `${PREFERENCES_API}/default_journal`,
data: { id: journalId },
});
export const unsetDefaultJournal = () =>
http({
method: "DELETE",
url: `${PREFERENCES_API}/default_journal`,
url: `${PREFERENCES_URL}/onboarding`,
data,
});
};