def get_channel_profiles( channel_oid: ObjectId, partial_name: Optional[str] = None) -> List[ChannelProfileEntry]: ret = [] # Get channel profiles. Terminate if no available profiles profs = list( ProfileManager.get_channel_profiles(channel_oid, partial_name)) if not profs: return ret # Get channel data. Terminate if no channel data found channel_model = ChannelManager.get_channel_oid(channel_oid) if not channel_model: return ret # Get user names, and the prof-channel dict user_oids_dict = ProfileManager.get_profiles_user_oids( [prof.id for prof in profs]) user_oids = [] for k, v in user_oids_dict.items(): user_oids.extend(v) user_names = IdentitySearcher.get_batch_user_name(user_oids, channel_model, on_not_found=None) for prof in profs: uids = user_oids_dict.get(prof.id, []) ret.append( ChannelProfileEntry(prof, [user_names.get(uid) for uid in uids])) return ret
def profile_delete(e: TextMessageEventObject, name: str): # --- Check profile name prof = ProfileManager.get_profile_name(name) if not prof: return [ HandledMessageEventText(content=_( "Profile with the name `{}` not found.").format(name)) ] # --- Check permission profiles = ProfileManager.get_user_profiles(e.channel_model.id, e.user_model.id) user_permissions = ProfileManager.get_permissions(profiles) if ProfilePermission.PRF_CED not in user_permissions: return [ HandledMessageEventText( content=_("Insufficient permission to delete profile.")) ] deleted = ProfileManager.delete_profile(e.channel_oid, prof.id, e.user_model.id) if deleted: return [HandledMessageEventText(content=_("Profile deleted."))] else: return [ HandledMessageEventText(content=_("Failed to delete the profile.")) ]
def get(self, request, *args, **kwargs): # `kwargs` will be used as `nav_param` so extract channel_oid from `kwargs` instead of creating param. channel_oid_str = kwargs.get("channel_oid", "") channel_oid = safe_cast(channel_oid_str, ObjectId) u_profs = ProfileManager.get_user_profiles(channel_oid, get_root_oid(request)) if u_profs: return render_template( self.request, _("Channel Management - {}").format(channel_oid), "account/channel/manage.html", { "user_profiles": u_profs, "perm_sum": sorted(ProfileManager.get_permissions(u_profs), key=lambda x: x.code), "channel_oid": channel_oid }, nav_param=kwargs) else: c_prof = ChannelManager.get_channel_oid(channel_oid) if c_prof: messages.info( request, _("You are redirected to the channel info page " "because you don't have any connections linked to the channel."), extra_tags="info" ) return redirect(reverse("info.channel", kwargs={"channel_oid": channel_oid})) else: return WebsiteErrorView.website_error( request, WebsiteError.PROFILE_LINK_NOT_FOUND, {"channel_oid": channel_oid_str})
def handle_member_join(request, event, destination): cdata = ChannelManager.get_channel_token( Platform.LINE, LineApiUtils.get_channel_id(event), auto_register=True) joined_names = [] for user in event.joined.members: uid = user.user_id udata_result = RootUserManager.get_root_data_onplat(Platform.LINE, uid) if udata_result.success and cdata: ProfileManager.register_new_default_async(cdata.id, udata_result.model.id) uname = RootUserManager.get_root_data_uname( udata_result.model.get_oid(), cdata).user_name if uname: joined_names.append(uname) LINE.temp_apply_format(event_dest_fmt, logging.INFO, "LINE Join Group.", extra={ ExtraKey.Event: event, ExtraKey.Destination: destination }) LineApiWrapper.reply_text( event.reply_token, _("{} joined the group.").format(" & ".join(joined_names)))
async def on_member_remove(self, member: Member): udata_result = RootUserManager.get_root_data_onplat(Platform.DISCORD, member.id, auto_register=True) cdata = ChannelManager.get_channel_token(Platform.DISCORD, member.guild.id, auto_register=True) if udata_result.success and cdata: ProfileManager.mark_unavailable_async(cdata.id, udata_result.model.id) sys_channel = member.guild.system_channel if sys_channel: await sys_channel.send(_("{} left the server.").format(member.mention))
def post(self, request, *args, **kwargs): data = get_post_keys(request.POST) root_uid = get_root_oid(request) model = ProfileManager.register_new(root_uid, ProfileManager.process_create_profile_kwargs(data)) if model: messages.info(request, _("Profile successfully created.")) else: messages.warning(request, _("Failed to create the profile.")) return redirect(reverse("info.profile", kwargs={"profile_oid": model.id}))
def get(self, request, *args, **kwargs): root_oid = get_root_oid(request) channel_data = self.get_channel_data(*args, **kwargs) profiles = ProfileManager.get_user_profiles(channel_data.model.id, root_oid) max_perm_lv = ProfileManager.get_highest_permission_level(profiles) return render_template( self.request, _("Create Profile"), "account/channel/prof/create.html", { "channel_oid": channel_data.model.id, "max_perm_lv": max_perm_lv, "perm_cats_controllable": ProfilePermissionDefault.get_overridden_permissions(max_perm_lv), "perm_cats": list(ProfilePermission), "value_color": ColorFactory.DEFAULT.color_hex }, nav_param=kwargs)
def action_detach(request, channel_oid, sender_oid, target_uid, permissions, profile_oid): target_self = sender_oid == target_uid # Permission Check if target_self and ProfilePermission.PRF_CONTROL_SELF in permissions: pass elif ProfilePermission.PRF_CONTROL_MEMBER in permissions: pass else: return HttpResponse(status=403) # Main Action detach_outcome = ProfileManager.detach_profile(channel_oid, profile_oid, sender_oid, target_uid) # Alert messages if detach_outcome.is_success: messages.info(request, _("Profile detached.")) else: messages.warning( request, _("Failed to detach the profile. ({})").format(detach_outcome)) return redirect( reverse("info.profile", kwargs={"profile_oid": profile_oid}))
def get(self, request, **kwargs): profile_result = get_profile_data(kwargs) if not profile_result.ok: return WebsiteErrorView.website_error( request, WebsiteError.PROFILE_NOT_FOUND, {"profile_oid": profile_result.oid_org}) root_oid = get_root_oid(request) profile_model = profile_result.model channel_model = ChannelManager.get_channel_oid( profile_model.channel_oid) permissions = ProfileManager.get_user_permissions( channel_model.id, root_oid) # noinspection PyTypeChecker return render_template( request, _("Profile Info - {}").format(profile_model.name), "info/profile.html", { "profile_data": profile_model, "profile_controls": ProfileHelper.get_user_profile_controls( channel_model, profile_model.id, root_oid, permissions), "perm_cats": list(ProfilePermission), "is_default": profile_model.id == channel_model.config.default_profile_oid }, nav_param=kwargs)
def _perform_existence_check_(set_name_to_cache: bool): list_prof_conn = list(ProfileManager.get_available_connections()) def fn(): marked_unavailable = 0 dict_onplat_oids = RootUserManager.get_root_to_onplat_dict() dict_onplat_data = RootUserManager.get_onplat_data_dict() dict_channel = ChannelManager.get_channel_dict([p.channel_oid for p in list_prof_conn], accessbible_only=True) with ThreadPoolExecutor(max_workers=4, thread_name_prefix="ExstCheck") as executor: futures = [ executor.submit(_check_on_prof_conn_, d, set_name_to_cache, dict_onplat_oids, dict_onplat_data, dict_channel) for d in list_prof_conn ] # Non-lock call & Free resources when execution is done executor.shutdown(False) for completed in futures: ret = completed.result() if ret: marked_unavailable += 1 SYSTEM.logger.info(f"Marked {marked_unavailable} connections unavailable.") SYSTEM.logger.info(f"Performing user channel existence check on {len(list_prof_conn)} connections...") result = exec_timing_result(fn) SYSTEM.logger.info(f"User channel existence check completed in {result.execution_ms:.2f} ms.")
def get_profile_data(kwargs) -> ProfileDataGetResult: profile_oid_str = kwargs.get("profile_oid", "") profile_oid = safe_cast(profile_oid_str, ObjectId) model = ProfileManager.get_profile(profile_oid) return ProfileDataGetResult(ok=model is not None, model=model, oid_org=profile_oid_str)
def get_per_user_bot_usage( channel_data: ChannelModel, *, hours_within: Optional[int] = None) \ -> PerMemberStats: data = [] features = [f for f in BotFeature] members = ProfileManager.get_channel_members(channel_data.id, available_only=True) usage_data = BotFeatureUsageDataManager.get_channel_per_user_usage( channel_data.id, hours_within=hours_within, member_oid_list=[d.user_oid for d in members]).data uid_handled = IdentitySearcher.get_batch_user_name( usage_data.keys(), channel_data) for uid, name in uid_handled.items(): usage_dict = usage_data[uid] data.append( PerMemberStatsEntry( user_name=name, data_points=[usage_dict.get(ft, 0) for ft in features], data_sum=sum(usage_dict.values()))) return PerMemberStats(data=data, features=features)
def get(self, request, *args, **kwargs): channel_data = self.get_channel_data(*args, **kwargs) chcoll_data: Optional[ChannelCollectionModel] = \ ChannelCollectionManager.get_chcoll_child_channel(channel_data.model.id) root_oid = get_root_oid(request) channel_name = channel_data.model.get_channel_name(root_oid) msgdata_1d = MessageStatsDataProcessor.get_user_channel_messages(channel_data.model, hours_within=24) msgdata_7d = MessageStatsDataProcessor.get_user_channel_messages(channel_data.model, hours_within=168) member_info = InfoProcessor.get_member_info(channel_data.model) return render_template( self.request, _("Channel Info - {}").format(channel_name), "info/channel/main.html", { "channel_name": channel_name, "channel_data": channel_data.model, "chcoll_data": chcoll_data, "user_message_data1d": msgdata_1d, "user_message_data7d": msgdata_7d, "member_info": member_info, "manageable": bool(ProfileManager.get_user_profiles(channel_data.model.id, root_oid)), "bot_usage_7d": BotFeatureUsageDataManager.get_channel_usage(channel_data.model.id, hours_within=168), "bot_usage_all": BotFeatureUsageDataManager.get_channel_usage(channel_data.model.id) }, nav_param=kwargs)
def get_user_profile_controls( channel_model, profile_oid: ObjectId, requester_oid: ObjectId, permissions: Set[ProfilePermission]) \ -> List[ProfileControlEntry]: ret = [] names = IdentitySearcher.get_batch_user_name( ProfileManager.get_profile_user_oids(profile_oid), channel_model, on_not_found=None) remove_self = ProfilePermission.PRF_CONTROL_SELF in permissions remove_member = ProfilePermission.PRF_CONTROL_MEMBER in permissions is_default = channel_model.config.default_profile_oid == profile_oid for uid, name in sorted(names.items(), key=lambda item: item[1]): if not name: name = str(uid) removable = False if not is_default: if uid == requester_oid: removable = remove_self else: removable = remove_member ret.append( ProfileControlEntry(root_oid=uid, name=name, removable=removable)) return ret
def _excde_register_channel_(action_model: ExecodeEntryModel, xparams: dict) -> ExecodeCompletionOutcome: try: channel_data = ChannelManager.register( xparams[param.Execode.PLATFORM], xparams[param.Execode.CHANNEL_TOKEN]) except Exception: return ExecodeCompletionOutcome.X_IDT_CHANNEL_ERROR if channel_data: try: ProfileManager.register_new_default(channel_data.model.id, action_model.creator_oid) except Exception: return ExecodeCompletionOutcome.X_IDT_REGISTER_DEFAULT_PROFILE else: return ExecodeCompletionOutcome.X_IDT_CHANNEL_NOT_FOUND return ExecodeCompletionOutcome.O_OK
def get(self, request, *args, **kwargs): root_uid = get_root_oid(request) return render_template( request, _("Add an Auto-Reply module"), "ar/add.html", { "max_responses": AutoReply.MaxResponses, "max_length": AutoReply.MaxContentLength, "platform_list": list(Platform), "contenttype_list_kw": [ t for t in list(AutoReplyContentType) if t != AutoReplyContentType.IMAGE ], "contenttype_list_rep": list(AutoReplyContentType), "tag_splitter": AutoReply.TagSplitter, "user_ch_list": ProfileManager.get_user_channel_profiles( root_uid, inside_only=True, accessbible_only=True), "root_uid_str": str(root_uid), "perm_pin_access": ProfilePermission.AR_ACCESS_PINNED_MODULE.code, "oid_key": OID_KEY })
def get_member_info(channel_model: ChannelModel) -> List[MemberInfoEntry]: ret = [] prof_conns = ProfileManager.get_channel_members(channel_model.id, available_only=True) user_oids = [mdl.user_oid for mdl in prof_conns] user_name_dict = IdentitySearcher.get_batch_user_name( user_oids, channel_model) last_message_oids = MessageRecordStatisticsManager.get_user_last_message_ts( channel_model.id, user_oids) for prof_conn in prof_conns: user_oid = prof_conn.user_oid user_name = user_name_dict.get(user_oid) or str(user_oid) first_joined = localtime(prof_conn.id.generation_time) last_message_at = last_message_oids.get(user_oid) if last_message_at: last_message_at = localtime(last_message_at) ret.append( MemberInfoEntry(user_oid=user_oid, user_name=user_name, first_joined=first_joined, last_message_at=last_message_at)) return ret
def get(self, request, *args, **kwargs): root_uid = get_root_oid(request) return render_template( request, _("Auto-Reply ranking channel list"), "ar/rk-chlist.html", { "channel_list": ProfileManager.get_user_channel_profiles( root_uid, inside_only=True, accessbible_only=True) }, nav_param=kwargs)
def _check_on_prof_conn_( prof_conn: ChannelProfileConnectionModel, set_name_to_cache: bool, dict_onplat_oids: Dict[ObjectId, List[ObjectId]], dict_onplat_data: Dict[ObjectId, OnPlatformUserModel], dict_channel: Dict[ObjectId, ChannelModel]) -> bool: """Return ``True`` if marked unavailable. Otherwise ``False``.""" oid_user = prof_conn.user_oid list_onplat_oids = dict_onplat_oids.get(oid_user) if not list_onplat_oids: return False model_channel = dict_channel.get(prof_conn.channel_oid) if not model_channel: return False attempts = 0 attempts_allowed = len(list_onplat_oids) for oid_onplat in list_onplat_oids: model_onplat = dict_onplat_data.get(oid_onplat) if not model_onplat: MailSender.send_email_async( f"Missing OnPlatform data of data ID: {oid_onplat}<br>" f"Root User ID: {prof_conn.user_oid}", subject="Missing OnPlatform Data in Root User Model" ) continue n = model_onplat.get_name(model_channel) if n: if set_name_to_cache: set_uname_cache(model_onplat.id, n) break attempts += 1 if attempts >= attempts_allowed: ProfileManager.mark_unavailable_async(model_channel.id, oid_user) return attempts >= attempts_allowed
def handle_message_main(e: MessageEventObject) -> HandledMessageEventsHolder: try: if e.user_model: # Ensure User existence in channel ProfileManager.register_new_default_async(e.channel_model.id, e.user_model.id) # Translation activation activate(e.user_model.config.language) else: # User model could be `None` if user token is not provided. This happens on LINE. # Notify users when they attempted to use any features related of the Jelly Bot from .spec.no_utoken import handle_no_user_token return HandledMessageEventsHolder(e.channel_model, handle_no_user_token(e)) # Main handle process event_type = type(e) if event_type in fn_box: ret = HandledMessageEventsHolder(e.channel_model, fn_box[event_type](e)) else: logger.logger.info( f"Message handle object not handled. Raw: {e.raw}") ret = HandledMessageEventsHolder(e.channel_model) # Translation deactivation deactivate() # Record message for stats / User model could be `None` on LINE if e.user_model: MessageRecordStatisticsManager.record_message_async( e.channel_model.id, e.user_model.id, e.message_type, e.content, e.constructed_time) return ret except Exception as ex: from .spec.error import handle_error_main return HandledMessageEventsHolder(e.channel_model, handle_error_main(e, ex))
def post(self, request, *args, **kwargs): profile_result = get_profile_data(kwargs) if not profile_result.ok: return HttpResponse(status=404) profile_oid = profile_result.model.id outcome = ProfileManager.update_profile( profile_result.model.id, ProfileManager.process_edit_profile_kwargs(get_post_keys(request.POST))) if outcome.is_success: messages.info(request, _("Profile successfully updated.")) return redirect(reverse("info.profile", kwargs={"profile_oid": profile_oid})) else: channel_oid = profile_result.model.channel_oid messages.warning(request, _("Failed to update the profile.")) return redirect(reverse("account.profile.edit", kwargs={"channel_oid": channel_oid, "profile_oid": profile_oid}))
def handle_member_left(request, event, destination): for user in event.left.members: uid = user.user_id udata_result = RootUserManager.get_root_data_onplat(Platform.LINE, uid) cdata = ChannelManager.get_channel_token( Platform.LINE, LineApiUtils.get_channel_id(event), auto_register=True) if udata_result.success and cdata: ProfileManager.mark_unavailable_async(cdata.id, udata_result.model.id) LINE.temp_apply_format(event_dest_fmt, logging.INFO, "LINE Left Group.", extra={ ExtraKey.Event: event, ExtraKey.Destination: destination })
def get(self, request, *args, **kwargs): root_oid = get_root_oid(request) channel_data = self.get_channel_data(*args, **kwargs) channel_oid = channel_data.model.id attach_member = ProfileManager.can_control_profile_member( ProfileManager.get_user_permissions(channel_oid, root_oid)) member_list = {} if attach_member: member_list = IdentitySearcher.get_batch_user_name( ProfileManager.get_channel_member_oids(channel_oid, available_only=True), channel_data.model) member_list = sorted(member_list.items(), key=lambda item: item[1]) return render_template( self.request, _("Attach Profile"), "account/channel/prof/attach.html", { "channel_oid": channel_oid, "attachable_profiles": ProfileManager.get_attachable_profiles(channel_data.model.get_oid(), root_oid), "member_list": member_list }, nav_param=kwargs)
def get_name(self, channel_data=None) -> Optional[str]: # Checking `get_oid()` because the model might be constructed in the code (no ID) and # call `get_name()` afterward without storing it to the database if self.get_oid() is not None and self.id not in _user_name_cache_: n = None if self.platform == Platform.LINE: from extline import LineApiWrapper from models import ChannelCollectionModel if isinstance(channel_data, ChannelCollectionModel): raise ValueError( "Finding the user name with `ChannelCollectionModel` " "currently not yet supported. Check issue #38.") if channel_data: n = LineApiWrapper.get_user_name_safe( self.token, channel_data) else: n = LineApiWrapper.get_user_name_safe(self.token) elif self.platform == Platform.DISCORD: from extdiscord import DiscordClientWrapper n = DiscordClientWrapper.get_user_name_safe(self.token) if n: set_uname_cache(self.id, n) else: # Mark unavailable from mongodb.factory import ProfileManager, RootUserManager root_data_result = RootUserManager.get_root_data_onplat( self.platform, self.token, auto_register=False) if root_data_result.success: ProfileManager.mark_unavailable_async( channel_data.id, root_data_result.model.id) return _user_name_cache_.get(self.id)
def get(self, request, *args, **kwargs): channel_result = self.get_channel_data(*args, **kwargs) permissions = ProfileManager.get_user_permissions(channel_result.model.id, get_root_oid(request)) can_ced_profile = ProfilePermission.PRF_CED in permissions return render_template( self.request, _("List Profile"), "account/channel/prof/list.html", { "prof_entry": ProfileHelper.get_channel_profiles(channel_result.model.id), "perm_cats": list(ProfilePermission), "can_ced_profile": can_ced_profile, "channel_oid": channel_result.model.id }, nav_param=kwargs)
def dispatch(self, request, *args, **kwargs): root_oid = get_root_oid(request) pass_ = ProfileManager.get_user_permissions(self.get_channel_data(*args, **kwargs).model.id, root_oid)\ .issuperset(self.required_permission()) if not pass_: return WebsiteErrorView.website_error( request, WebsiteError.INSUFFICIENT_PERMISSION, { "channel_oid": self.get_channel_data(*args, ** kwargs).oid_org, "required_permission": self.required_permission() }, nav_param=kwargs) else: return super().dispatch(request, *args, **kwargs)
def check_channel_member(e: TextMessageEventObject): ret = [] user_oids = ProfileManager.get_channel_member_oids(e.channel_oid, available_only=True) user_name_dict = IdentitySearcher.get_batch_user_name(user_oids, e.channel_model, on_not_found="(N/A)") for user_oid, user_name in sorted(user_name_dict.items(), key=lambda x: x[1]): ret.append(f"<li>`{user_oid}` - {user_name}</li>") ret = '\n'.join(ret) return [ HandledMessageEventText(content=f"<ul>{ret}</ul>", force_extra=True) ]
def get(self, request, *args, **kwargs): root_oid = get_root_oid(request) access_ok = [] access_no = [] for channel_conn in ProfileManager.get_user_channel_profiles(root_oid, accessbible_only=False): if channel_conn.channel_data.bot_accessible: access_ok.append(channel_conn) else: access_no.append(channel_conn) return render_template( self.request, _("Channel List"), "account/channel/list.html", { "conn_access_ok": access_ok, "conn_access_no": access_no, "bot_cmd_info_code": cmd_id.main_cmd_code })
def get(self, request, *args, **kwargs): channel_data = self.get_channel_data(*args, **kwargs) # Check if the user is in the channel root_oid = get_root_oid(request) profs = ProfileManager.get_user_profiles(channel_data.model.id, root_oid) if not profs or profs == ChannelProfileConnectionModel.ProfileOids.none_obj( ): return WebsiteErrorView.website_error( request, WebsiteError.NOT_IN_THE_CHANNEL, {"channel_oid": channel_data.oid_org}, nav_param=kwargs) # Process the necessary data channel_name = channel_data.model.get_channel_name(root_oid) limit = get_limit(request.GET, Website.RecentActivity.MaxMessageCount) ctxt = { "channel_name": channel_name, "channel_data": channel_data.model, "recent_msg_limit": limit or "", "recent_msg_limit_max": Website.RecentActivity.MaxMessageCount, "recent_msg_data": MessageStatsDataProcessor.get_recent_messages( channel_data.model, limit) } return render_template(self.request, _("Recent Messages - {}").format(channel_name), "info/recent/message.html", ctxt, nav_param=kwargs)
def action_delete(request, channel_model, profile_oid): # Terminate if the profile to be deleted is the default profile if channel_model.config.default_profile_oid == profile_oid: messages.warning( request, _("Attempted to delete the default profile which is not allowed." )) return redirect( reverse("info.profile", kwargs={"profile_oid": profile_oid})) # Detach profile from all users and delete the profile from the database deleted = ProfileManager.delete_profile(channel_model.id, profile_oid, get_root_oid(request)) # Alert messages if deleted: messages.info(request, _("Profile deleted.")) return redirect(reverse("account.channel.list")) else: messages.warning(request, _("Failed to delete the profile.")) return redirect( reverse("info.profile", kwargs={"profile_oid": profile_oid}))