first commit
This commit is contained in:
commit
7b3fcdb9c1
12 changed files with 788 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
venv
|
||||
*.mbp
|
||||
*.db
|
||||
*.log
|
||||
plugins/*
|
||||
config.yaml
|
0
README.md
Normal file
0
README.md
Normal file
21
base-config.yaml
Normal file
21
base-config.yaml
Normal file
|
@ -0,0 +1,21 @@
|
|||
update_interval: 5
|
||||
whitelist:
|
||||
- '@ravage:xentonix.net'
|
||||
rooms:
|
||||
'#release:ubuntu.com':
|
||||
queue: [New, Unapproved]
|
||||
tracker: Builds
|
||||
packageset: Packageset
|
||||
mute: []
|
||||
'#lubuntu-devel:ubuntu.com':
|
||||
tracker: Builds
|
||||
tracker_filter: lubuntu
|
||||
queue: [New, Unapproved]
|
||||
queue_filter: lubuntu
|
||||
packageset: Packageset
|
||||
packageset_filter: lubuntu
|
||||
mute: []
|
||||
'#queuebot:xentonix.net':
|
||||
queue: [Unapproved]
|
||||
packageset: Packageset
|
||||
mute: []
|
14
maubot.yaml
Normal file
14
maubot.yaml
Normal file
|
@ -0,0 +1,14 @@
|
|||
id: com.ubuntu.qbot
|
||||
version: 1.0.1
|
||||
modules:
|
||||
- queuebot
|
||||
main_class: Queuebot
|
||||
maubot: 0.1.0
|
||||
database: false
|
||||
config: true
|
||||
webapp: false
|
||||
license: MIT
|
||||
extra_files:
|
||||
- base-config.yaml
|
||||
dependencies: []
|
||||
soft_dependencies: []
|
1
queuebot/__init__.py
Normal file
1
queuebot/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .bot import Queuebot
|
230
queuebot/bot.py
Normal file
230
queuebot/bot.py
Normal file
|
@ -0,0 +1,230 @@
|
|||
from __future__ import annotations
|
||||
import json, os, re, requests, asyncio, pytz, importlib, traceback, logging
|
||||
from aiohttp.web import Request, Response, json_response
|
||||
from datetime import datetime, timedelta
|
||||
from launchpadlib.launchpad import Launchpad
|
||||
from maubot import Plugin, MessageEvent
|
||||
from maubot.handlers import command
|
||||
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
|
||||
from mautrix.util import background_task
|
||||
from mautrix.types import (
|
||||
EventType,
|
||||
MemberStateEventContent,
|
||||
PowerLevelStateEventContent,
|
||||
RoomID,
|
||||
RoomAlias,
|
||||
StateEvent,
|
||||
UserID,
|
||||
)
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
from typing import Type, Tuple
|
||||
from urllib.parse import urlparse, unquote
|
||||
from .plugs import queue, tracker, packageset
|
||||
from .floodprotection import FloodProtection
|
||||
|
||||
qbot_change_level = EventType.find("com.ubuntu.qbot", t_class=EventType.Class.STATE)
|
||||
|
||||
class Config(BaseProxyConfig):
|
||||
def do_update(self, helper: ConfigUpdateHelper) -> None:
|
||||
helper.copy("whitelist")
|
||||
helper.copy("rooms")
|
||||
|
||||
class Queuebot(Plugin):
|
||||
reminder_loop_task: asyncio.Future
|
||||
VERBOSE=False
|
||||
plugin_queue_new = queue.Queue("New", VERBOSE)
|
||||
plugin_queue_unapproved = queue.Queue("Unapproved", VERBOSE)
|
||||
plugin_packageset = packageset.Packageset("Packageset", VERBOSE)
|
||||
plugin_tracker = tracker.Tracker("Builds", VERBOSE)
|
||||
room_ids = []
|
||||
room_mapping = {}
|
||||
power_level_cache: dict[RoomID, tuple[int, PowerLevelStateEventContent]]
|
||||
|
||||
async def start(self) -> None:
|
||||
self.config.load_and_update()
|
||||
self.flood_protection = FloodProtection()
|
||||
self.power_level_cache = {}
|
||||
#logger = logging.getLogger(self.id)
|
||||
#logger.setLevel(logging.INFO)
|
||||
#self.log = logger
|
||||
if await self.resolve_room_aliases():
|
||||
self.poll_task = asyncio.create_task(self.poll_plugins())
|
||||
self.log.info("Queuebot started")
|
||||
|
||||
async def stop(self) -> None:
|
||||
await super().stop()
|
||||
self.poll_task.cancel()
|
||||
|
||||
async def get_power_levels(self, room_id: RoomID) -> PowerLevelStateEventContent:
|
||||
try:
|
||||
expiry, levels = self.power_level_cache[room_id]
|
||||
if expiry < int(time()):
|
||||
return levels
|
||||
except KeyError:
|
||||
pass
|
||||
levels = await self.client.get_state_event(room_id, EventType.ROOM_POWER_LEVELS)
|
||||
now = int(time())
|
||||
self.power_level_cache[room_id] = (now + 5 * 60, levels)
|
||||
return levels
|
||||
|
||||
async def can_manage(self, evt: MessageEvent) -> bool:
|
||||
if evt.sender in self.config["whitelist"]:
|
||||
return True
|
||||
levels = await self.get_power_levels(evt.room_id)
|
||||
user_level = levels.get_user_level(evt.sender)
|
||||
state_level = levels.get_event_level(qbot_change_level)
|
||||
if not isinstance(state_level, int):
|
||||
state_level = 50
|
||||
if user_level < state_level:
|
||||
return False
|
||||
return True
|
||||
|
||||
@command.new(name="qbot", require_subcommand=False)
|
||||
async def qbot(self, evt: MessageEvent) -> None:
|
||||
if not await self.can_manage(evt) and self.flood_protection.flood_check(evt.sender):
|
||||
await evt.respond("You don't have the permission to use this command.")
|
||||
return False
|
||||
await evt.respond("Invalid argument. Example: !qbot mute queue")
|
||||
return False
|
||||
@qbot.subcommand("mute", aliases=["unmute"])
|
||||
@command.argument("plugin", "(un)mute a plugin", required=False)
|
||||
async def mute(self, evt: MessageEvent, plugin: str) -> None:
|
||||
if not await self.can_manage(evt) and self.flood_protection.flood_check(evt.sender):
|
||||
await evt.respond("You don't have the permission to manage mutes.")
|
||||
return False
|
||||
if not plugin or plugin not in ["queue", "tracker", "packageset"]:
|
||||
await evt.respond("Invalid plugin. Valid plugins are queue, tracker and packageset. Example: !qbot mute queue")
|
||||
return False
|
||||
room_alias = self.room_mapping[evt.room_id]
|
||||
if plugin in self.config["rooms"][room_alias]['mute']:
|
||||
self.config["rooms"][room_alias]['mute'].remove(plugin)
|
||||
await evt.respond(f"Unmuted {plugin}")
|
||||
else:
|
||||
self.config["rooms"][room_alias]['mute'].append(plugin)
|
||||
await evt.respond(f"Muted {plugin}")
|
||||
self.config.save()
|
||||
|
||||
async def resolve_room_alias_to_id(self, room_alias: str):
|
||||
try:
|
||||
room_id = await self.client.resolve_room_alias(RoomAlias(room_alias))
|
||||
return room_id
|
||||
except Exception as e:
|
||||
self.log.error(f"Error resolving room alias {room_alias}: {e}")
|
||||
self.log.debug(traceback.format_exc())
|
||||
return None
|
||||
|
||||
async def resolve_room_aliases(self):
|
||||
self.room_ids = []
|
||||
self.room_mapping = {}
|
||||
for room_alias in self.config["rooms"]:
|
||||
if room_alias.startswith("#"):
|
||||
if room_id_obj := await self.resolve_room_alias_to_id(room_alias):
|
||||
room_id = str(room_id_obj.room_id)
|
||||
self.room_ids.append(room_id)
|
||||
self.room_mapping[room_id] = room_alias
|
||||
self.log.info("Added room " + room_alias + " with id " + room_id)
|
||||
elif room_alias.startswith("!"):
|
||||
self.room_ids.append(room_alias)
|
||||
self.room_mapping[room_alias] = room_alias
|
||||
self.log.info("Added room id " + room_alias)
|
||||
else:
|
||||
self.log.debug("Error addming room " + room_alias)
|
||||
return True
|
||||
|
||||
def check_access_sender(self, sender):
|
||||
if sender in self.config["whitelist"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_plugin_filter_mute(self, plugin_name, queue, notice, room_id, room_alias):
|
||||
try:
|
||||
# is the plugin enabled
|
||||
if self.config['rooms'][room_alias].get(plugin_name) is None:
|
||||
self.log.debug(f"plugin_name {plugin_name} is None for {room_alias}")
|
||||
return False
|
||||
queues = self.config['rooms'][room_alias].get(plugin_name)
|
||||
if isinstance(queues, list):
|
||||
if queue not in queues:
|
||||
self.log.debug(f"queue {plugin_name}.{queue} not queues for {room_alias}")
|
||||
return False
|
||||
elif isinstance(queues, str):
|
||||
if queue != queues:
|
||||
self.log.debug(f"queue {plugin_name}.{queue} not in queue string for {room_alias}")
|
||||
return False
|
||||
else:
|
||||
self.log.debug(f"queues is not str or list for {room_alias}")
|
||||
return False
|
||||
|
||||
if self.config['rooms'][room_alias].get('mute') is None:
|
||||
return True
|
||||
mutes = self.config['rooms'][room_alias].get('mute')
|
||||
mute_name = f"{plugin_name}.{queue}".lower()
|
||||
if not isinstance(mutes, list) or len(mutes) < 1:
|
||||
self.log.debug(f"mutes in {room_alias} is not a valid list or empty")
|
||||
elif mute_name in mutes:
|
||||
self.log.debug(f"{mute_name} is in mutes for {room_alias}")
|
||||
return False
|
||||
# is there a filter
|
||||
filter_name = plugin_name + '_filter'.lower()
|
||||
if self.config['rooms'][room_alias].get(filter_name) is not None:
|
||||
if str(self.config['rooms'][room_alias][filter_name]).lower() not in notice.lower():
|
||||
self.log.debug(f"not sending notice to {room_alias} as it matches filter " + str(self.config['rooms'][room_alias][filter_name]).lower() )
|
||||
return False
|
||||
except Exception as e:
|
||||
self.log.debug("Error checking filter or mute: " + str(e))
|
||||
self.log.debug(traceback.format_exc())
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def poll_plugins(self) -> None:
|
||||
try:
|
||||
await self._poll_plugins()
|
||||
except asyncio.CancelledError:
|
||||
self.log.info("Polling stopped")
|
||||
except Exception:
|
||||
self.log.exception("Fatal error while polling plugins")
|
||||
async def _poll_plugins(self) -> None:
|
||||
self.log.info("Polling started")
|
||||
while True:
|
||||
try:
|
||||
await self._poll_once()
|
||||
except asyncio.CancelledError:
|
||||
self.log.info("Polling stopped")
|
||||
except Exception:
|
||||
self.log.exception("Error while polling plugins")
|
||||
self.log.debug("Sleeping " + str(self.config["update_interval"] * 60) + " seconds")
|
||||
await asyncio.sleep(self.config["update_interval"] * 60)
|
||||
async def _poll_once(self) -> None:
|
||||
try:
|
||||
plugins_to_poll = [self.plugin_queue_new, self.plugin_queue_unapproved, self.plugin_tracker, self.plugin_packageset]
|
||||
for plugin_name in plugins_to_poll:
|
||||
if not hasattr(plugin_name, 'update'):
|
||||
continue
|
||||
notices = plugin_name.update()
|
||||
self.log.debug('update() function called on ' + str(plugin_name.name) + '.' + str(plugin_name.queue))
|
||||
if notices:
|
||||
self.log.debug(f"New notices available")
|
||||
if await self.resolve_room_aliases():
|
||||
for notice in notices:
|
||||
for room_id in self.room_ids:
|
||||
self.log.debug(f"Checking notices or {room_id}")
|
||||
try:
|
||||
room_alias = self.room_mapping[room_id]
|
||||
self.log.debug(f"Checking notices or {room_alias} ( {room_id} )")
|
||||
if self.check_plugin_filter_mute(plugin_name=plugin_name.name,queue=plugin_name.queue, notice=notice[0], room_id=room_id, room_alias=room_alias):
|
||||
self.log.debug(f"new notice from {plugin_name.name}.{plugin_name.queue} to {room_alias}")
|
||||
await self.client.send_notice(room_id, notice[0])
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error sending notice to {room_id}: {e}")
|
||||
self.log.debug(traceback.format_exc())
|
||||
pass
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error polling plugins: {e}")
|
||||
self.log.debug(traceback.format_exc())
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_config_class(cls) -> Type[BaseProxyConfig]:
|
||||
return Config
|
25
queuebot/floodprotection.py
Normal file
25
queuebot/floodprotection.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from collections import defaultdict
|
||||
from time import time
|
||||
|
||||
class FloodProtection:
|
||||
def __init__(self):
|
||||
self.user_commands = defaultdict(list) # Stores timestamps of commands for each user
|
||||
self.max_commands = 3
|
||||
self.time_window = 60 # 60 seconds
|
||||
|
||||
def flood_check(self, user_id):
|
||||
"""Check if a user can send a command based on flood protection limits."""
|
||||
current_time = time()
|
||||
if user_id not in self.user_commands:
|
||||
self.user_commands[user_id] = [current_time]
|
||||
return True # Allow the command if the user has no recorded commands
|
||||
|
||||
# Remove commands outside the time window
|
||||
self.user_commands[user_id] = [timestamp for timestamp in self.user_commands[user_id] if current_time - timestamp < self.time_window]
|
||||
|
||||
if len(self.user_commands[user_id]) < self.max_commands:
|
||||
self.user_commands[user_id].append(current_time)
|
||||
return True # Allow the command if under the limit
|
||||
|
||||
# Otherwise, do not allow the command
|
||||
return False
|
0
queuebot/plugs/__init__.py
Normal file
0
queuebot/plugs/__init__.py
Normal file
113
queuebot/plugs/packageset.py
Normal file
113
queuebot/plugs/packageset.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
#!/usr/bin/python
|
||||
from __future__ import print_function
|
||||
|
||||
import traceback
|
||||
import threading
|
||||
from launchpadlib.launchpad import Launchpad
|
||||
|
||||
|
||||
class PackagesetScanner(threading.Thread):
|
||||
notices = list()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
# Authenticated login to Launchpad
|
||||
self.lp = Launchpad.login_anonymously(
|
||||
'maubot-queuebot', 'production',
|
||||
launchpadlib_dir="/tmp/queuebot-%s/" % self.queue)
|
||||
|
||||
self.notices = list()
|
||||
|
||||
ubuntu = self.lp.distributions['ubuntu']
|
||||
ubuntu_series = [series for series in ubuntu.series
|
||||
if series.active]
|
||||
|
||||
# In verbose mode, show the current content of the queue
|
||||
if self.verbose and self.queue not in self.queue_state:
|
||||
self.queue_state[self.queue] = set()
|
||||
|
||||
# Get the content of the current queue
|
||||
new_list = set()
|
||||
for series in ubuntu_series:
|
||||
for pkgset in self.lp.packagesets.getBySeries(
|
||||
distroseries=series):
|
||||
for pkg in list(pkgset.getSourcesIncluded()):
|
||||
new_list.add(";".join([
|
||||
series.self_link,
|
||||
series.name,
|
||||
pkgset.name,
|
||||
pkg
|
||||
]))
|
||||
|
||||
if self.queue in self.queue_state:
|
||||
if len(new_list - self.queue_state[self.queue]) > 25:
|
||||
self.notices.append(("%s: %s entries have been"
|
||||
" added or removed" %
|
||||
(self.queue,
|
||||
len(new_list -
|
||||
self.queue_state[self.queue])),
|
||||
['packageset']))
|
||||
elif len(self.queue_state[self.queue] - new_list) > 25:
|
||||
self.notices.append(("%s: %s entries have been"
|
||||
" added or removed" %
|
||||
(self.queue,
|
||||
len(self.queue_state[self.queue] -
|
||||
new_list)),
|
||||
['packageset']))
|
||||
else:
|
||||
# Print removed packages
|
||||
for pkg in sorted(self.queue_state[self.queue] - new_list):
|
||||
pkg_seriesurl, pkg_series, pkg_set, \
|
||||
pkg_name = pkg.split(';')
|
||||
|
||||
self.notices.append(("%s: Removed %s from %s in %s" % (
|
||||
self.queue, pkg_name, pkg_set, pkg_series),
|
||||
['packageset']))
|
||||
|
||||
# Print added packages
|
||||
for pkg in sorted(new_list - self.queue_state[self.queue]):
|
||||
pkg_seriesurl, pkg_series, pkg_set, \
|
||||
pkg_name = pkg.split(';')
|
||||
|
||||
self.notices.append(("%s: Added %s to %s in %s" % (
|
||||
self.queue, pkg_name, pkg_set, pkg_series),
|
||||
['packageset']))
|
||||
|
||||
self.queue_state[self.queue] = new_list
|
||||
except:
|
||||
# We don't want the bot to crash when LP fails
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class Packageset():
|
||||
queue_state = dict()
|
||||
scanner = PackagesetScanner()
|
||||
name = "packageset"
|
||||
queue = ""
|
||||
|
||||
def __init__(self, queue, verbose=False):
|
||||
self.queue = queue
|
||||
self.verbose = verbose
|
||||
self.spawn_scanner()
|
||||
|
||||
def spawn_scanner(self):
|
||||
if self.scanner.is_alive():
|
||||
raise Exception("Scanner is already running")
|
||||
|
||||
self.scanner = PackagesetScanner()
|
||||
self.scanner.queue_state = self.queue_state
|
||||
self.scanner.verbose = self.verbose
|
||||
self.scanner.queue = self.queue
|
||||
self.scanner.start()
|
||||
|
||||
def update(self):
|
||||
if self.scanner.is_alive():
|
||||
return False
|
||||
|
||||
# Get the result from the thread
|
||||
notices = list(self.scanner.notices)
|
||||
|
||||
# Spawn a new insance of the monitoring thread
|
||||
self.spawn_scanner()
|
||||
|
||||
return notices
|
191
queuebot/plugs/queue.py
Normal file
191
queuebot/plugs/queue.py
Normal file
|
@ -0,0 +1,191 @@
|
|||
#!/usr/bin/python
|
||||
from __future__ import print_function
|
||||
|
||||
import traceback
|
||||
import threading
|
||||
from launchpadlib.launchpad import Launchpad
|
||||
|
||||
class QueueScanner(threading.Thread):
|
||||
notices = list()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
# Authenticated login to Launchpad
|
||||
self.lp = Launchpad.login_anonymously(
|
||||
'maubot-queuebot', 'production',
|
||||
launchpadlib_dir="/tmp/queuebot-%s/" % self.queue)
|
||||
|
||||
self.notices = list()
|
||||
|
||||
ubuntu = self.lp.distributions['ubuntu']
|
||||
ubuntu_series = [series for series in ubuntu.series
|
||||
if series.active]
|
||||
|
||||
# In verbose mode, show the current content of the queue
|
||||
if self.verbose and self.queue not in self.queue_state:
|
||||
self.queue_state[self.queue] = set()
|
||||
|
||||
# Get the content of the current queue
|
||||
new_list = set()
|
||||
for series in ubuntu_series:
|
||||
for pkg in series.getPackageUploads(status=self.queue):
|
||||
# Split the different sub-packages
|
||||
all_name = pkg.display_name.split(', ')
|
||||
all_arch = pkg.display_arches.split(', ')
|
||||
all_pkg = []
|
||||
for name in all_name:
|
||||
all_pkg.append((name, all_arch[all_name.index(name)]))
|
||||
|
||||
for (name, arch) in all_pkg:
|
||||
if name.startswith('language-pack-'):
|
||||
continue
|
||||
|
||||
if name.startswith('kde-l10n-'):
|
||||
continue
|
||||
|
||||
if arch.startswith('raw-'):
|
||||
continue
|
||||
|
||||
if arch == 'uefi' or arch == 'signing':
|
||||
continue
|
||||
|
||||
new_list.add(";".join([
|
||||
series.self_link,
|
||||
"%s-%s" % (series.name.lower(),
|
||||
pkg.pocket.lower()),
|
||||
name,
|
||||
pkg.display_version,
|
||||
arch,
|
||||
pkg.archive.name,
|
||||
pkg.self_link,
|
||||
]))
|
||||
|
||||
if self.queue in self.queue_state:
|
||||
# Print removed packages
|
||||
for pkg in sorted(self.queue_state[self.queue] - new_list):
|
||||
pkg_seriesurl, pkg_pocket, pkg_name, pkg_version, \
|
||||
pkg_arch, pkg_archive, pkg_self = pkg.split(';')
|
||||
pkg_status = self.lp.load(pkg_self).status
|
||||
if pkg_status == "Rejected":
|
||||
status = "rejected"
|
||||
elif pkg_status in ("Accepted", "Done"):
|
||||
status = "accepted"
|
||||
else:
|
||||
print("Impossible package status: %s "
|
||||
"(%s, %s, %s, %s, %s)" %
|
||||
(pkg_status, self.queue, pkg_name,
|
||||
pkg_arch, pkg_pocket, pkg_version))
|
||||
continue
|
||||
|
||||
mute = (
|
||||
"queue;%s" % (pkg_pocket),
|
||||
"queue;%s" % (self.queue.lower()),
|
||||
"queue;%s;%s" % (pkg_pocket, self.queue.lower()),
|
||||
"queue;%s;%s" % (self.queue.lower(), pkg_pocket)
|
||||
)
|
||||
self.notices.append(("%s: %s %s [%s] (%s) [%s]" % (
|
||||
self.queue, status, pkg_name, pkg_arch,
|
||||
pkg_pocket, pkg_version), mute))
|
||||
|
||||
# Print added packages
|
||||
for pkg in sorted(new_list - self.queue_state[self.queue]):
|
||||
pkg_seriesurl, pkg_pocket, pkg_name, pkg_version, \
|
||||
pkg_arch, pkg_archive, pkg_self = pkg.split(';')
|
||||
pkg_series = self.lp.load(pkg_seriesurl)
|
||||
|
||||
# Try to get some more data by looking at
|
||||
# the current archive
|
||||
current_component = 'none'
|
||||
current_version = 'none'
|
||||
current_pkgsets = set()
|
||||
for archive in ubuntu.archives:
|
||||
current_pkg = archive.getPublishedSources(
|
||||
source_name=pkg_name, status="Published",
|
||||
distro_series=pkg_series, exact_match=True)
|
||||
if list(current_pkg):
|
||||
current_component = current_pkg[0].component_name
|
||||
current_version = \
|
||||
current_pkg[0].source_package_version
|
||||
break
|
||||
|
||||
for pkgset in self.lp.packagesets.setsIncludingSource(
|
||||
distroseries=pkg_series,
|
||||
sourcepackagename=pkg_name):
|
||||
current_pkgsets.add(pkgset.name)
|
||||
|
||||
# Prepare the packageset list
|
||||
if current_pkgsets:
|
||||
pkg_pkgsets = ", ".join(sorted(current_pkgsets))
|
||||
else:
|
||||
pkg_pkgsets = "no packageset"
|
||||
|
||||
# Post the mssage to the channel
|
||||
message = ""
|
||||
if self.queue == 'New':
|
||||
if pkg_arch == "source":
|
||||
message = "%s source: %s (%s/%s) [%s]" % (
|
||||
self.queue, pkg_name, pkg_pocket,
|
||||
pkg_archive, pkg_version)
|
||||
elif pkg_arch == "sync":
|
||||
message = "%s sync: %s (%s/%s) [%s]" % (
|
||||
self.queue, pkg_name, pkg_pocket,
|
||||
pkg_archive, pkg_version)
|
||||
else:
|
||||
message = "%s binary: %s [%s] (%s/%s) [%s] (%s)" \
|
||||
% (self.queue, pkg_name, pkg_arch,
|
||||
pkg_pocket, current_component,
|
||||
pkg_version, pkg_pkgsets)
|
||||
else:
|
||||
message = "%s: %s (%s/%s) [%s => %s] (%s)" % (
|
||||
self.queue, pkg_name, pkg_pocket,
|
||||
current_component, current_version,
|
||||
pkg_version, pkg_pkgsets)
|
||||
|
||||
if pkg_arch == "sync":
|
||||
message += " (sync)"
|
||||
|
||||
mute = (
|
||||
"queue;%s" % (pkg_pocket),
|
||||
"queue;%s" % (self.queue.lower()),
|
||||
"queue;%s;%s" % (pkg_pocket, self.queue.lower()),
|
||||
"queue;%s;%s" % (self.queue.lower(), pkg_pocket)
|
||||
)
|
||||
self.notices.append((message, mute))
|
||||
self.queue_state[self.queue] = new_list
|
||||
except:
|
||||
# We don't want the bot to crash when LP fails
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class Queue():
|
||||
queue_state = dict()
|
||||
scanner = QueueScanner()
|
||||
name = "queue"
|
||||
queue = ""
|
||||
|
||||
def __init__(self, queue, verbose=False):
|
||||
self.queue = queue
|
||||
self.verbose = verbose
|
||||
self.spawn_scanner()
|
||||
|
||||
def spawn_scanner(self):
|
||||
if self.scanner.is_alive():
|
||||
raise Exception("Scanner is already running")
|
||||
|
||||
self.scanner = QueueScanner()
|
||||
self.scanner.queue_state = self.queue_state
|
||||
self.scanner.verbose = self.verbose
|
||||
self.scanner.queue = self.queue
|
||||
self.scanner.start()
|
||||
|
||||
def update(self):
|
||||
if self.scanner.is_alive():
|
||||
return False
|
||||
|
||||
# Get the result from the thread
|
||||
notices = list(self.scanner.notices)
|
||||
|
||||
# Spawn a new insance of the monitoring thread
|
||||
self.spawn_scanner()
|
||||
|
||||
return notices
|
161
queuebot/plugs/tracker.py
Normal file
161
queuebot/plugs/tracker.py
Normal file
|
@ -0,0 +1,161 @@
|
|||
#!/usr/bin/python
|
||||
from __future__ import print_function
|
||||
import threading
|
||||
import traceback
|
||||
import xmlrpc.client as xmlrpclib
|
||||
|
||||
|
||||
class TrackerScanner(threading.Thread):
|
||||
notices = list()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.notices = list()
|
||||
|
||||
# In verbose mode, show the current content of the queue
|
||||
if self.verbose and self.queue not in self.tracker_state:
|
||||
self.tracker_state[self.queue] = set()
|
||||
|
||||
milestones = [milestone for milestone in
|
||||
self.drupal.qatracker.milestones.get_list([0])
|
||||
if milestone['notify'] == "1"
|
||||
and 'Touch' not in milestone['title']]
|
||||
|
||||
products = {}
|
||||
for product in self.drupal.qatracker.products.get_list([0]):
|
||||
products[product['id']] = product
|
||||
|
||||
new_list = set()
|
||||
for milestone in milestones:
|
||||
for build in self.drupal.qatracker.builds.get_list(
|
||||
int(milestone['id']), [0, 1, 4]):
|
||||
build_milestone = milestone['title']
|
||||
build_product = products[build['productid']]['title']
|
||||
build_version = build['version']
|
||||
build_status = build['status_string']
|
||||
new_list.add("%s;%s;%s;%s" % (build_milestone,
|
||||
build_product,
|
||||
build_version,
|
||||
build_status))
|
||||
|
||||
if self.queue in self.tracker_state:
|
||||
build_products = [";".join(build.split(';')[0:2])
|
||||
for build in self.tracker_state[self.queue]]
|
||||
new_build_products = [";".join(build.split(';')[0:2])
|
||||
for build in new_list]
|
||||
|
||||
# Print removed images
|
||||
for build in self.tracker_state[self.queue] - new_list:
|
||||
build_milestone, build_product, build_version, \
|
||||
build_status = build.split(';')
|
||||
|
||||
if "%s;%s" % (build_milestone, build_product) \
|
||||
in new_build_products:
|
||||
continue
|
||||
|
||||
# Post to the channels. Don't mark all the records
|
||||
# as removed when we remove a milestone
|
||||
skip = False
|
||||
for milestone in milestones:
|
||||
if build_milestone == milestone['title']:
|
||||
skip = True
|
||||
break
|
||||
else:
|
||||
skip = True
|
||||
|
||||
if not skip:
|
||||
self.notices.append(("%s: %s [%s] has been removed" % (
|
||||
self.queue, build_product, build_milestone),
|
||||
("tracker",)))
|
||||
|
||||
# Print other changes and deal with cases where a released
|
||||
# milestone is moved back to testing
|
||||
if len(new_list - self.tracker_state[self.queue]) > 25:
|
||||
self.notices.append((
|
||||
"%s: %s entries have been "
|
||||
"added, updated or disabled" % (
|
||||
self.queue, len(new_list -
|
||||
self.tracker_state[self.queue])),
|
||||
("tracker",)))
|
||||
elif len(self.tracker_state[self.queue] - new_list) > 25:
|
||||
self.notices.append((
|
||||
"%s: %s entries have been "
|
||||
"added, updated or disabled" % (
|
||||
self.queue,
|
||||
len(self.tracker_state[self.queue] - new_list)),
|
||||
("tracker",)))
|
||||
else:
|
||||
for build in sorted(
|
||||
new_list - self.tracker_state[self.queue]):
|
||||
|
||||
build_milestone, build_product, build_version, \
|
||||
build_status = build.split(';')
|
||||
|
||||
if "%s;%s" % (build_milestone, build_product) \
|
||||
in build_products:
|
||||
if build_status == "Re-building":
|
||||
self.notices.append((
|
||||
"%s: %s [%s] has been disabled" % (
|
||||
self.queue, build_product,
|
||||
build_milestone), ("tracker",)))
|
||||
elif build_status == "Ready":
|
||||
self.notices.append((
|
||||
"%s: %s [%s] has been marked as ready" % (
|
||||
self.queue, build_product,
|
||||
build_milestone), ("tracker",)))
|
||||
else:
|
||||
self.notices.append((
|
||||
"%s: %s [%s] has been updated (%s)" % (
|
||||
self.queue, build_product,
|
||||
build_milestone, build_version),
|
||||
("tracker",)))
|
||||
else:
|
||||
self.notices.append((
|
||||
"%s: %s [%s] (%s) has been added" % (
|
||||
self.queue, build_product, build_milestone,
|
||||
build_version), ("tracker",)))
|
||||
|
||||
self.tracker_state[self.queue] = new_list
|
||||
except:
|
||||
# We don't want the bot to crash when LP fails
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class Tracker():
|
||||
tracker_state = dict()
|
||||
scanner = TrackerScanner()
|
||||
name = "tracker"
|
||||
queue = ""
|
||||
|
||||
def __init__(self, queue, verbose=False):
|
||||
self.queue = queue
|
||||
self.verbose = verbose
|
||||
|
||||
# Setup ISO tracker
|
||||
self.drupal = xmlrpclib.ServerProxy(
|
||||
"https://iso.qa.ubuntu.com/xmlrpc.php")
|
||||
|
||||
self.spawn_scanner()
|
||||
|
||||
def spawn_scanner(self):
|
||||
if self.scanner.is_alive():
|
||||
raise Exception("Scanner is already running")
|
||||
|
||||
self.scanner = TrackerScanner()
|
||||
self.scanner.tracker_state = self.tracker_state
|
||||
self.scanner.verbose = self.verbose
|
||||
self.scanner.drupal = self.drupal
|
||||
self.scanner.queue = self.queue
|
||||
self.scanner.start()
|
||||
|
||||
def update(self):
|
||||
if self.scanner.is_alive():
|
||||
return False
|
||||
|
||||
# Get the result from the thread
|
||||
notices = list(self.scanner.notices)
|
||||
|
||||
# Spawn a new insance of the monitoring thread
|
||||
self.spawn_scanner()
|
||||
|
||||
return notices
|
26
requirements.txt
Normal file
26
requirements.txt
Normal file
|
@ -0,0 +1,26 @@
|
|||
aiohttp==3.9.3
|
||||
aiosignal==1.3.1
|
||||
aiosqlite==0.18.0
|
||||
asyncpg==0.28.0
|
||||
attrs==23.2.0
|
||||
bcrypt==4.1.2
|
||||
click==8.1.7
|
||||
colorama==0.4.6
|
||||
commonmark==0.9.1
|
||||
frozenlist==1.4.1
|
||||
idna==3.6
|
||||
Jinja2==3.1.3
|
||||
MarkupSafe==2.1.5
|
||||
maubot==0.4.2
|
||||
mautrix==0.20.4
|
||||
multidict==6.0.5
|
||||
packaging==24.0
|
||||
prompt-toolkit==3.0.43
|
||||
questionary==1.10.0
|
||||
ruamel.yaml==0.17.40
|
||||
ruamel.yaml.clib==0.2.8
|
||||
schedule==1.2.1
|
||||
setuptools==69.2.0
|
||||
SQLAlchemy==1.3.24
|
||||
wcwidth==0.2.13
|
||||
yarl==1.9.4
|
Loading…
Reference in a new issue