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