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()
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")
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, )
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)
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 })
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? )
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
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, )
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
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")
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()
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()
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()
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)
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")
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")