Пример #1
0
def unload(update, context):
    if str(update.message.from_user.id) not in get_config(
            "BOT_ADMIN_USER_IDS").split():
        update.message.reply_text(
            "Error: You are not authorized to unload modules")
        LOGI("Access denied to user " + str(update.message.from_user.id))
        return

    try:
        module_name = update.message.text.split(' ', 1)[1]
    except IndexError:
        update.message.reply_text("Error: Module name not provided")
        return

    if module_name == "core":
        update.message.reply_text(
            "Error: You can't unload module used for loading/unloading modules"
        )
        return

    for module in get_modules_list():
        if module_name == module.name:
            module.unload()
            update.message.reply_text("Module {} unloaded".format(module_name))
            return

    update.message.reply_text("Error: Module not found")
Пример #2
0
def main():
    global modules
    modules += import_bot_modules()
    updater = HomeBotUpdater(get_config("BOT_API_TOKEN"))
    LOGI(f"HomeBot started, version {__version__}")
    LOGI(f"Bot username: @{updater.bot.get_me().username}")
    updater.start_polling()
Пример #3
0
def weather(update, context):
    try:
        city = update.message.text.split(' ', 1)[1]
    except IndexError:
        update.message.reply_text("City not provided")
        return
    if get_config("WEATHER_API_KEY", None) == None:
        update.message.reply_text(
            "OpenWeatherMap API key not specified\nAsk the bot hoster to configure it"
        )
        LOGE(
            "OpenWeatherMap API key not specified, get it at https://home.openweathermap.org/api_keys"
        )
        return
    URL = "https://api.openweathermap.org/data/2.5/weather"
    parameters = {
        "appid": get_config("WEATHER_API_KEY", None),
        "q": city,
        "units": get_config("WEATHER_TEMP_UNIT", "metric"),
    }
    temp_unit = {"imperial": "F", "metric": "C"}
    wind_unit = {"imperial": "mph", "metric": "km/h"}
    temp_unit = temp_unit.get(get_config("WEATHER_TEMP_UNIT", None), "K")
    wind_unit = wind_unit.get(get_config("WEATHER_TEMP_UNIT", None), "km/h")
    response = requests.get(url=URL, params=parameters).json()
    if response["cod"] != 200:
        update.message.reply_text("Error: " + response["message"])
        return
    city_name = response["name"]
    city_country = response["sys"]["country"]
    city_lat = response["coord"]["lat"]
    city_lon = response["coord"]["lon"]
    weather_type = response["weather"][0]["main"]
    weather_type_description = response["weather"][0]["description"]
    temp = response["main"]["temp"]
    temp_min = response["main"]["temp_min"]
    temp_max = response["main"]["temp_max"]
    humidity = response["main"]["humidity"]
    wind_speed = response["wind"]["speed"]
    update.message.reply_text(
        f"Current weather for {city_name}, {city_country} ({city_lat}, {city_lon}):\n"
        f"Weather: {weather_type} ({weather_type_description})\n"
        f"Temperature: {temp}{temp_unit} (Min: {temp_min}{temp_unit} Max: {temp_max}{temp_unit})\n"
        f"Humidity: {humidity}%\n"
        f"Wind: {wind_speed}{wind_unit}")
Пример #4
0
def user_is_admin(user_id):
	"""
	Check if the given user ID is in the list
	of the approved user IDs.
	"""
	if str(user_id) not in get_config("CI_APPROVED_USER_IDS").split():
		LOGI(f"Access denied to user {user_id}")
		return False

	LOGI(f"Access granted to user {user_id}")
	return True
Пример #5
0
def ci_build(update: Update, context: CallbackContext):
	update.message.reply_text("Generation started")
	tempdir = TemporaryDirectory()
	path = Path(tempdir.name)
	url = update.message.text.split()[2]
	file = path / "recovery.img"
	open(file, 'wb').write(requests.get(url, allow_redirects=True).content)

	try:
		devicetree = generate_device_tree(file, path / "working")
	except Exception as e:
		update.message.reply_text("TWRP device tree generation failed\n"
								  f"Error: {e}")
		return

	# Upload to GitHub
	gh_username = get_config("CI_GITHUB_USERNAME")
	gh_token = get_config("CI_GITHUB_TOKEN")
	gh_org_name = get_config("CI_TWRPDTGEN_GITHUB_ORG")
	repo_name = "android_device_" + devicetree.manufacturer + "_" + devicetree.codename
	git_repo_url = f"https://{gh_username}:{gh_token}@github.com/{gh_org_name}/{repo_name}"

	try:
		gh = Github(gh_token)
		gh_org = gh.get_organization(gh_org_name)
		devicetree_repo = gh_org.create_repo(name=repo_name, private=False, auto_init=False)
		devicetree.git_repo.git.push(git_repo_url, "master")
	except GithubException as error:
		if error.status == 422:
			push_result = "Error: A device tree for this device already exists!"
		else:
			push_result = "Error: Push to GitHub failed!"
		update.message.reply_text(push_result)
	else:
		context.bot.send_message(get_config("CI_TWRPDTGEN_CHANNEL_ID"),
								  "TWRP device tree generated\n"
								 f"Codename: {devicetree.codename}\n"
								 f"Manufacturer: {devicetree.manufacturer}\n"
								 f"Device tree: {devicetree_repo.html_url}")
	finally:
		tempdir.cleanup()
Пример #6
0
	def ci(update: Update, context: CallbackContext):
		if not user_is_admin(update.message.from_user.id):
			update.message.reply_text("Error: You are not authorized to use CI function of this bot.\n"
									  "Ask to who host this bot to add you to the authorized people list")
			return

		if get_config("CI_CHANNEL_ID") == "":
			update.message.reply_text("Error: CI channel or user ID not defined")
			LOGE("CI channel or user ID not defined")
			return

		parser = CIParser(prog="/ci")
		parser.set_output(update.message.reply_text)
		parser.add_argument('project', help='CI project',
							nargs='?', default=None,)
		parser.add_argument('-s', '--status',
							action='store_true', help='show queue status')

		args, project_args = parser.parse_known_args(context.args)

		if args.status:
			update.message.reply_text(queue_manager.get_formatted_queue_list())
			return

		if args.project is None:
			parser.error("Please specify a project")

		try:
			project_class = import_module(f"homebot.modules.ci.projects.{args.project}", package="Project").Project
		except ModuleNotFoundError:
			update.message.reply_text(f"Error: Project script not found")
			return
		except Exception as e:
			text = "Error: Error while importing project:"
			text += format_exception(e)
			update.message.reply_text(text)
			LOGE(text)
			return

		try:
			project = project_class(update, context, project_args)
		except Exception as e:
			text = "Error: Project class initialization failed:\n"
			text += format_exception(e)
			update.message.reply_text(text)
			LOGE(text)
			return

		workflow = Workflow(project)
		queue_manager.put(workflow)
		update.message.reply_text("Workflow added to the queue")
		LOGI("Workflow added to the queue")
Пример #7
0
def ci(update, context):
    if str(update.message.from_user.id) not in get_config(
            "CI_APPROVED_USER_IDS").split():
        update.message.reply_text(
            "Error: You are not authorized to use CI function of this bot.\nAsk to who host this bot to add you to the authorized people list"
        )
        LOGI("Access denied to user " + str(update.message.from_user.id))
        return
    LOGI("Access granted to user " + str(update.message.from_user.id))
    if get_config("CI_CHANNEL_ID") == "":
        update.message.reply_text("Error: CI channel or user ID not defined")
        LOGE("CI channel or user ID not defined")
        return
    project = update.message.text.split()[1]
    if not os.path.isfile(bot_path / "modules" / "ci" / "projects" /
                          (project + ".py")):
        update.message.reply_text("Error: Project script not found")
        return
    project_module = import_module('homebot.modules.ci.projects.' + project,
                                   package="*")
    LOGI("CI workflow started, project: " + project)
    project_module.ci_build(update, context)
    LOGI("CI workflow finished, project: " + project)
Пример #8
0
def create_artifacts_list(artifacts):
    upload_method = get_config("CI_ARTIFACTS_UPLOAD_METHOD")
    artifact_total = len(artifacts)
    artifact_uploaded = 0
    for artifact in artifacts:
        artifact_uploaded += 1

    text = f"Uploaded {artifact_uploaded} out of {artifact_total} artifact(s)\n"
    text += f"Upload method: {upload_method}\n\n"
    artifact_index = 1
    for artifact in artifacts:
        artifact_result = artifacts.get(artifact, "On queue")
        text += f"{artifact_index}) {artifact.name}: {artifact_result}\n"
        artifact_index = artifact_index + 1
    return text
Пример #9
0
    def __init__(self):
        """
		Initialize the uploader variables.
		"""
        self.method = get_config("CI_ARTIFACTS_UPLOAD_METHOD")
        self.destination_path_base = Path(get_config("CI_UPLOAD_BASE_DIR"))
        self.host = get_config("CI_UPLOAD_HOST")
        self.port = get_config("CI_UPLOAD_PORT")
        self.server = self.host if self.port is None or self.port == "" else f"{self.host}:{self.port}"
        self.username = get_config("CI_UPLOAD_USERNAME")
        self.password = get_config("CI_UPLOAD_PASSWORD")

        if self.method not in ALLOWED_METHODS:
            raise NotImplementedError("Upload method not valid")
Пример #10
0
    def build(self):
        project_dir = Path(
            f"{get_config('CI_MAIN_DIR')}/{self.name}-{self.version}")
        device_out_dir = project_dir / "out" / "target" / "product" / self.parsed_args.device

        artifacts = Artifacts(device_out_dir, self.artifacts)
        post_manager = PostManager(self, self.parsed_args.device, artifacts)

        if self.parsed_args.clean is True:
            clean_type = "clean"
        elif self.parsed_args.installclean is True:
            clean_type = "installclean"
        else:
            clean_type = "none"

        post_manager.update("Building")

        command = [
            bot_path / "modules" / "ci" / "projects" / "aosp" / "tools" /
            "building.sh", "--sources", project_dir, "--lunch_prefix",
            self.lunch_prefix, "--lunch_suffix", self.lunch_suffix,
            "--build_target", self.build_target, "--clean", clean_type,
            "--device", self.parsed_args.device
        ]

        last_edit = datetime.now()
        process = subprocess.Popen(command,
                                   encoding="UTF-8",
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.STDOUT)
        while True:
            output = process.stdout.readline()
            if output == '' and process.poll() is not None:
                break
            if not output:
                continue

            now = datetime.now()
            if (now - last_edit).seconds < 300:
                continue

            result = re.search(r"\[ +([0-9]+% [0-9]+/[0-9]+)\]",
                               output.strip())
            if result is None:
                continue
            result_split = str(result.group(1)).split()
            if len(result_split) != 2:
                continue

            percentage, targets = re.split(" +", result.group(1))
            post_manager.update(f"Building: {percentage} ({targets})")

            last_edit = now

        returncode = process.poll()

        # Process return code
        build_result = ERROR_CODES.get(returncode,
                                       "Build failed: Unknown error")

        post_manager.update(build_result)

        needs_logs_upload = NEEDS_LOGS_UPLOAD.get(returncode, False)
        if needs_logs_upload != False:
            log_file = open(project_dir / needs_logs_upload, "rb")
            self.context.bot.send_document(get_config("CI_CHANNEL_ID"),
                                           log_file)
            log_file.close()

        if returncode != SUCCESS or get_config(
                "CI_UPLOAD_ARTIFACTS") != "true":
            return

        # Upload artifacts
        try:
            uploader = Uploader()
        except Exception as e:
            post_manager.update(f"{build_result}\n"
                                f"Upload failed: {type(e)}: {e}")
            return

        artifacts.update()

        post_manager.update(build_result)

        for artifact in artifacts.artifacts:
            artifact.status = STATUS_UPLOADING
            post_manager.update(build_result)

            try:
                uploader.upload(
                    artifact,
                    Path(self.category) / self.parsed_args.device / self.name /
                    self.android_version)
            except Exception as e:
                artifact.status = f"{STATUS_NOT_UPLOADED}: {type(e)}: {e}"
            else:
                artifact.status = STATUS_UPLOADED

            post_manager.update(build_result)
Пример #11
0
def ci_build(update, context):
    parser = CIParser(prog="/ci AOSP")
    parser.set_output(update.message.reply_text)
    parser.add_argument('project', help='AOSP project')
    parser.add_argument('device', help='device codename')
    parser.add_argument('-ic',
                        '--installclean',
                        help='make installclean before building',
                        action='store_true')
    parser.add_argument('-c',
                        '--clean',
                        help='make clean before building',
                        action='store_true')
    parser.set_defaults(clean=False, installclean=False)

    try:
        args_passed = update.message.text.split(' ', 2)[2].split()
    except IndexError:
        args_passed = ""

    args = parser.parse_args(args_passed)

    project_module = import_module(
        'homebot.modules.ci.projects.aosp.projects.' + args.project,
        package="*")

    projects_dir = Path(get_config("CI_MAIN_DIR"))
    project_dir = projects_dir / (project_module.project + "-" +
                                  project_module.version)
    out_dir = project_dir / "out"
    device_out_dir = out_dir / "target" / "product" / args.device

    if args.clean is True:
        clean_type = "clean"
    elif args.installclean is True:
        clean_type = "installclean"
    else:
        clean_type = "none"

    message_id = context.bot.send_message(
        get_config("CI_CHANNEL_ID"),
        make_ci_post(project_module, args.device, "Building", None)).message_id
    process = Popen([
        bot_path / "modules" / "ci" / "projects" / "aosp" / "tools" /
        "building.sh", "--project",
        project_module.project + "-" + project_module.version, "--name",
        project_module.project + " " + project_module.version,
        "--android_version", project_module.android_version, "--lunch_prefix",
        project_module.lunch_prefix, "--lunch_suffix",
        project_module.lunch_suffix, "--build_target",
        project_module.build_target, "--artifacts", project_module.artifacts,
        "--device", args.device, "--main_dir",
        get_config("CI_MAIN_DIR"), "--clean", clean_type
    ],
                    stdout=PIPE,
                    stderr=PIPE,
                    universal_newlines=True)
    _, _ = process.communicate()

    context.bot.edit_message_text(chat_id=get_config("CI_CHANNEL_ID"),
                                  message_id=message_id,
                                  text=make_ci_post(
                                      project_module, args.device,
                                      error_code.get(
                                          process.returncode,
                                          "Build failed: Unknown error"),
                                      None))

    if needs_logs_upload.get(process.returncode, False) != False:
        log_file = open(
            project_dir / needs_logs_upload.get(process.returncode), "rb")
        context.bot.send_document(get_config("CI_CHANNEL_ID"), log_file)
        log_file.close()

    if get_config("CI_UPLOAD_ARTIFACTS") != "true":
        return

    build_result = error_code.get(process.returncode,
                                  "Build failed: Unknown error")

    artifacts = {
        artifact: "On queue"
        for artifact in list(device_out_dir.glob(project_module.artifacts))
    }

    context.bot.edit_message_text(chat_id=get_config("CI_CHANNEL_ID"),
                                  message_id=message_id,
                                  text=make_ci_post(
                                      project_module, args.device,
                                      build_result,
                                      create_artifacts_list(artifacts)))

    for artifact in artifacts:
        artifacts[artifact] = "Uploading"
        context.bot.edit_message_text(chat_id=get_config("CI_CHANNEL_ID"),
                                      message_id=message_id,
                                      text=make_ci_post(
                                          project_module, args.device,
                                          build_result,
                                          create_artifacts_list(artifacts)))

        result = upload(
            artifact,
            Path(project_module.project_type) / args.device /
            project_module.project / project_module.android_version)
        if result is True:
            artifacts[artifact] = "Upload successful"
        else:
            artifacts[artifact] = "Upload failed"

        context.bot.edit_message_text(chat_id=get_config("CI_CHANNEL_ID"),
                                      message_id=message_id,
                                      text=make_ci_post(
                                          project_module, args.device,
                                          build_result,
                                          create_artifacts_list(artifacts)))
Пример #12
0
from homebot import get_config
from homebot.modules.ci.artifacts import Artifacts
from telegram.error import TimedOut

chat_id = get_config("CI_CHANNEL_ID")


class PostManager:
    def __init__(self, project, device: str, artifacts: Artifacts):
        self.project = project
        self.device = device
        self.artifacts = artifacts
        self.base_message_text = self.get_base_message_text()
        self.message = self.project.context.bot.send_message(
            chat_id, self.base_message_text)

    def get_base_message_text(self):
        text = f"🛠 CI | {self.project.name} {self.project.version} ({self.project.android_version})\n"
        text += f"Device: {self.device}\n"
        text += f"Lunch flavor: {self.project.lunch_prefix}_{self.device}-{self.project.lunch_suffix}\n"
        text += "\n"
        return text

    def update(self, status: str):
        text = self.base_message_text
        text += f"Status: {status}\n"
        text += "\n"
        if self.artifacts.artifacts:
            text += self.artifacts.get_readable_artifacts_list()
        self.edit_text(text)
Пример #13
0
def upload(file: Path, destination_path_ci: Path):
    """
	Upload an artifact using settings from config.env

	Returns True if the upload went fine,
	else a string containing an explanation of the error 
	"""
    method = get_config("CI_ARTIFACTS_UPLOAD_METHOD")
    destination_path_base = Path(get_config("CI_UPLOAD_BASE_DIR"))
    host = get_config("CI_UPLOAD_HOST")
    port = get_config("CI_UPLOAD_PORT")
    username = get_config("CI_UPLOAD_USERNAME")
    password = get_config("CI_UPLOAD_PASSWORD")

    ALLOWED_METHODS = ["localcopy", "ftp", "sftp"]
    file_path = Path(file)
    file_base = file_path.name

    if destination_path_base is None:
        destination_path = destination_path_ci
    else:
        destination_path = destination_path_base / destination_path_ci

    if method not in ALLOWED_METHODS:
        return "Upload method not valid"

    if not file_path.is_file():
        return "File doesn't exists"

    LOGI("Started uploading of " + file.name)

    if method == "localcopy":
        os.makedirs(destination_path, exist_ok=True)
        shutil.copy(file_path, destination_path)

    elif method == "ftp":
        if port is None or port == "":
            server = host
        else:
            server = host + ":" + port
        ftp = FTP(server)
        ftp.login(username, password)
        ftp_chdir(ftp, destination_path)
        with open(file_path, 'rb') as f:
            ftp.storbinary('STOR %s' % file_base, f)
            f.close()
        ftp.close()

    elif method == "sftp":
        if port is None or port == "":
            server = host
        else:
            server = host + ":" + port
        transport = paramiko.Transport(server)
        transport.connect(username=username, password=password)
        sftp = paramiko.SFTPClient.from_transport(transport)

        sftp_chdir(sftp, destination_path)
        sftp.put(file_path, file_base)

        sftp.close()
        transport.close()

    LOGI("Finished uploading of " + file.name)
    return True
Пример #14
0
def ci_build(update: Update, context: CallbackContext):
    status_message = update.message.reply_text("Downloading file...")

    # Download file
    tempdir = TemporaryDirectory()
    path = Path(tempdir.name)
    url = update.message.text.split()[2]
    file = path / "recovery.img"
    open(file, 'wb').write(requests.get(url, allow_redirects=True).content)

    # Generate device tree
    status_message.edit_text("Generating device tree...")
    try:
        devicetree = DeviceTree(path / "working", recovery_image=file)
    except Exception as e:
        status_message.edit_text("Device tree generation failed\n"
                                 f"Error: {e}")
        return

    try:
        build_description = devicetree.build_prop_reader.get_prop(
            BUILD_DESCRIPTION, "build description")
        branch = build_description.replace(" ", "-")
    except AssertionError:
        status_message.edit_text(
            "Failed to get build description prop, using date as a branch")
        today = date.today()
        build_description = None
        branch = f"{today.year}-{today.month}-{today.day}"

    # Upload to GitHub
    status_message.edit_text("Pushing to GitHub...")
    gh_username = get_config("CI_GITHUB_USERNAME")
    gh_token = get_config("CI_GITHUB_TOKEN")
    gh_org_name = get_config("CI_TWRPDTGEN_GITHUB_ORG")
    repo_name = f"android_device_{devicetree.manufacturer}_{devicetree.codename}"
    git_repo_url = f"https://{gh_username}:{gh_token}@github.com/{gh_org_name}/{repo_name}"

    # Get organization
    try:
        gh = Github(gh_token)
        gh_org = gh.get_organization(gh_org_name)
    except GithubException as error:
        status_message.edit_text(f"Failed to get organization\n"
                                 f"Error: {error}")
        return

    # Create repo if needed
    status_message.edit_text("Creating repo if needed...")
    try:
        devicetree_repo = gh_org.create_repo(name=repo_name,
                                             private=False,
                                             auto_init=False)
    except GithubException as error:
        if error.status != 422:
            status_message.edit_text("Repo creation failed\n"
                                     f"Error: {error.status} {error}")
            return
        devicetree_repo = gh_org.get_repo(name=repo_name)

    status_message.edit_text("Pushing...")
    try:
        devicetree.git_repo.git.push(git_repo_url, f"HEAD:refs/heads/{branch}")
        devicetree_repo.edit(default_branch=branch)
    except GitCommandError as error:
        status_message.edit_text(f"Error: Push to remote failed!")
        return

    status_message.edit_text("Done")

    channel_id = get_config("CI_TWRPDTGEN_CHANNEL_ID")
    context.bot.send_message(
        channel_id, "TWRP device tree generated\n"
        f"Codename: {devicetree.codename}\n"
        f"Manufacturer: {devicetree.manufacturer}\n"
        f"Build description: {build_description}\n"
        f"Device tree: {devicetree_repo.html_url}/tree/{branch}",
        disable_web_page_preview=True)
Пример #15
0
def ci_build(update: Update, context: CallbackContext):
    # Parse arguments
    parser = CIParser(prog="/ci aosp")
    parser.set_output(update.message.reply_text)
    parser.add_argument('project', help='AOSP project')
    parser.add_argument('device', help='device codename')
    parser.add_argument('-ic',
                        '--installclean',
                        help='make installclean before building',
                        action='store_true')
    parser.add_argument('-c',
                        '--clean',
                        help='make clean before building',
                        action='store_true')
    parser.add_argument('-g',
                        '--gapped',
                        help='make gapped build',
                        action='store_true')
    parser.set_defaults(clean=False, installclean=False, gapped=False)

    try:
        args_passed = update.message.text.split(' ', 2)[2].split()
    except IndexError:
        args_passed = []

    args = parser.parse_args(args_passed)

    # Import project
    project: AOSPProject
    project = import_module(
        f"homebot.modules.ci.projects.aosp.projects.{args.project}",
        package="*").project

    project_dir = Path(
        f"{get_config('CI_MAIN_DIR')}/{project.name}-{project.version}")
    device_out_dir = project_dir / "out" / "target" / "product" / args.device

    if args.clean is True:
        clean_type = "clean"
    elif args.installclean is True:
        clean_type = "installclean"
    else:
        clean_type = "none"

    message_id = update_ci_post(context,
                                None,
                                project,
                                args.device,
                                "Building",
                                gapped=args.gapped)

    command = [
        bot_path / "modules" / "ci" / "projects" / "aosp" / "tools" /
        "building.sh", "--sources", project_dir, "--lunch_prefix",
        project.lunch_prefix, "--lunch_suffix", project.lunch_suffix,
        "--build_target", project.build_target, "--clean", clean_type,
        "--gapped",
        str(args.gapped), "--device", args.device
    ]

    last_edit = datetime.now()
    process = subprocess.Popen(command,
                               encoding="UTF-8",
                               stdout=subprocess.PIPE,
                               stderr=subprocess.STDOUT)
    while True:
        output = process.stdout.readline()
        if output == '' and process.poll() is not None:
            break
        if not output:
            continue

        now = datetime.now()
        if (now - last_edit).seconds < 300:
            continue

        result = re.search(r"\[ +([0-9]+% [0-9]+/[0-9]+)\]", output.strip())
        if result is None:
            continue
        result_split = str(result.group(1)).split()
        if len(result_split) != 2:
            continue

        percentage, targets = re.split(" +", result.group(1))
        update_ci_post(context,
                       message_id,
                       project,
                       args.device,
                       f"Building: {percentage} ({targets})",
                       gapped=args.gapped)

        last_edit = now

    returncode = process.poll()

    # Process return code
    build_result = ERROR_CODES.get(returncode, "Build failed: Unknown error")

    update_ci_post(context,
                   message_id,
                   project,
                   args.device,
                   build_result,
                   gapped=args.gapped)

    needs_logs_upload = NEEDS_LOGS_UPLOAD.get(returncode, False)
    if needs_logs_upload != False:
        log_file = open(project_dir / needs_logs_upload, "rb")
        context.bot.send_document(get_config("CI_CHANNEL_ID"), log_file)
        log_file.close()

    if returncode != SUCCESS or get_config("CI_UPLOAD_ARTIFACTS") != "true":
        return

    # Upload artifacts
    try:
        uploader = Uploader()
    except Exception as e:
        update_ci_post(context,
                       message_id,
                       project,
                       args.device, f"{build_result}\n"
                       f"Upload failed: {type(e)}: {e}",
                       gapped=args.gapped)
        return

    artifacts = Artifacts(device_out_dir, project.artifacts)

    update_ci_post(context,
                   message_id,
                   project,
                   args.device,
                   build_result,
                   artifacts=artifacts,
                   gapped=args.gapped)

    for artifact in artifacts.artifacts:
        artifact.status = STATUS_UPLOADING
        update_ci_post(context,
                       message_id,
                       project,
                       args.device,
                       build_result,
                       artifacts=artifacts,
                       gapped=args.gapped)

        try:
            uploader.upload(
                artifact,
                Path(project.category) / args.device / project.name /
                project.android_version)
        except Exception as e:
            artifact.status = f"{STATUS_NOT_UPLOADED}: {type(e)}: {e}"
        else:
            artifact.status = STATUS_UPLOADED

        update_ci_post(context,
                       message_id,
                       project,
                       args.device,
                       build_result,
                       artifacts=artifacts,
                       gapped=args.gapped)