Пример #1
0
    def start(self, parameters: Parameters):
        url = "https://www.loomio.org/api/b1/polls"
        payload = parameters._json
        payload.pop("options")

        subgroup = payload.pop("subgroup", None)
        api_key = self.plugin_inst._get_api_key(subgroup)
        self.state.set("poll_api_key", api_key)

        payload["options[]"] = parameters.options
        payload["api_key"] = api_key
        resp = requests.post(url, payload)
        if not resp.ok:
            logger.error(f"Error: {resp.status_code} {resp.text}")
            raise PluginErrorInternal(resp.text)

        response = resp.json()

        if response.get("errors"):
            errors = response["errors"]
            raise PluginErrorInternal(str(errors))

        poll_key = response.get("polls")[0].get("key")
        self.url = f"https://www.loomio.org/p/{poll_key}"
        self.state.set("poll_key", poll_key)
        self.outcome = {}
        self.status = ProcessStatus.PENDING.value
        self.save()
Пример #2
0
def validate_slack_event(request):
    req_timestamp = request.headers.get("X-Slack-Request-Timestamp")
    if req_timestamp is None:
        raise PluginErrorInternal("missing request timestamp")
    req_signature = request.headers.get("X-Slack-Signature")
    if req_signature is None or not verify_signature(request, req_timestamp,
                                                     req_signature):
        raise PluginErrorInternal("Invalid request signature")
Пример #3
0
    def start(self, parameters: Parameters) -> None:
        discourse_server_url = self.plugin_inst.config["server_url"]
        url = f"{discourse_server_url}/posts.json"

        poll_type = parameters.poll_type
        if poll_type != "number" and not parameters.options:
            raise PluginErrorInternal(
                f"Options are required for poll type {poll_type}")

        optional_params = []
        if parameters.closing_at:
            optional_params.append(f"close={parameters.closing_at}")
        if parameters.groups:
            optional_params.append(f"groups={','.join(parameters.groups)}")
        if parameters.public is True:
            optional_params.append("public=true")
        if parameters.chart_type:
            optional_params.append(f"chartType={parameters.chart_type}")
        for p in ["min", "max", "step", "results"]:
            if getattr(parameters, p):
                optional_params.append(f"{p}={getattr(parameters, p)}")

        options = "".join([f"* {opt}\n" for opt in parameters.options
                           ]) if poll_type != "number" else ""
        raw = f"""
{parameters.details or ""}
[poll type={poll_type} {' '.join(optional_params)}]
# {parameters.title}
{options}
[/poll]
        """
        payload = {"raw": raw, "title": parameters.title}
        if parameters.category is not None:
            payload["category"] = parameters.category
        if parameters.topic_id is not None:
            payload["topic_id"] = parameters.topic_id

        logger.info(payload)
        logger.info(url)

        response = self.plugin_inst.discourse_request("POST",
                                                      "posts.json",
                                                      json=payload)
        if response.get("errors"):
            errors = response["errors"]
            raise PluginErrorInternal(str(errors))

        self.url = self.plugin_inst.construct_post_url(response)
        logger.info(f"Poll created at {self.url}")
        logger.debug(response)

        self.state.set("post_id", response.get("id"))
        self.state.set("topic_id", response.get("topic_id"))
        self.state.set("topic_slug", response.get("topic_slug"))

        self.outcome = {}
        self.status = ProcessStatus.PENDING.value
        self.save()
Пример #4
0
def validate_discord_interaction(request):
    timestamp = request.headers.get("X-Signature-Timestamp")
    signature = request.headers.get("X-Signature-Ed25519")
    if not timestamp or not signature:
        raise PluginErrorInternal("Bad request signature: missing headers")

    raw_body = request.body.decode("utf-8")
    client_public_key = DISCORD_PUBLIC_KEY

    if not verify_key(raw_body, signature, timestamp, client_public_key):
        raise PluginErrorInternal("Bad request signature: verification failed")
Пример #5
0
    def fetch_accounts_analysis(self):
        server = self.config["server_url"]
        resp = requests.get(f"{server}/output/accounts.json")
        if resp.status_code == 404:
            raise PluginErrorInternal(
                "'output/accounts.json' file not present. Run 'yarn sourcecred analysis' when generating sourcecred instance."
            )
        if resp.status_code == 200:
            accounts = resp.json()
            return accounts

        raise PluginErrorInternal(f"Error fetching SourceCred accounts.json: {resp.status_code} {resp.reason}")
Пример #6
0
    def validate_request_signature(self, request):
        event_signature = request.headers.get("X-Discourse-Event-Signature")
        if not event_signature:
            raise PluginErrorInternal("Missing event signature")
        key = bytes(self.config["webhook_secret"], "utf-8")
        string_signature = hmac.new(key, request.body,
                                    hashlib.sha256).hexdigest()
        expected_signature = f"sha256={string_signature}"
        if not hmac.compare_digest(event_signature, expected_signature):
            raise PluginErrorInternal("Invalid signature header")

        instance = request.headers["X-Discourse-Instance"]
        if instance != self.config["server_url"]:
            raise PluginErrorInternal("Unexpected X-Discourse-Instance")
Пример #7
0
    def fetch_and_update_outcome(self):
        poll_key = self.state.get("poll_key")
        api_key = self.state.get("poll_api_key")
        url = f"https://www.loomio.org/api/b1/polls/{poll_key}?api_key={api_key}"
        resp = requests.get(url)
        if not resp.ok:
            logger.error(
                f"Error fetching poll: {resp.status_code} {resp.text}")
            raise PluginErrorInternal(resp.text)

        logger.debug(resp.text)
        response = resp.json()
        if response.get("errors"):
            logger.error(f"Error fetching poll outcome: {response['errors']}")
            self.errors = response["errors"]

        # Update status
        poll = response["polls"][0]
        if poll.get("closed_at") is not None:
            self.status = ProcessStatus.COMPLETED.value

        # Update vote counts
        self.outcome["votes"] = create_vote_dict(response)

        # Add other data from poll
        self.outcome["voters_count"] = poll.get("voters_count")
        self.outcome["undecided_voters_count"] = poll.get(
            "undecided_voters_count")
        self.outcome["cast_stances_pct"] = poll.get("cast_stances_pct")

        logger.info(f"Updated outcome: {self.outcome}")
        self.save()
Пример #8
0
 def _get_memberships(self, api_key):
     resp = requests.get(
         f"https://www.loomio.org/api/b1/memberships?api_key={api_key}")
     if not resp.ok:
         logger.error(f"Error: {resp.status_code} {resp.text}")
         raise PluginErrorInternal(resp.text)
     return resp.json()
Пример #9
0
    def view(self, method_name, args=None):
        contract_id = self.config["contract_id"]
        account = self.create_master_account(
        )  # creates a new provider every time!

        try:
            return account.view_function(contract_id, method_name, args or {})
        except (TransactionError, ViewFunctionError) as e:
            raise PluginErrorInternal(str(e))
Пример #10
0
 def slack_request(self, method, route, json=None, data=None):
     url = f"https://slack.com/api/{route}"
     logger.debug(f"{method} {url}")
     resp = requests.request(method, url, json=json, data=data)
     if not resp.ok:
         logger.error(f"{resp.status_code} {resp.reason}")
         logger.error(resp.request.body)
         raise PluginErrorInternal(resp.text)
     if resp.content:
         data = resp.json()
         is_ok = data.pop("ok")
         if not is_ok:
             # logger.debug(f"X-OAuth-Scopes: {resp.headers.get('X-OAuth-Scopes')}")
             # logger.debug(f"X-Accepted-OAuth-Scopes: {resp.headers.get('X-Accepted-OAuth-Scopes')}")
             # logger.debug(data["error"])
             raise PluginErrorInternal(data["error"])
         return data
     return {}
Пример #11
0
    def run_query(self, query, variables):
        resp = requests.post(
            OPEN_COLLECTIVE_GRAPHQL,
            json={
                "query": query,
                "variables": variables
            },
            headers={"Api-Key": f"{self.config['api_key']}"},
        )
        if not resp.ok:
            logger.error(
                f"Query failed with {resp.status_code} {resp.reason}: {query}")
            raise PluginErrorInternal(resp.text)

        result = resp.json()
        if result.get("errors"):
            msg = ",".join([e["message"] for e in result["errors"]])
            raise PluginErrorInternal(msg)
        return result["data"]
Пример #12
0
 def pick_pointer(self, key=DEFAULT_KEY):
     pointers = self.state.get(key) or {}
     if len(pointers) == 0:
         raise PluginErrorInternal(f"No pointers for key {key}")
     # based on https://webmonetization.org/docs/probabilistic-rev-sharing/
     sum_ = sum(list(pointers.values()))
     choice = random.random() * sum_
     for (pointer, weight) in pointers.items():
         choice = choice - weight
         if choice <= 0:
             return {"pointer": pointer}
Пример #13
0
    def create_discussion(self, title, subgroup=None, **kwargs):
        payload = {"title": title, **kwargs}
        payload["api_key"] = self._get_api_key(subgroup)

        resp = requests.post(f"https://www.loomio.org/api/b1/discussions",
                             payload)
        if not resp.ok:
            logger.error(f"Error: {resp.status_code} {resp.text}")
            raise PluginErrorInternal(resp.text)
        response = resp.json()
        return response
Пример #14
0
    def _get_api_key(self, key_or_handle=None):
        """Get the API key for a specific Loomio group. Raises exception if not found."""
        if not key_or_handle:
            # if no subgroup, use the main group key
            return self.config["api_key"]

        api_key_group_map = self.state.get("api_key_group_map")
        for api_key, v in api_key_group_map.items():
            if v["key"] == key_or_handle or v["handle"] == key_or_handle:
                return api_key
        raise PluginErrorInternal(
            f"No API key found for Loomio group {key_or_handle}.")
Пример #15
0
 def update(self):
     """
     We make a request to Discourse EVERY time, here, so that we can catch cases where the poll was closed
     manually by a user. Would be simplified if we disallow that, and instead this function could just
     check if `closing_at` has happened yet (if set) and call close() if it has.
     """
     post_id = self.state.get("post_id")
     if post_id is None:
         raise PluginErrorInternal(f"Missing post ID, can't update {self}")
     response = self.plugin_inst.discourse_request("GET",
                                                   f"posts/{post_id}.json")
     poll = response["polls"][0]
     self.update_outcome_from_discourse_poll(poll)
Пример #16
0
 def __validate_collective_or_project(self, legacy_id):
     if legacy_id == self.state.get("collective_legacy_id"):
         return True
     project_legacy_ids = self.state.get("project_legacy_ids") or []
     if legacy_id in project_legacy_ids:
         return True
     # re-initialize and check projects again, in case a new project has been added
     self.initialize()
     project_legacy_ids = self.state.get("project_legacy_ids")
     if legacy_id in project_legacy_ids:
         return True
     raise PluginErrorInternal(
         f"Received webhook for the wrong collective. Expected {self.state.get('collective_legacy_id')} or projects {project_legacy_ids}, found "
         + str(legacy_id))
Пример #17
0
 def get_user_cred(self, username: Optional[str] = None, id: Optional[str] = None):
     cred_data = self.fetch_accounts_analysis()
     if not (username or id):
         raise PluginErrorInternal("Either a username or an id argument is required")
     for account in cred_data["accounts"]:
         name = account["account"]["identity"]["name"]
         """
         Account aliases is how sourcecred stores internal ids of accounts for
         all platforms, storing the id in a format like this 
         "N\u0000sourcecred\u0000discord\u0000MEMBER\u0000user\u0000140750062325202944\u0000"
         the discord id for example is store in the index before last always
         the same could apply to discourse, github, and whatever 
         """
         account_aliases: list = account["account"]["identity"]["aliases"]
         if id:
             # Making sure the id is in string form for comparison
             id = str(id)
             for alias in account_aliases:
                 alias_id = alias["address"].split("\u0000")[-2]
                 if alias_id == id:
                     return account["totalCred"]
         if username and name == username:
             return account["totalCred"]
     raise PluginErrorInternal(f"{username or id} not found in sourcecred instance")
Пример #18
0
    def handle_incoming_webhook(self, request):
        """
        Handler for processing interaction events from Discord.
        https://discord.com/developers/docs/interactions/receiving-and-responding


        The request body is an Interaction object: https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object
        """
        json_data = json.loads(request.body)
        logger.debug(f"received discord request: {json_data}")

        validate_discord_interaction(request)

        if json_data["application_id"] != DISCORD_CLIENT_ID:
            raise PluginErrorInternal(
                "Received event with wrong application ID")

        if json_data["type"] == 1:
            # PING response
            return JsonResponse({"type": 1})

        community_platform_id = str(json_data.get("guild_id"))

        # Process type 2, APPLICATION_COMMAND (a user sending a slash command)
        if json_data["type"] == 2:
            # Pass the interaction event to the Plugin for the Guild it occured in
            for plugin in Discord.objects.filter(
                    community_platform_id=community_platform_id):
                logger.info(f"Passing interaction request to {plugin}")
                response_data = plugin.receive_event(request)
                if response_data:
                    return JsonResponse(response_data)

        # Process type 3, MESSAGE_COMPONENT (a user interacting with an interactive message component that was posted by the bot. for example, clicking a voting button.)
        if json_data["type"] == 3:
            # Pass the interaction event to all active governance processes in this guild
            # TODO: maybe should pass message components to Plugins too, in case bots are posting interactive messages
            active_processes = DiscordVote.objects.filter(
                plugin__community_platform_id=community_platform_id,
                status=ProcessStatus.PENDING.value)
            for process in active_processes:
                logger.info(f"Passing interaction request to {process}")
                response_data = process.receive_webhook(request)
                if response_data:
                    return JsonResponse(response_data)

        return HttpResponse()
Пример #19
0
def get_access_token(installation_id):
    """Get installation access token using installation id"""
    headers = {
        "Accept": "application/vnd.github.v3+json",
        "Authorization": f"Bearer {get_jwt()}"
    }
    url = f"https://api.github.com/app/installations/{installation_id}/access_tokens"
    resp = requests.request("POST", url, headers=headers)

    if not resp.ok:
        logger.error(
            f"Error refreshing token: status {resp.status_code}, details: {resp.text}"
        )
        raise PluginErrorInternal(resp.text)
    if resp.content:
        token = resp.json()["token"]
        return token
Пример #20
0
    def discourse_request(self, method, route, json=None, data=None):
        url = f"{self.config['server_url']}/{route}"
        logger.info(f"{method} {url}")

        headers = {"Api-Key": self.config["api_key"]}
        resp = requests.request(method,
                                url,
                                headers=headers,
                                json=json,
                                data=data)
        if not resp.ok:
            logger.error(f"{resp.status_code} {resp.reason}")
            logger.error(resp.request.body)
            raise PluginErrorInternal(resp.text)
        if resp.content:
            return resp.json()
        return None
Пример #21
0
    def _make_discord_request(self, route, method="GET", json=None):
        if not route.startswith("/"):
            route = f"/{route}"

        resp = requests.request(
            method,
            f"https://discord.com/api{route}",
            headers={"Authorization": f"Bot {DISCORD_BOT_TOKEN}"},
            json=json,
        )

        if not resp.ok:
            logger.error(f"{resp.status_code} {resp.reason}")
            logger.debug(resp.request.headers)
            logger.debug(resp.request.url)
            raise PluginErrorInternal(resp.text)
        if resp.content:
            return resp.json()
        return None
Пример #22
0
    def initialize(self):
        slug = self.config["collective_slug"]
        response = self.run_query(Queries.collective, {"slug": slug})
        result = response["collective"]
        if result is None:
            raise PluginErrorInternal(f"Collective '{slug}' not found.")

        logger.info("Initialized Open Collective: " + str(result))

        self.state.set("collective_name", result["name"])
        self.state.set("collective_id", result["id"])
        self.state.set("collective_legacy_id", result["legacyId"])
        project_legacy_ids = []
        if result.get("childrenAccounts"):
            project_legacy_ids = [
                node["legacyId"]
                for node in result["childrenAccounts"]["nodes"]
                if node["type"] == "PROJECT"
            ]

        self.state.set("project_legacy_ids", project_legacy_ids)
Пример #23
0
    def github_request(self,
                       method,
                       route,
                       data=None,
                       add_headers=None,
                       refresh=False,
                       use_jwt=False):
        """Makes request to Github. If status code returned is 401 (bad credentials), refreshes the
        access token and tries again. Refresh parameter is used to make sure we only try once."""

        authorization = f"Bearer {get_jwt()}" if use_jwt else f"token {self.state.get('installation_access_token')}"
        headers = {
            "Authorization": authorization,
            "Accept": "application/vnd.github.v3+json"
        }
        if add_headers:
            headers.update(add_headers)

        url = f"https://api.github.com{route}"
        logger.info(f"Making request {method} to {route}")
        resp = requests.request(method, url, headers=headers, json=data)

        if resp.status_code == 401 and refresh == False and use_jwt == False:
            logger.info(f"Bad credentials, refreshing token and retrying")
            self.refresh_token()
            return self.github_request(method=method,
                                       route=route,
                                       data=data,
                                       add_headers=add_headers,
                                       refresh=True)
        if not resp.ok:
            logger.error(
                f"Request error for {method}, {route}; status {resp.status_code}, details: {resp.text}"
            )
            raise PluginErrorInternal(resp.text)
        if resp.content:
            return resp.json()
        return None
Пример #24
0
    def call(self, method_name, **kwargs):
        """
        "Contract calls require a transaction fee (gas) so you will need an access key for the --accountId that will be charged. (near login)"
        Rght now we only support making calls from the "master account"... ?
        """
        contract_id = self.config["contract_id"]

        account = self.create_master_account(
        )  # creates a new provider every time!

        optional_args = {
            key: kwargs[key]
            for key in kwargs.keys() if key in ["gas", "amount"]
        }
        try:
            return account.function_call(
                contract_id=contract_id,
                method_name=method_name,
                args=kwargs.get("args", {}),
                **optional_args,
            )
        except (TransactionError, ViewFunctionError) as e:
            raise PluginErrorInternal(str(e))
Пример #25
0
    def start(self, parameters: Parameters) -> None:
        poll_type = parameters.poll_type
        options = [Bool.YES, Bool.NO] if poll_type == "boolean" else parameters.options
        if options is None:
            raise PluginErrorInternal("Options are required for non-boolean votes")

        self.state.set("parameters", parameters._json)
        self.state.set("poll_type", poll_type)
        self.state.set("options", options)
        self.outcome = {
            "votes": dict([(k, {"users": [], "count": 0}) for k in options]),
        }

        contents = self._construct_content()
        components = self._construct_blocks()
        resp = self.plugin_inst.post_message(text=contents, components=components, channel=parameters.channel)
        logger.debug(resp)

        message_id = resp["id"]
        guild_id = self.plugin.community_platform_id
        self.outcome["message_id"] = message_id
        self.url = f"https://discord.com/channels/{guild_id}/{parameters.channel}/{message_id}"
        self.status = ProcessStatus.PENDING.value
        self.save()