Esempio n. 1
0
def enable_integration(request, integration):
    """
    API Endpoint to enable a Metagov Plugin. This gets called on config form submission from JS.
    This is the default implementation; platforms with PolicyKit integrations may override it.
    """
    user = get_user(request)
    community = user.community.community

    config = json.loads(request.body)
    logger.debug(
        f"Enabling {integration} with config {config} for {community}")
    plugin = metagov.get_community(community.metagov_slug).enable_plugin(
        integration, config)

    # Create the corresponding CommunityPlatform instance
    from django.apps import apps
    cls = apps.get_app_config(integration).get_model(f"{integration}community")
    cp, created = cls.objects.get_or_create(
        community=community,
        team_id=plugin.community_platform_id,
        defaults={"community_name": plugin.community_platform_id})
    logger.debug(
        f"CommunityPlatform '{cp.platform} {cp}' {'created' if created else 'already exists'}"
    )

    return HttpResponse()
Esempio n. 2
0
def disable_integration(request, integration):
    """
    API Endpoint to disable a Metagov plugin (navigated to from Settings page).
    This is the default implementation; platforms with PolicyKit integrations may override it.
    """
    id = int(request.GET.get("id"))  # id of the plugin
    user = get_user(request)
    community = user.community.community
    logger.debug(
        f"Deleting plugin {integration} {id} for community {community}")

    # Delete the Metagov Plugin
    metagov.get_community(community.metagov_slug).disable_plugin(integration,
                                                                 id=id)

    # Delete the PlatformCommunity
    community_platform = community.get_platform_community(name=integration)
    if community_platform:
        community_platform.delete()

    return redirect("/main/settings")
Esempio n. 3
0
 def process_expense(self, expense_id, action):
     mg_community = metagov.get_community(self.community.metagov_slug)
     return mg_community.perform_action(
         plugin_name="opencollective",
         action_id="process-expense",
         parameters={
             "expense_id": expense_id,
             "action": action
         },
         jsonschema_validation=True,
         community_platform_id=self.team_id,
     )
Esempio n. 4
0
def settings_page(request):
    """
    Settings page for enabling/disabling platform integrations.
    """
    user = get_user(request)
    community = user.community

    context = {
        "user": user,
        "enabled_integrations": [],
        "disabled_integrations": []
    }

    if community.metagov_slug:
        enabled_integrations = {}
        # Iterate through all Metagov Plugins enabled for this community
        for plugin in metagov.get_community(
                community.metagov_slug).plugins.all():
            integration = plugin.name
            if integration not in integration_data.keys():
                logger.warn(
                    f"unsupported integration {integration} is enabled for community {community}"
                )
                continue

            # Only include configs if user has permission, since they may contain API Keys
            config_tuples = []
            if user.has_role(INTEGRATION_ADMIN_ROLE_NAME):
                for (k, v) in plugin.config.items():
                    readable_key = k.replace("_",
                                             " ").replace("-",
                                                          " ").capitalize()
                    config_tuples.append((readable_key, v))

            # Add additional data about the integration, like description and webhook URL
            additional_data = integration_data[integration]
            if additional_data.get("webhook_instructions"):
                additional_data[
                    "webhook_url"] = f"{settings.SERVER_URL}/api/hooks/{plugin.name}/{plugin.community.slug}"

            enabled_integrations[integration] = {
                **plugin.serialize(),
                **additional_data, "config": config_tuples
            }

        context["enabled_integrations"] = enabled_integrations.items()
        context["disabled_integrations"] = [
            (k, v) for (k, v) in integration_data.items()
            if k not in enabled_integrations.keys()
        ]

    return render(request, 'policyadmin/dashboard/settings.html', context)
Esempio n. 5
0
    def setUp(self):
        # Set up a Slack community and a user
        self.slack_community, self.user = TestUtils.create_slack_community_and_user(
        )
        self.community = self.slack_community.community

        # Enable a plugin to use in tests
        self.metagov_community = metagov.get_community(
            self.community.metagov_slug)
        self.metagov_community.enable_plugin("randomness", {
            "default_low": 2,
            "default_high": 200
        })
Esempio n. 6
0
    def perform_action(self, name, **kwargs):
        """
        Perform an action through Metagov. If the requested action belongs to a plugin that is
        not active for the current community, this will throw an exception.
        """
        community = metagov.get_community(self.metagov_slug)
        plugin_name, action_id = name.split(".")

        return community.perform_action(
            plugin_name,
            action_id,
            parameters=kwargs,
            community_platform_id=None  # FIXME pass team_id?
        )
Esempio n. 7
0
    def start_process(self, process_name, **kwargs) -> MetagovProcessData:
        """
        Kick off a governance process in Metagov. Store the process URL and data on the `proposal`
        """

        community = metagov.get_community(self.metagov_slug)
        plugin_name, process_name = process_name.split(".")
        plugin = community.get_plugin(plugin_name)
        process = plugin.start_process(process_name, **kwargs)

        # store reference to process on the proposal
        self.proposal.governance_process = process
        self.proposal.save()
        return process
Esempio n. 8
0
    def post_message(self, proposal, text, expense_id):
        # TODO: could use the 'proposal' to try to infer the expense_id (if proposal was triggered by an expense event)

        mg_community = metagov.get_community(self.community.metagov_slug)
        return mg_community.perform_action(
            plugin_name="opencollective",
            action_id="create-comment",
            parameters={
                "raw": text,
                "expense_id": expense_id
            },
            jsonschema_validation=True,
            community_platform_id=self.team_id,
        )
Esempio n. 9
0
    def find_linked_username(self, platform):
        mg_community = metagov.get_community(self.community.metagov_slug)
        from metagov.core import identity

        users = identity.get_users(
            community=mg_community,
            platform_type=self.community.platform,
            community_platform_id=self.community.team_id,
            platform_identifier=self.username)
        logger.debug(f"Users linked to {self}: {users}")
        if len(users) > 1:
            raise Exception("More than 1 matching user found")
        if len(users) == 1:
            for account in users[0]["linked_accounts"]:
                if account["platform_type"] == platform:
                    return account["platform_identifier"]
        return None
Esempio n. 10
0
def github_install(request):
    """
    Gets called after the oauth install flow is completed. This is the redirect_uri that was passed to the oauth flow.
    """
    logger.debug(f"GitHub installation completed: {request.GET}")

    # Metagov identifier for the "parent community" to install GitHub to
    metagov_community_slug = request.GET.get("community")
    # We can only add GitHub to existing communities for now, so redirect to the settings page
    redirect_route = "/main/settings"

    try:
        community = Community.objects.get(metagov_slug=metagov_community_slug)
    except Community.DoesNotExist:
        return redirect(f"{redirect_route}?error=community_not_found")

    if request.GET.get("error"):
        return redirect(f"{redirect_route}?error={request.GET.get('error')}")

    plugin = metagov.get_community(metagov_community_slug).get_plugin("github")
    team_id = plugin.community_platform_id

    github_community = GithubCommunity.objects.filter(team_id=team_id,
                                                      community=community)

    if github_community.exists():
        logger.debug(
            f"Github community for installation {team_id} already exists, doing nothing."
        )
        return redirect(f"{redirect_route}?success=true")

    logger.debug(f"Creating new GithubCommunity under {community}")
    data = plugin.get_installation()
    readable_name = data["account"]["login"]
    target_type = data["target_type"]  # User or Organization

    github_community = GithubCommunity.objects.create(
        community=community,
        community_name=readable_name,
        team_id=team_id,
    )

    return redirect(f"{redirect_route}?success=true")
Esempio n. 11
0
    def initiate_vote(self,
                      proposal,
                      title,
                      closing_at,
                      options,
                      details=None,
                      poll_type="proposal",
                      **kwargs):
        """
        Start a new poll on Loomio.

        Accepted keyword args:
            subgroup,
            specified_voters_only,
            hide_results_until_closed,
            anonymous,
            discussion_id,
            voter_can_add_options,
            recipient_audience,
            notify_on_closing_soon,
            recipient_user_ids,
            recipient_emails,
            recipient_message
        """

        if isinstance(closing_at, datetime.datetime):
            closing_at = closing_at.strftime("%Y-%m-%d")

        # Kick off process in Metagov
        mg_community = metagov.get_community(self.community.metagov_slug)
        plugin = mg_community.get_plugin("loomio")
        process = plugin.start_process(
            "poll",
            title=title,
            closing_at=closing_at,
            poll_type=poll_type,
            options=options,
            details=details or title,
            # pass on any other kwargs
            **kwargs,
        )
        proposal.governance_process = process
        proposal.save()
Esempio n. 12
0
    def initiate_vote(self,
                      proposal,
                      users=None,
                      post_type="channel",
                      text=None,
                      channel=None,
                      options=None):
        args = SlackUtils.construct_vote_params(proposal, users, post_type,
                                                text, channel, options)

        # get plugin instance
        plugin = metagov.get_community(self.community.metagov_slug).get_plugin(
            "slack", self.team_id)
        # start process
        process = plugin.start_process("emoji-vote", **args)
        # save reference to process on the proposal, so we can link up the signals later
        proposal.governance_process = process
        proposal.vote_post_id = process.outcome["message_ts"]
        logger.debug(
            f"Saving proposal with vote_post_id '{proposal.vote_post_id}'")
        proposal.save()
Esempio n. 13
0
def post_delete_community(sender, instance, **kwargs):
    # After deleting a Community, delete it in Metagov too.
    if instance.metagov_slug:
        metagov.get_community(instance.metagov_slug).delete()
Esempio n. 14
0
 def metagov_plugin(self):
     mg_community = metagov.get_community(self.metagov_slug)
     team_id = getattr(self, "team_id", None)
     return mg_community.get_plugin(self.platform,
                                    community_platform_id=team_id)
Esempio n. 15
0
def discord_install(request):
    """
    Gets called after the oauth install flow is completed. This is the redirect_uri that was passed to the oauth flow.
    """
    logger.debug(f"Discord installation completed: {request.GET}")

    # metagov identifier for the "parent community" to install Discord to
    metagov_community_slug = request.GET.get("community")
    community, is_new_community = Community.objects.get_or_create(
        metagov_slug=metagov_community_slug)

    # if we're enabling an integration for an existing community, so redirect to the settings page
    redirect_route = "/login" if is_new_community else "/main/settings"

    if request.GET.get("error"):
        return redirect(f"{redirect_route}?error={request.GET.get('error')}")

    # TODO(issue): stop passing user id and token
    user_id = request.GET.get("user_id")
    user_token = request.GET.get("user_token")
    guild_id = request.GET.get("guild_id")

    # Fetch guild info to get the guild name
    # TODO: catch Plugin.DoesNotExist
    mg_community = metagov.get_community(community.metagov_slug)
    discord_plugin = mg_community.get_plugin("discord", guild_id)
    guild_info = discord_plugin.get_guild()
    logger.debug(
        f"Found Metagov Plugin {discord_plugin} with guild info: {guild_info}")
    guild_name = guild_info["name"]

    # Register/update the default PolicyKit guild command
    discord_plugin.register_guild_command(
        DISCORD_SLASH_COMMAND_NAME,
        description=DISCORD_SLASH_COMMAND_DESCRIPTION,
        options=[{
            "name": DISCORD_SLASH_COMMAND_OPTION,
            "description": "Command",
            "type": 3,
            "required": True
        }],
    )

    discord_community = DiscordCommunity.objects.filter(
        team_id=guild_id).first()
    if discord_community is None:
        logger.debug(
            f"Creating new DiscordCommunity for guild '{guild_name}' under {community}"
        )
        discord_community = DiscordCommunity.objects.create(
            community=community,
            community_name=guild_name,
            team_id=guild_id,
        )

        # Get the list of users and create a DiscordUser object for each user
        guild_members = discord_plugin.method(
            route=f"guilds/{guild_id}/members?limit=1000")
        owner_id = guild_info["owner_id"]
        creator_user = None
        for member in guild_members:
            if member["user"].get("bot"):
                continue

            member_user_id = member["user"]["id"]
            u, _ = discord_community._update_or_create_user(member["user"])

            # If this user is the user that's installing discord, mark them as an admin and store their token
            if user_token and user_id and member_user_id == user_id:
                logger.debug(
                    f"Storing access_token for installing user ({user_id})")
                u.is_community_admin = True
                u.access_token = user_token
                u.save()
                creator_user = u

            # Make guild owner and admin by default
            if member_user_id == owner_id:
                u.is_community_admin = True
                u.save()

        if is_new_community:
            # if this is an entirely new parent community, select starter kit
            return render_starterkit_view(
                request,
                discord_community.community.pk,
                creator_username=creator_user.username
                if creator_user else None)
        else:
            # discord is being added to an existing community that already has other platforms and starter policies
            return redirect(f"{redirect_route}?success=true")

    else:
        logger.debug("community already exists, updating name..")
        discord_community.community_name = guild_name
        discord_community.save()

        # Store token for the user who (re)installed Discord
        if user_token and user_id:
            installer, created = discord_community._update_or_create_user(
                {"id": user_id})
            installer.is_community_admin = True
            installer.access_token = user_token
            installer.save()

            if created:
                logger.debug(
                    f"Installer user '{user_id}' is a new user, fetching user details..."
                )
                user_data = discord_community.make_call(
                    "discord.get_user", {"user_id": user_id})
                discord_community._update_or_create_user(user_data)

        return redirect(f"{redirect_route}?success=true")
Esempio n. 16
0
def slack_install(request):
    """
    Gets called after the oauth install flow is completed. This is the redirect_uri that was passed to the oauth flow.
    """
    logger.debug(f"Slack installation completed: {request.GET}")

    # metagov identifier for the "parent community" to install Slack to
    metagov_community_slug = request.GET.get("community")
    community, is_new_community = Community.objects.get_or_create(metagov_slug=metagov_community_slug)

    # if we're adding slack to an existing community, redirect to the settings page
    redirect_route = "/login" if is_new_community else "/main/settings"

    if request.GET.get("error"):
        return redirect(f"{redirect_route}?error={request.GET.get('error')}")

    # TODO(issue): stop passing user id and token
    user_id = request.GET.get("user_id")
    user_token = request.GET.get("user_token")
    team_id = request.GET.get("team_id")

    mg_community = metagov.get_community(community.metagov_slug)

    # TODO: catch Plugin.DoesNotExist
    slack_plugin = mg_community.get_plugin("slack", team_id)
    team_info = slack_plugin.method(method_name="team.info")
    team = team_info["team"]
    team_id = team["id"]
    readable_name = team["name"]

    slack_community = SlackCommunity.objects.filter(team_id=team_id).first()
    if slack_community is None:
        logger.debug(f"Creating new SlackCommunity under {community}")
        slack_community = SlackCommunity.objects.create(
            community=community, community_name=readable_name, team_id=team_id
        )

        # get the list of users, create SlackUser object for each user
        logger.debug(f"Fetching user list for {slack_community}...")
        # from policyengine.models import LogAPICall

        response = slack_plugin.method(method_name="users.list")

        for new_user in response["members"]:
            if (not new_user["deleted"]) and (not new_user["is_bot"]) and (new_user["id"] != "USLACKBOT"):
                u, _ = SlackUser.objects.get_or_create(
                    username=new_user["id"],
                    readable_name=new_user["real_name"],
                    avatar=new_user["profile"]["image_24"],
                    community=slack_community,
                )
                if user_token and user_id and new_user["id"] == user_id:
                    logger.debug(f"Storing access_token for installing user ({user_id})")
                    # Installer has is_community_admin because they are an admin in Slack, AND we requested special user scopes from them
                    u.is_community_admin = True
                    u.access_token = user_token
                    u.save()

        if is_new_community:
            return render_starterkit_view(request, slack_community.community.pk, creator_username=user_id)
        else:
            return redirect(f"{redirect_route}?success=true")

    else:
        logger.debug("community already exists, updating name..")
        slack_community.community_name = readable_name
        slack_community.save()

        # Store token for the user who (re)installed Slack
        if user_token and user_id:
            installer = SlackUser.objects.filter(community=slack_community, username=user_id).first()
            if installer is not None:
                logger.debug(f"Storing access_token for installing user ({user_id})")
                # Installer has is_community_admin because they are an admin in Slack, AND we requested special user scopes from them
                installer.is_community_admin = True
                installer.access_token = user_token
                installer.save()
            else:
                logger.debug(f"User '{user_id}' is re-installing but no SlackUser exists for them, creating one..")
                response = slack_community.make_call("slack.method", {"method_name": "users.info", "user": user_id})
                user_info = response["user"]
                user_fields = get_slack_user_fields(user_info)
                user_fields["is_community_admin"] = True
                user_fields["access_token"] = user_token
                SlackUser.objects.update_or_create(
                    community=slack_community,
                    username=user_info["id"],
                    defaults=user_fields,
                )

        return redirect(f"{redirect_route}?success=true")