Compare commits

..

No commits in common. "master" and "v0.2.0" have entirely different histories.

5 changed files with 15 additions and 259 deletions

View file

@ -1,37 +0,0 @@
workspace:
base: /maubot-osc
path: src
pipeline:
build:
image: dock.mau.dev/maubot/maubot:latest
commands:
- mbc build /maubot-osc/src -o /maubot-osc/src/
secrets: [mbc_username, mbc_password]
release:
image: plugins/gitea-release
settings:
base_url: https://git.dennogumi.org
api_key:
from_secret: gitea_token
files:
- /maubot-osc/src/*.mbp
checksum:
- sha256
when:
event: tag
notify:
image: plugins/matrix
settings:
homeserver: https://conference.heavensinferno.net
roomid:
from_secret: roomid
accesstoken:
from_secret: access_token
userid:
from_secret: user_id
when:
status:
- failure
- success

View file

@ -6,8 +6,6 @@ SPDX-License-Identifier: CC0-1.0
## maubot-osc
![status-badge](https://ci.dennogumi.org/api/badges/einar/maubot-osc/status.svg)
`maubot-osc` is a simple [maubot](https://maubot.xyz/) plugin to query an [Open Build Service](https://openbuildservice.org/) instance. Basically it replicates a few of the features of the `osc` command line tool used to interact with the OBS.
### Features
@ -23,7 +21,6 @@ These are in addition to what maubot requires by itself.
- lxml
- jinja2
- [cryptocode](https://github.com/gdavid7/cryptocode)
### Installation
@ -33,11 +30,9 @@ Then you need to modify a few configuration parameters for your instance:
- `api_url`: the URL to make API requests to the OBS instance. By default it points to the [public OBS instance of the openSUSE project](https://build.opensuse.org) (`https://api.opensuse.org`).
- `instance_url`: the main URL of the Build Service instance. Defaults to `https://build.opensuse.org`.
- `username`: A valid username for the instance
- `password`: A password for the username. Suggestions on how to improve this are welcome.
- `password`: A password for the username. **Currently kept in cleartext**. Suggestions on how to improve this are welcome.
- `trigger_token`: An OBS access token with the `runservice` capability. See "Creating tokens" below.
- `rebuild_token`: An OBS access token with the `rebuild` capability. See "Creating tokens" below.
- `secret`: A random string used to encrypt the password in the configuration file.
- `acl`: User IDs (`@user:homeserver.com`) which are allowed to interact with the bot. See "Access control lists" below.
### Creating tokens
@ -63,26 +58,9 @@ Save both tokens in the configuration. Should you need to, you can view them lat
Make sure your bot instance is in a room (refer to the maubot docs for how) and then type `!osc help` for help.
### Access control lists (ACLs)
By default, no user is allowed to perform any operation. You need to change the `acl` section of the configuration to add user IDs (`@user:homeserver.com`) allowed to interact with the bot. Users in the `admin` list have full powers over the bot, while users in the `user` list can only perform read-only (e.g. status querying) actions.
This example shows how it works in practice:
```yaml
acl:
admin:
# Fred and Sue will be able to perform all commands
- @fred:myhome.com
- @sue:otherserver.com
user:
# Phil will only be able to run read-only (status, etc.) commands
- @phil:elsewhere.com
```
### Notice
I made this for my own use. Don't expect high quality code or a lot of polish.
I made this in about three or four hours for my own use. Don't expect high quality code or a lot of polish.
### License

View file

@ -34,8 +34,3 @@ repo_aliases:
repository: "KDE_Unstable_Frameworks_openSUSE_Factory"
state: "failed"
arch: "all"
acl:
admin:
- "@CHANGE_ME"
user: []

View file

@ -2,7 +2,7 @@
# SPDX-License-Identifier: CC0-1.0
maubot: 0.1.0
id: org.dennogumi.osc
version: 0.4.0
version: 0.2.0
license: AGPL-3.0-or-later
modules:
- oscbot

View file

@ -2,12 +2,10 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
from dataclasses import dataclass, field
from time import sleep
from typing import Optional, List, Type, Tuple
import aiohttp
import cryptocode
import more_itertools as mit
from lxml import objectify
from jinja2 import BaseLoader, Environment
@ -24,11 +22,13 @@ Only showing packages with {{ state }} state.
{% endif %}"""
STATUS_TEMPLATE = """
{% for package in packages %}
REPO_TEMPLATE = """
#### {{ repo.name }} - {{ repo.arch }}
{% for package in repo.packages %}
{%if package.status != "disabled" %}
{% set build_log_url = "{0}/package/live_build_log/{1}/{2}/{3}/{4}/".format(
base_url, project, package.name, repo.name, repo.arch) %}
{% set build_log_url = "{0}/public/build/{1}/{2}/{3}/{4}/_log".format(
base_url,project, repo.name, repo.arch, package.name) %}
- {{ package.name }} - *[{{ package.status }}]({{ build_log_url }})*
{% else %}
- {{ package.name }} - *{{ package.status }}*
@ -36,21 +36,6 @@ STATUS_TEMPLATE = """
{% endfor %}
"""
ALIAS_TEMPLATE = """
### Configured aliases
{% for alias, contents in repo_aliases.items() %}
#### {{ alias }}
- project: {{ contents.project }}
- repository: {{ contents.repository }}
- package(s): {{ contents.package }}
- architecture(s): {{ contents.arch }}
- state: {{ contents.state }}
{% endfor %}
"""
@dataclass
class BuildResult:
@ -83,7 +68,6 @@ class Config(BaseProxyConfig):
helper.copy("username")
helper.copy("repo_aliases")
helper.copy("acl")
class OSCBot(Plugin):
@ -115,18 +99,6 @@ class OSCBot(Plugin):
return (project, package, repository, state, arch)
def check_acl(self, user_id: str, need_admin: bool = False) -> bool:
acls = self.config["acl"]
if not need_admin:
if user_id in acls["admin"] or acls["user"]:
return True
else:
if user_id in acls["admin"]:
return True
self.log.debug(f"Denied operation as {user_id}")
return False
async def parse_rebuilpac(
self,
project: str,
@ -228,120 +200,6 @@ class OSCBot(Plugin):
async def osc(self) -> None:
pass
@osc.subcommand("alias", help="Manage aliases")
async def alias(self) -> None:
pass
@alias.subcommand("list", help="List configured aliases")
async def list_aliases(self, evt: MessageEvent) -> None:
if not self.check_acl(evt.sender, need_admin=False):
await evt.reply("You are not authorized to perform this action.")
return
body = self.template.from_string(ALIAS_TEMPLATE)
if self.config.get("repo_aliases", None) is None:
await evt.reply("No aliases defined.")
return
message = body.render(repo_aliases=self.config["repo_aliases"])
await evt.respond(message, markdown=True)
@alias.subcommand("edit", help="Edit configured aliases")
@command.argument("alias", "alias name")
@command.argument("project", "project name")
@command.argument("package", "package name", required=False)
@command.argument("repository", "repository", required=False)
@command.argument("arch", "architecture", required=False)
@command.argument("state", "state", required=False)
async def edit_alias(self, evt: MessageEvent,
alias: str,
project: Optional[str] = None,
package: Optional[str] = None,
repository: Optional[str] = None,
arch: Optional[str] = None,
state: Optional[str] = None) -> None:
if not self.check_acl(evt.sender, need_admin=True):
await evt.reply("You are not authorized to perform this action.")
return
if alias not in self.config["repo_aliases"]:
await evt.respond(f"Unknown alias {alias}")
return
alias_data = self.config["repo_aliases"][alias]
if project:
alias_data["project"] = project
if repository:
alias_data["repository"] = repository
if package:
alias_data["package"] = package
if arch:
alias_data["arch"] = alias
if state:
alias_data["state"] = state
self.config["repo_aliases"][alias] = alias_data
self.config.save()
await evt.reply(f"Alias {alias} successfully changed.")
@alias.subcommand("create", help="Create a new alias")
@command.argument("alias", "alias name")
@command.argument("project", "project name")
@command.argument("package", "package name", required=False)
@command.argument("repository", "repository", required=False)
@command.argument("arch", "architecture", required=False)
@command.argument("state", "state", required=False)
async def create_alias(self, evt: MessageEvent,
alias: str,
project: Optional[str] = None,
package: Optional[str] = None,
repository: Optional[str] = None,
arch: Optional[str] = None,
state: Optional[str] = None) -> None:
if not self.check_acl(evt.sender, need_admin=True):
await evt.reply("You are not authorized to perform this action.")
return
repository = "all" if not repository else repository
package = "all" if not package else package
arch = "all" if not arch else arch
state = "all" if not state else state
alias_data = {
"project": project,
"package": package,
"repository": repository,
"arch": arch,
"state": state
}
self.config["repo_aliases"][alias] = alias_data
self.config.save()
await evt.reply(f"Alias {alias} successfully created.")
@alias.subcommand("delete", help="Delete an alias", aliases=("rm", ))
@command.argument("alias", "alias name")
async def delete_alias(self, evt: MessageEvent, alias: str):
if not self.check_acl(evt.sender, need_admin=True):
await evt.reply("You are not authorized to perform this action.")
return
if alias not in self.config["repo_aliases"]:
await evt.respond(f"Unknown alias {alias}")
return
del self.config["repo_aliases"][alias]
self.config.save()
await evt.reply(f"Alias {alias} successfully deleted.")
@osc.subcommand(
"rebuildpac", aliases=("rb",),
help="Rebuild a package or all packages in the repositories")
@ -355,18 +213,9 @@ class OSCBot(Plugin):
repository: Optional[str] = None,
arch: Optional[str] = None) -> None:
if not self.check_acl(evt.sender, need_admin=True):
await evt.reply("You are not authorized to perform this action.")
return
if project in self.config["repo_aliases"]:
# We're not interested in state and we query package explicitly
project, _, repository, _, arch = self.get_alias(project)
else:
repository = None if repository == "all" else repository
arch = None if arch == "all" else arch
package = None if package == "all" else package
repository = None if repository == "all" else repository
arch = None if arch == "all" else arch
result, status = await self.parse_rebuilpac(project, package,
repository,
@ -387,10 +236,6 @@ class OSCBot(Plugin):
project: str,
package: str) -> None:
if not self.check_acl(evt.sender, need_admin=True):
await evt.reply("You are not authorized to perform this action.")
return
token = self.config["trigger_token"]
trigger_url = f"{self.config['api_url']}/trigger/runservice"
params = {"project": project, "package": package}
@ -420,10 +265,6 @@ class OSCBot(Plugin):
repository: Optional[str] = None,
arch: Optional[str] = None) -> None:
if not self.check_acl(evt.sender, need_admin=False):
await evt.reply("You are not authorized to perform this action.")
return
if project in self.config["repo_aliases"]:
project, package, repository, state, arch = self.get_alias(project)
else:
@ -446,29 +287,8 @@ class OSCBot(Plugin):
base_url = self.config["instance_url"]
if len(response) > 50:
await evt.respond(f"Too many repositories ({len(response)})"
f"to display in project {project}, "
"please narrow down your search.")
return
for repository in response:
await evt.respond(f"#### {repository.name} - {repository.arch}",
markdown=True)
# Just so that someone doesn't flood by listing the whole
# openSUSE:Factory
if len(repository.packages) > 1000:
await evt.respond(
f"Too many packages ({len(repository.packages)}) "
"to display, please narrow down your search.")
continue
# To avoid creating too large messages, we chunk packages in
# groups of 100
for packagelist in mit.chunked(repository.packages, 100):
body = self.template.from_string(STATUS_TEMPLATE)
message = body.render(packages=packagelist, base_url=base_url,
project=project, repo=repository)
await evt.respond(message, markdown=True)
# Wait 200 milliseconds to avoid triggering rate limiting
sleep(0.2)
body = self.template.from_string(REPO_TEMPLATE)
message = body.render(repo=repository, project=project,
base_url=base_url)
await evt.respond(message, markdown=True)