Skip to content

Commit

Permalink
better ergonomics for setting up paybots (#197)
Browse files Browse the repository at this point in the history
* add ensure_address and do_setup

* fix lints, do_setup

* docstrings
  • Loading branch information
technillogue authored Apr 8, 2022
1 parent 4ceea76 commit 790d3e2
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 23 deletions.
85 changes: 65 additions & 20 deletions forest/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from asyncio.subprocess import PIPE
from decimal import Decimal
from functools import wraps
from pathlib import Path
from textwrap import dedent
from typing import (
Any,
Expand Down Expand Up @@ -57,6 +58,11 @@
from forest.cryptography import hash_salt
from forest.message import AuxinMessage, Message, StdioMessage

try:
import captcha
except ImportError:
captcha = None # type: ignore

JSON = dict[str, Any]
Response = Union[str, list, dict[str, str], None]
AsyncFunc = Callable[..., Awaitable]
Expand All @@ -67,11 +73,7 @@

MessageParser = AuxinMessage if utils.AUXIN else StdioMessage
logging.info("Using message parser: %s", MessageParser)
fee_pmob = int(1e12 * 0.0004)
try:
import captcha
except ImportError:
captcha = None # type: ignore
FEE_PMOB = int(1e12 * 0.0004)


def rpc(
Expand All @@ -85,6 +87,26 @@ def rpc(
}


async def get_attachment_paths(message: Message) -> list[str]:
if not utils.AUXIN:
return [
str(Path("./attachments") / attachment["id"])
for attachment in message.attachments
]
await asyncio.sleep(2)
attachments = []
for attachment_info in message.attachments:
attachment_path = attachment_info.get("fileName")
timestamp = attachment_info.get("uploadTimestamp")
if attachment_path is None:
attachment_paths = glob.glob(f"/tmp/unnamed_attachment_{timestamp}.*")
if attachment_paths:
attachments.append(attachment_paths.pop())
else:
attachments.append(f"/tmp/{attachment_path}")
return attachments


ActivityQueries = pghelp.PGExpressions(
table="user_activity",
create_table="""CREATE TABLE user_activity (
Expand Down Expand Up @@ -355,6 +377,7 @@ async def set_profile_auxin(
family_name: Optional[str] = "",
payment_address: Optional[str] = "",
profile_path: Optional[str] = None,
**kwargs: Optional[str],
) -> str:
"""set given and family name, payment address (must be b64 format),
and profile picture"""
Expand All @@ -365,6 +388,9 @@ async def set_profile_auxin(
params["mobilecoinAddress"] = payment_address
if profile_path:
params["avatarFile"] = profile_path
for parameter, value in kwargs.items():
if value:
params[parameter] = value
rpc_id = f"setProfile-{get_uid()}"
await self.outbox.put(rpc("setProfile", params, rpc_id))
return rpc_id
Expand Down Expand Up @@ -1003,6 +1029,17 @@ async def do_fsr(self, msg: Message) -> Response:
return str(await self.mobster.req_(fsr_command, **params))
return "/fsr [command] ([arg1] [val1]( [arg2] [val2])...)"

@requires_admin
async def do_setup(self, _: Message) -> str:
if not utils.AUXIN:
return "Can't set payment address without auxin"
await self.set_profile_auxin(
mobilecoin_address=mc_util.b58_wrapper_to_b64_public_address(
await self.mobster.ensure_address()
)
)
return "OK"

@requires_admin
async def do_balance(self, _: Message) -> Response:
"""Returns bot balance in MOB."""
Expand Down Expand Up @@ -1077,19 +1114,9 @@ async def do_address(self, msg: Message) -> Response:

@requires_admin
async def do_set_profile(self, message: Message) -> Response:
"""Renames bot (requires admin) - accepts first name, last name, and address."""
user_image = None
if message.attachments and len(message.attachments):
await asyncio.sleep(2)
attachment_info = message.attachments[0]
attachment_path = attachment_info.get("fileName")
timestamp = attachment_info.get("uploadTimestamp")
if attachment_path is None:
attachment_paths = glob.glob(f"/tmp/unnamed_attachment_{timestamp}.*")
if attachment_paths:
user_image = attachment_paths.pop()
else:
user_image = f"/tmp/{attachment_path}"
"""Renames bot (requires admin) - accepts first name, last name, and payment address."""
attachments = await get_attachment_paths(message)
user_image = attachments[0] if attachments else None
if user_image or (message.tokens and len(message.tokens) > 0):
await self.set_profile_auxin(
given_name=message.arg1,
Expand Down Expand Up @@ -1135,7 +1162,7 @@ async def build_gift_code(self, amount_pmob: int) -> list[str]:
"build_gift_code",
account_id=await self.mobster.get_account(),
value_pmob=str(int(amount_pmob)),
fee=str(fee_pmob),
fee=str(FEE_PMOB),
memo="Gift code built with MOBot!",
)
prop = raw_prop["result"]["tx_proposal"]
Expand All @@ -1150,7 +1177,7 @@ async def build_gift_code(self, amount_pmob: int) -> list[str]:
return [
"Built Gift Code",
b58,
f"redeemable for {str(mc_util.pmob2mob(amount_pmob - fee_pmob)).rstrip('0')} MOB",
f"redeemable for {str(mc_util.pmob2mob(amount_pmob - FEE_PMOB)).rstrip('0')} MOB",
]

# FIXME: clarify signature and return details/docs
Expand Down Expand Up @@ -1683,6 +1710,24 @@ async def do_challenge(self, msg: Message) -> Response:
return await self.do_challenge(msg)
return "Thanks for helping protect our community!"

@requires_admin
async def do_setup(self, msg: Message) -> str:
if not utils.AUXIN:
return "Can't set profile without auxin"
fields = {}
for field in ["given_name", "family_name", "about", "mood_emoji"]:
resp = await self.ask_freeform_question(
msg.source, f"value for field {field}?"
)
if resp and resp.lower() != "none":
fields[field] = resp
fields["mobilecoin_address"] = await self.mobster.ensure_address()
attachments = await get_attachment_paths(msg)
if attachments:
fields["profile_path"] = attachments[0]
await self.set_profile_auxin(**fields)
return f"set {', '.join(fields)}"


async def no_get(request: web.Request) -> web.Response:
raise web.HTTPFound(location="https://signal.org/")
Expand Down
2 changes: 1 addition & 1 deletion forest/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ def __init__(self, blob: dict) -> None:
# msg data
msg = envelope.get("dataMessage", {})
# "attachments":[{"contentType":"image/png","filename":"image.png","id":"1484072582431702699","size":2496}]}
self.attachments: list[dict[str, str]] = msg.get("attachments")
self.attachments: list[dict[str, str]] = msg.get("attachments", [])
# "mentions":[{"name":"+447927948360","number":"+447927948360","uuid":"fc4457f0-c683-44fe-b887-fe3907d7762e","start":0,"length":1}
self.mentions = msg.get("mentions") or []
self.full_text = self.text = msg.get("message", "")
Expand Down
21 changes: 19 additions & 2 deletions forest/payments_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,11 @@ async def get_rate(self) -> float:
return mob_rate

async def pmob2usd(self, pmob: int) -> float:
"takes picoMOB, returns USD"
return float(mc_util.pmob2mob(pmob)) * await self.get_rate()

async def usd2mob(self, usd: float, perturb: bool = False) -> float:
"takes USD, returns MOB"
invnano = 100000000
# invpico = 100000000000 # doesn't work in mixin
mob_rate = await self.get_rate()
Expand All @@ -228,16 +230,31 @@ async def usd2mob(self, usd: float, perturb: bool = False) -> float:
return round(mob_amount, 8)
return round(mob_amount, 3) # maybe ceil?

async def import_account(self) -> dict:
async def import_account(self, name: str = "bot") -> dict:
"import an account using the MNEMONIC secret"
if not utils.get_secret("MNEMONIC"):
raise ValueError
params = {
"mnemonic": utils.get_secret("MNEMONIC"),
"key_derivation_version": "1",
"name": "falloopa",
"name": name,
"next_subaddress_index": "2",
"first_block_index": "3500",
}
return await self.req({"method": "import_account", "params": params})

async def ensure_address(self) -> str:
"""if we don't have an address, either import an account if MNEMONIC is set,
or create a new account. then return our address"""
try:
await self.get_my_address()
except IndexError:
if utils.get_secret("MNEMONIC"):
await self.import_account()
else:
await self.req_(method="create_account", name="bot")
return await self.get_my_address()

async def get_my_address(self) -> str:
"""Returns either the address set, or the address specified by the secret
or the first address in the full service instance in that order"""
Expand Down

0 comments on commit 790d3e2

Please sign in to comment.