diff --git a/.gitignore b/.gitignore index b6e4761..38a6c99 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ __pycache__/ *.py[cod] *$py.class +# PyCharm +.idea/ # C extensions *.so diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..aa0e6fb --- /dev/null +++ b/AUTHORS @@ -0,0 +1,4 @@ +## AUTHORS +====================================== + +* Dedaldino Antonio, 3D - dedaldinoantonio7@gmail.com \ No newline at end of file diff --git a/LICENSE b/LICENSE index 0e259d4..bfd5c15 100644 --- a/LICENSE +++ b/LICENSE @@ -1,121 +1,21 @@ -Creative Commons Legal Code - -CC0 1.0 Universal - - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. +The MIT License (MIT) + +Copyright (c) 2020 Dedaldino Antonio, a.k.a 3D + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..0385206 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE +include README.md +include requirements.txt \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f14436 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +**A python lib to run ejabberd XML-RPC commands** + +**Features** + + +#### How to install +When working with python its a common approach to use a virtualenv to encapsulate all dependencies. +First create a virtual environment: +__if you have virtualenv installed run this code__ +```python +virtualenv ejabberd_python3d_venv +``` +__if not, so install with this code:__ +````python +pip install virtualenv +```` +and then install **ejabberd_python3d** lib: +```python +pip install ejabberd_python3d +``` + +To get the most updated version, you'll need to clone this repository: +````git +git clone http://github.com/Dedaldino3D/ejabberd_python3d.git +```` +Run +````python +python setup.py +```` + +After installation is completed, create a client instance: + +````python +from ejabberd_python3d.client import EjabberdAPIClient + +client = EjabberdAPIClient('localhost','dedaldino','123456') +users = client.registered_users('localhost') +# assuming that you have an user registered (the admin) +print(users) # [dedaldino] +client.register('dedaldino3d','localhost','nopassword') +users = client.registered_users('localhost') +print(users) # ['dedaldino3d'] +```` + diff --git a/TODOS.md b/TODOS.md new file mode 100644 index 0000000..ced464d --- /dev/null +++ b/TODOS.md @@ -0,0 +1,122 @@ +# METHODS TO IMPLEMENT +### TODO def backup(self, file): Store the database to backup file +### TODO def change_room_option(self, name, service, option, value) +### TODO def compile(self, file): +### TODO def convert_to_scram(self, host): +### TODO def convert_to_yaml(self, in, out): +### TODO def create_room(self, name, service, host): +### TODO def create_room_with_opts(self, name, service, host, options): +### TODO def create_rooms_file(self, file): +### TODO def delete_mnesia(self, host): +### TODO def delete_old_mam_messages(self, type, days): +### TODO def destroy_room(self, name, service): +### TODO def destroy_rooms_file(self, file): +### TODO def dump(self, file): +### TODO def dump_table(self, file, table): +### TODO def export2sql(self, host, file): +### TODO def export_piefxis(self, dir): +### TODO def export_piefxis_host(self, dir, host): +### TODO def gen_html_doc_for_commands(self, file, regexp, examples): +### TODO def gen_markdown_doc_for_commands(self, file, regexp, examples): +### TODO def get_offline_count(self): +### TODO def get_room_affiliations(self, name, service): +### TODO def get_room_occupants(self, name, service): +### TODO def get_room_occupants_number(self, name, service): +### TODO def get_room_options(self, name, service): +### TODO get_subscribers(self, name, service): +### TODO get_user_rooms(self, user, host): +### TODO def import_dir(self, file): +### TODO def import_file(self, file): +### TODO def import_piefxis(self, file): +### TODO def import_prosody(self, dir) Import data from Prosody +### TODO def install_fallback(self, file): +### TODO def join_cluster(self, node): +### TODO def leave_cluster(self, node): +### TODO def load(self, file): +### TODO def mnesia_change_nodename(self, +### TODO def module_check(self, module): +### TODO def module_install(self, module): +### TODO def module_uninstall(self, module): +### TODO def module_upgrade(self, module): +### TODO def modules_update_specs(self): +### TODO def muc_online_rooms(self, host): +### TODO def muc_unregister_nick(self, nick): +### TODO def privacy_set(self, user, host, xmlquery): +### TODO def private_get(self, user, host, element, ns): +### TODO def private_set(self, user, host, element): +### TODO def push_roster(self, file, user, host): +### TODO def push_roster_all(self, file): +### TODO def restore(self, file): +### TODO def rooms_unused_destroy(self, host, days): +### TODO def rooms_unused_list(self, host, days): +### TODO def rotate_log(self): +### TODO def send_direct_invitation(self, +### TODO def send_stanza(self, from, to, stanza): +### TODO def set_room_affiliation(self, name, service, jid, affiliation): +### TODO def subscribe_room(self, user, nick, room, nodes): +### TODO def unsubscribe_room(self, user, room): +### TODO: add argument options: [{name::string,value::string}]: List of options +### TODO: nodes must be separated by commas, so therefore you can use an array and before send transform arguments +### TODO: some arguments is not required +### TODO: name is between: registeredusers onlineusers onlineusersnode uptimeseconds processes +### TODO: name is between: registeredusers onlineusers +### TODO def backup(self, file): Store the database to backup file +### TODO def change_room_option(self, name, service, option, value) +### TODO def compile(self, file): +### TODO def convert_to_scram(self, host): +### TODO def convert_to_yaml(self, in, out): +### TODO def create_room(self, name, service, host): +### TODO def create_room_with_opts(self, name, service, host, options): +### TODO def create_rooms_file(self, file): +### TODO def delete_mnesia(self, host): +### TODO def delete_old_mam_messages(self, type, days): +### TODO def destroy_room(self, name, service): +### TODO def destroy_rooms_file(self, file): +### TODO def dump(self, file): +### TODO def dump_table(self, file, table): +### TODO def export2sql(self, host, file): +### TODO def export_piefxis(self, dir): +### TODO def export_piefxis_host(self, dir, host): +### TODO def gen_html_doc_for_commands(self, file, regexp, examples): +### TODO def gen_markdown_doc_for_commands(self, file, regexp, examples): +### TODO def get_offline_count(self): +### TODO def get_room_affiliations(self, name, service): +### TODO def get_room_occupants(self, name, service): +### TODO def get_room_occupants_number(self, name, service): +### TODO def get_room_options(self, name, service): +### TODO get_subscribers(self, name, service): +### TODO get_user_rooms(self, user, host): +### TODO def import_dir(self, file): +### TODO def import_file(self, file): +### TODO def import_piefxis(self, file): +### TODO def import_prosody(self, dir) Import data from Prosody +### TODO def install_fallback(self, file): +### TODO def join_cluster(self, node): +### TODO def leave_cluster(self, node): +### TODO def load(self, file): +### TODO def mnesia_change_nodename(self, +### TODO def module_check(self, module): +### TODO def module_install(self, module): +### TODO def module_uninstall(self, module): +### TODO def module_upgrade(self, module): +### TODO def modules_update_specs(self): +### TODO def muc_online_rooms(self, host): +### TODO def muc_unregister_nick(self, nick): +### TODO def privacy_set(self, user, host, xmlquery): +### TODO def private_get(self, user, host, element, ns): +### TODO def private_set(self, user, host, element): +### TODO def push_roster(self, file, user, host): +### TODO def push_roster_all(self, file): +### TODO def restore(self, file): +### TODO def rooms_unused_destroy(self, host, days): +### TODO def rooms_unused_list(self, host, days): +### TODO def rotate_log(self): +### TODO def send_direct_invitation(self, +### TODO def send_stanza(self, from, to, stanza): +### TODO def set_room_affiliation(self, name, service, jid, affiliation): +### TODO def subscribe_room(self, user, nick, room, nodes): +### TODO def unsubscribe_room(self, user, room): + +## Generics TODOS +### TODO: add endpoint parameter +### TODO: add it to logger \ No newline at end of file diff --git a/ejabberd_python3d/__init__.py b/ejabberd_python3d/__init__.py new file mode 100644 index 0000000..e7bc29a --- /dev/null +++ b/ejabberd_python3d/__init__.py @@ -0,0 +1,2 @@ +from ejabberd_python3d import client, serializers, ejabberd_auth +from ejabberd_python3d import muc, abc, defaults diff --git a/ejabberd_python3d/abc/__init__.py b/ejabberd_python3d/abc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ejabberd_python3d/abc/api.py b/ejabberd_python3d/abc/api.py new file mode 100644 index 0000000..f16669f --- /dev/null +++ b/ejabberd_python3d/abc/api.py @@ -0,0 +1,770 @@ +from __future__ import unicode_literals + +from abc import ABC, abstractmethod +from enum import Enum as BaseClassEnum + + +class APIArgumentSerializer(ABC): + @abstractmethod + def to_api(self, value): + pass + + @abstractmethod + def to_builtin(self, value): + pass + + +class APIArgument(ABC): + def __init__(self, name, description=None, required=True, **kwargs): + self.name = name + self.description = description + self.required = required + + @abstractmethod + def serializer_class(self): + pass + + +class Enum(BaseClassEnum): + @classmethod + def get_by_name(cls, name): + return getattr(cls, name, None) + + @classmethod + def get_by_value(cls, value): + return cls(value) + + +class API(ABC): + @abstractmethod + def method(self): + """ + Return the exact name of the XML-RPC API method to call + """ + pass + + @abstractmethod + def arguments(self): + """ + Return an (ordered) list of APIArgument objects + """ + pass + + @property + def authenticate(self): + """ + Defines whether or not we should authenticate when calling API + """ + return True + + def transform_arguments(self, **kwargs): + """ + Handler methods to transform an argument before processing + :param kwargs: Named argument dictionary + """ + return kwargs + + def validate_response(self, api, arguments, response): + """ + Handler to validate the API response, Can be used + to raise an Exception to indicate fail, the pipeline will continue with the + 'transform_response' method + :param arguments: The dictionary containing the arguments that have been used to perform the call + :param response: object + """ + pass + + def transform_response(self, api, arguments, response): + """ + Handler method to process the response, The output of this method + will be return as the output of the API + :param response: + :param api: The api object that has been used fot the call + :param arguments: The dictionary containing the arguments + """ + return response + + +class EjabberdBaseAPI(ABC): + @abstractmethod + def echo(self, sentence): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def registered_users(self, host): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def register(self, user, host, password): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def unregister(self, user, host): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def change_password(self, user, host, newpass): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def check_password_hash(self, user, host, password): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def connected_users(self): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def connected_users_info(self): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def connected_users_number(self): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def user_sessions_info(self, user, host): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def muc_online_rooms(self, service=None): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def create_room(self, name, service, host): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def destroy_room(self, name, service): + """Destroy a MUC room""" + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def get_room_options(self, name, service): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def change_room_option(self, name, service, option, value): + """Change an option in a MUC room""" + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def set_room_affiliation(self, name, service, jid, affiliation): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def get_room_affiliations(self, name, service): + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def add_rosteritem(self, + localuser, localhost, + user, host, + nick, group, subs): + """ + Add an item to a user's roster (self,supports ODBC): + """ + raise NotImplementedError("subclass must implement this method") + + def backup(self, file): + """Store the database to backup file""" + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def ban_account(self, user, host, reason): + """ + Ban an account: kick sessions and set random password + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def check_account(self, user, host): + """ + Check if an account exists or not + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def check_password(self, user, host, password): + """ + Check if a password is correct + """ + raise NotImplementedError("subclass must implement this method") + + def compile(self, file): + """Recompile and reload Erlang source code file""" + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def connected_users_vhost(self, host): + """ + Get the list of established sessions in a vhost + """ + raise NotImplementedError("subclass must implement this method") + + def convert_to_scram(self, host): + """Convert the passwords in ‘users’ SQL table to SCRAM """ + raise NotImplementedError("subclass must implement this method") + + def convert_to_yaml(self, in_file, out_file): + """Convert the input file from Erlang to YAML format""" + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def create_room_with_opts(self, name, service, host, options): + """ + Create a MUC room name@service in host with given options + """ + raise NotImplementedError("subclass must implement this method") + + def create_rooms_file(self, file): + """Create the rooms indicated in file""" + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def delete_expired_messages(self): + """ + Delete expired offline messages from database + """ + raise NotImplementedError("subclass must implement this method") + + def delete_mnesia(self, host): + """Delete elements in Mnesia database for a given vhost""" + raise NotImplementedError("subclass must implement this method") + + def delete_old_mam_messages(self, type, days): + """Delete MAM messages older than DAYS""" + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def delete_old_messages(self, days): + """ + Delete offline messages older than DAYS + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def delete_old_users(self, days): + """ + Delete users that didn't log in last days, or that never logged + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def delete_old_users_vhost(self, host, days): + """ + Delete users that didn't log in last days in vhost, + or that never logged + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def delete_rosteritem(self, localuser, localserver, user, server): + """ + Delete an item from a user's roster (self,supports ODBC): + """ + raise NotImplementedError("subclass must implement this method") + + def destroy_rooms_file(self, file): + """Destroy the rooms indicated in file. Provide one room JID per line.""" + raise NotImplementedError("subclass must implement this method") + + def dump(self, file): + """Dump the database to text file""" + raise NotImplementedError("subclass must implement this method") + + # TODO def dump_table(self, file, table): + # Dump a table to text file + + # TODO def export2sql(self, host, file): + # Export virtual host information from Mnesia tables to SQL files + + # TODO def export_piefxis(self, dir): + # Export data of all users in the server to PIEFXIS files (XEP-0227) + + # TODO def export_piefxis_host(self, dir, host): + # Export data of users in a host to PIEFXIS files (XEP-0227) + + # TODO def gen_html_doc_for_commands(self, file, regexp, examples): + # Generates html documentation for ejabberd_commands + + # TODO def gen_markdown_doc_for_commands(self, file, regexp, examples): + # Generates markdown documentation for ejabberd_commands + @abstractmethod + def get_cookie(self): + """ + Get the Erlang cookie of this node + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def get_last(self, user, host): + """ + Get last activity information (self,timestamp and status): + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def get_loglevel(self): + """ + Get the current loglevel + """ + raise NotImplementedError("subclass must implement this method") + + # TODO def get_offline_count(self): + # Get the number of unread offline messages + + # TODO def get_room_affiliations(self, name, service): + # Get the list of affiliations of a MUC room + + def get_room_occupants(self, name, service): + """ + Get the list of occupants of a MUC room + """ + raise NotImplementedError("subclass must implement this method") + + # TODO def get_room_occupants_number(self, name, service): + # Get the number of occupants of a MUC room + + # TODO def get_room_options(self, name, service): + # Get options from a MUC room + @abstractmethod + def get_roster(self, user, server): + """ + Get roster of a local user. + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def get_subscribers(self, name, service): + """ + List subscribers of a MUC conference + """ + raise NotImplementedError("subclass must implement this method") + + def get_user_rooms(self, user, host): + """ + Get the list of rooms where this user is occupant + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def get_vcard(self, user, host, name): + """ + Get content from a vCard field + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def get_vcard2(self, user, host, name, subname): + """ + Get content from a vCard field + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def get_vcard2_multi(self, user, host, name, subname): + """ + Get multiple contents from a vCard field + """ + raise NotImplementedError("subclass must implement this method") + + # TODO def import_dir(self, file): + # Import users data from jabberd14 spool dir + + # TODO def import_file(self, file): + # Import users data from jabberd14 spool file + + # TODO def import_piefxis(self, file): + # Import users data from a PIEFXIS file (XEP-0227) + + # TODO def import_prosody(self, dir) Import data from Prosody + @abstractmethod + def incoming_s2s_number(self): + """ + Number of incoming s2s connections on the node + """ + raise NotImplementedError("subclass must implement this method") + + # TODO def install_fallback(self, file): + # Install the database from a fallback file + + # TODO def join_cluster(self, node): + # Join this node into the cluster handled by Node + @abstractmethod + def kick_session(self, user, host, resource, reason): + """ + Kick a user session + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def kick_user(self, user, host): + """ + Disconnect user's active sessions + """ + raise NotImplementedError("subclass must implement this method") + + # TODO def leave_cluster(self, node): + # Remove node handled by Node from the cluster + @abstractmethod + def list_cluster(self): + """ + List nodes that are part of the cluster handled by Node + """ + raise NotImplementedError("subclass must implement this method") + + # TODO def load(self, file): + # Restore the database from text file + + # TODO def mnesia_change_nodename(self, + # oldnodename, + # newnodename, + # oldbackup, + # newbackup): + # Change the erlang node name in a backup file + + # TODO def module_check(self, module): + + # TODO def module_install(self, module): + + # TODO def module_uninstall(self, module): + + # TODO def module_upgrade(self, module): + @abstractmethod + def modules_available(self): + """ + List available modules + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def modules_installed(self): + """ + List installed modules + """ + raise NotImplementedError("subclass must implement this method") + + # TODO def modules_update_specs(self): + + # TODO def muc_online_rooms(self, host): + # List existing rooms (‘global’ to get all vhosts) + + # TODO def muc_unregister_nick(self, nick): + # Unregister the nick in the MUC service + + @abstractmethod + def num_resources(self, user, host): + """ + Get the number of resources of a user + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def outgoing_s2s_number(self): + """ + Number of outgoing s2s connections on the node + """ + raise NotImplementedError("subclass must implement this method") + + # TODO def privacy_set(self, user, host, xmlquery): + # Send a IQ set privacy stanza for a local account + + # TODO def private_get(self, user, host, element, ns): + # Get some information from a user private storage + + # TODO def private_set(self, user, host, element): + # Set to the user private storage + @abstractmethod + def process_rosteritems(self, action, subs, asks, users, contacts): + """ + List or delete rosteritems that match filtering options + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def push_alltoall(self, host, group): + """ + Add all the users to all the users of Host in Group + """ + raise NotImplementedError("subclass must implement this method") + + # TODO def push_roster(self, file, user, host): + # Push template roster from file to a user + + # TODO def push_roster_all(self, file): + # Push template roster from file to all those users + @abstractmethod + def registered_vhosts(self): + """ + List all registered vhosts in SERVER + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def reload_config(self): + """ + Reload ejabberd configuration file into memory + (only affects ACL and Access) + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def reopen_log(self): + """ + Reopen the log files + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def resource_num(self, user, host, num): + """ + Resource string of a session number + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def restart(self): + """ + Restart ejabberd + """ + raise NotImplementedError("subclass must implement this method") + + # TODO def restore(self, file): + # Restore the database from backup file + + # TODO def rooms_unused_destroy(self, host, days): + # Destroy the rooms that are unused for many days in host + + # TODO def rooms_unused_list(self, host, days): + # List the rooms that are unused for many days in host + + # TODO def rotate_log(self): + # Rotate the log files + + # TODO def send_direct_invitation(self, + # name, + # service, + # password, + # reason, + # users): + # Send a direct invitation to several destinations + @abstractmethod + def send_message(self, type, from_jid, to, subject, body): + """ + Send a message to a local or remote bare of full JID + """ + raise NotImplementedError("subclass must implement this method") + + # TODO def send_stanza(self, from, to, stanza): + # Send a stanza; provide From JID and valid To JID + @abstractmethod + def send_stanza_c2s(self, user, host, resource, stanza): + """ + Send a stanza as if sent from a c2s session + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def set_last(self, user, host, timestamp, status): + """ + Set last activity information + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def set_loglevel(self, loglevel): + """ + Set the loglevel (0 to 5) + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def set_master(self, nodename): + """ + Set master node of the clustered Mnesia tables + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def set_nickname(self, user, host, nickname): + """ + Set nickname in a user's vCard + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def set_presence(self, user, host, resource, type, show, status, priority): + """ + Set presence of a session + """ + raise NotImplementedError("subclass must implement this method") + + # TODO def set_room_affiliation(self, name, service, jid, affiliation): + # Change an affiliation in a MUC room + + @abstractmethod + def set_vcard(self, user, host, name, content): + """ + Set content in a vCard field + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def set_vcard2(self, user, host, name, subname, content): + """ + Set content in a vCard subfield + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def set_vcard2_multi(self, user, host, name, subname, contents): + """ + *Set multiple contents in a vCard subfield + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def srg_create(self, group, host, name, description, display): + """ + Create a Shared Roster Group + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def srg_delete(self, group, host): + """ + Delete a Shared Roster Group + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def srg_get_info(self, group, host): + """ + Get info of a Shared Roster Group + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def srg_get_members(self, group, host): + """ + Get members of a Shared Roster Group + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def srg_list(self, host): + """ + List the Shared Roster Groups in Host + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def srg_user_add(self, user, host, group, grouphost): + """ + Add the JID user@host to the Shared Roster Group + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def srg_user_del(self, user, host, group, grouphost): + """ + Delete this JID user@host from the Shared Roster Group + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def stats(self, name): + """ + Get statistical value: + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def stats_host(self, name, host): + """ + Get statistical value for this host: + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def status(self): + """ + Get ejabberd status + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def status_list(self, status): + """ + List of logged users with this status + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def status_list_host(self, host, status): + """ + List of users logged in host with their statuses + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def status_num(self, status): + """ + Number of logged users with this status + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def status_num_host(self, host, status): + """ + Number of logged users with this status in host + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def stop(self): + """ + Stop ejabberd + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def stop_kindly(self, delay, announcement): + """ + Inform users and rooms, wait, and stop the server + """ + raise NotImplementedError("subclass must implement this method") + + def subscribe_room(self, user, nick, room, nodes): + """ + Subscribe to a MUC conference + """ + raise NotImplementedError("subclass must implement this method") + + def unsubscribe_room(self, user, room): + """ + Unsubscribe from a MUC conference + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def update(self, module): + """ + Update the given module, or use the keyword: all + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def update_list(self): + """ + List modified modules that can be updated + """ + raise NotImplementedError("subclass must implement this method") + + @abstractmethod + def user_resources(self, user, host): + """ + List user's connected resources + """ + raise NotImplementedError("subclass must implement this method") diff --git a/ejabberd_python3d/abc/methods.py b/ejabberd_python3d/abc/methods.py new file mode 100644 index 0000000..512f165 --- /dev/null +++ b/ejabberd_python3d/abc/methods.py @@ -0,0 +1,953 @@ +from __future__ import unicode_literals + +from ejabberd_python3d.abc.api import API +from ejabberd_python3d.core.errors import UserAlreadyRegisteredError +from ejabberd_python3d.core.utils import format_password_hash_sha +from ejabberd_python3d.defaults import LogLevelOptions, loglevel_options_serializers +from ejabberd_python3d.defaults.arguments import StringArgument, IntegerArgument, PositiveIntegerArgument, \ + LogLevelArgument, ListArgument, GenericArgument +from ejabberd_python3d.muc import muc_room_options_serializers +from ejabberd_python3d.muc.arguments import MUCRoomArgument, AffiliationArgument +from ejabberd_python3d.muc.enums import Affiliation, MUCNodes +from ejabberd_python3d.muc.serializers import MUCNodesSerializer +from ejabberd_python3d.serializers import StringSerializer +from six import string_types + + +class Echo(API): + method = 'dedaldino_denis_3D' + arguments = [StringArgument('sentence')] + + def transform_response(self, api, arguments, response): + return response.get('repeated') + + +class RegisteredUsers(API): + method = 'registered_users' + arguments = [StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get('users', []) + + +class Register(API): + method = 'register' + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('password')] + + def validate_response(self, api, arguments, response): + if response.get('res') == 1: + username = arguments.get('user') + raise UserAlreadyRegisteredError('User with username %s already exist' % username) + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class Unregister(API): + method = 'unregister' + arguments = [StringArgument('user'), StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class ChangePassword(API): + method = 'change_password' + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('newpass')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class CheckPasswordHash(API): + method = 'check_password_hash' + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('passwordhash'), + StringArgument('hashmethod')] + + def transform_arguments(self, **kwargs): + password_hash = format_password_hash_sha(password=kwargs.pop('password')) + kwargs.update({ + 'passwordhash': password_hash, + 'hashmethod': 'sha' + }) + return kwargs + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class SetNickname(API): + method = 'set_nickname' + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('nickname')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class ConnectedUsers(API): + method = 'connected_users' + arguments = [] + + def transform_response(self, api, arguments, response): + connected_users = response.get('connected_users', []) + + return [user["sessions"] for user in connected_users] + + +class ConnectedUsersInfo(API): + method = 'connected_users_info' + arguments = [] + + def transform_response(self, api, arguments, response): + connected_users_info = response.get('connected_users_info', []) + + return [user["sessions"] for user in connected_users_info] + + +class ConnectedUsersNumber(API): + method = 'connected_users_number' + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get('num_sessions') + + +class UserSessionInfo(API): + method = 'user_sessions_info' + arguments = [StringArgument('user'), StringArgument('host')] + + def transform_response(self, api, arguments, response): + sessions_info = response.get('sessions_info', []) + return [ + dict((k, v) for property_k_v in session["session"] for k, v in property_k_v.items()) + for session in sessions_info + ] + + +class CreateRoom(API): + method = 'create_room' + arguments = [StringArgument('name'), StringArgument('service'), StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class DestroyRoom(API): + method = 'destroy_room' + arguments = [StringArgument('name'), StringArgument('service')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class GetRoomOptions(API): + method = 'get_room_options' + arguments = [StringArgument('name'), StringArgument('service')] + + def transform_response(self, api, arguments, response): + result = {} + for option_dict in response.get('options', []): + option = option_dict.get('option') + if option is None: + raise ValueError('Unexpected option in response: {}'.format(str(option_dict))) + name_dict, value_dict = option + result[name_dict['name']] = value_dict['value'] + return result + + +class ChangeRoomOption(API): + method = 'change_room_option' + arguments = [StringArgument('name'), StringArgument('service'), MUCRoomArgument('option'), StringArgument('value')] + + def transform_arguments(self, **kwargs): + option = kwargs.get('option') + serializer_class = muc_room_options_serializers.get(option, StringSerializer) + kwargs['value'] = serializer_class().to_api(kwargs['value']) + return kwargs + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class GetRoomAffiliation(API): + method = 'get_room_affiliation' + arguments = [StringArgument('name'), StringArgument('service'), StringArgument('jid')] + + def transform_response(self, api, arguments, response): + return response.get('affiliation') == 0 + + +class SetRoomAffiliation(API): + method = 'set_room_affiliation' + arguments = [StringArgument('name'), StringArgument('service'), StringArgument('jid'), + AffiliationArgument('affiliation')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class GetRoomAffiliations(API): + method = 'get_room_affiliations' + arguments = [StringArgument('name'), StringArgument('service')] + + def transform_response(self, api, arguments, response): + affiliations = response.get('affiliations', []) + return [{ + 'username': subdict['affiliation'][0]['username'], + 'domain': subdict['affiliation'][1]['domain'], + 'affiliation': Affiliation.get_by_name(subdict['affiliation'][2]['affiliation']), + 'reason': subdict['affiliation'][3]['reason'], + } for subdict in affiliations] + + +class AddRosterItem(API): + method = 'add_rosteritem' + arguments = [StringArgument('localuser'), StringArgument('localhost'), + StringArgument('user'), StringArgument('host'), + StringArgument('nick', required=False), StringArgument('group', required=False), + StringArgument('subs', required=False)] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class DeleteRosterItem(API): + method = 'delete_rosteritem' + arguments = [StringArgument('localuser'), StringArgument('localserver'), + StringArgument('user'), StringArgument('server')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class GetRoster(API): + method = 'get_roster' + arguments = [StringArgument('user'), StringArgument('server')] + + def transform_response(self, api, arguments, response): + roster = [] + for contact in response.get('contacts', []): + contact_details = {} + for parameter in contact['contact']: + for key, value in parameter.items(): + contact_details[key] = value + roster.append(contact_details) + return roster + + +class Backup(API): + method = 'backup' + arguments = [StringArgument('file')] + + def transform_response(self, api, arguments, response): + return response.get('res').lower() == "success" + + +class BanAccount(API): + method = 'ban_account' + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('reason')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class BookmarksToPEP(API): + method = 'bookmarks_to_pep' + arguments = [StringArgument('user'), StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get('res') + + +class CheckAccount(API): + method = 'check_account' + arguments = [StringArgument('user'), StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class ClearCache(API): + method = 'clear_cache' + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class Compile(API): + method = 'compile' + arguments = [StringArgument('file')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class ConnectedUsersVhost(API): + method = 'connected_users_vhost' + arguments = [StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get('connected_users_vhost', []) + + +class ConvertToSCRAM(API): + method = 'convert_to_scram' + arguments = [StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class CreateRoomWithOpts(API): + method = "create_room_with_opts" + arguments = [StringArgument('name'), StringArgument('service'), StringArgument('host'), GenericArgument('options')] + + def transform_arguments(self, **kwargs): + options = kwargs.pop('options') + op = [] + for opt in options: + serializer_class = muc_room_options_serializers.get(opt['name'], StringSerializer) + if not isinstance(opt['name'], string_types): + opt['name'] = serializer_class().to_api(opt['name']) + opt['value'] = serializer_class().to_api(opt['value']) + op.append(opt) + kwargs.update({ + 'options': op + }) + return kwargs + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class GetLast(API): + method = 'get_last' + arguments = [StringArgument('user'), StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get('last_activity', {}) + + +class GetOfflineCount(API): + method = "get_offline_count" + arguments = [StringArgument('user'), StringArgument('server')] + + def transform_response(self, api, arguments, response): + return response.get('value') + + +class GetPresence(API): + method = "get_presence" + arguments = [StringArgument('user'), StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get('presence') + + +class GetRoomOccupants(API): + method = 'get_room_occupants' + arguments = [StringArgument('name'), StringArgument('service')] + + def transform_response(self, api, arguments, response): + return response.get('occupants') + + +class GetRoomOccupantsNumber(API): + method = 'get_room_occupants_number' + arguments = [StringArgument('name'), StringArgument('service')] + + def transform_response(self, api, arguments, response): + return response.get('occupants') + + +class GetSubscribers(API): + method = 'get_subscribers' + arguments = [StringArgument('name'), StringArgument('service')] + + def transform_response(self, api, arguments, response): + return response.get('subscribers') + + +class GetUserRooms(API): + method = 'get_user_rooms' + arguments = [StringArgument('user'), StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get('rooms') + + +class MucOnlineRooms(API): + method = "muc_online_rooms" + arguments = [StringArgument('service')] + + def transform_response(self, api, arguments, response): + return response.get('rooms') + + +class MucOnlineRoomsByRegex(API): + method = "muc_online_rooms_bt_regex" + arguments = [StringArgument('service'), StringArgument('regex')] + + def transform_response(self, api, arguments, response): + return response.get('rooms') + + +class MucRegisterNick(API): + method = "muc_register_nick" + arguments = [StringArgument('nick'), StringArgument('service'), StringArgument('service')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class MucUnRegisterNick(API): + method = "muc_unregister_nick" + arguments = [StringArgument('service'), StringArgument('service')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class SendMessage(API): + method = "send_message" + arguments = [StringArgument('type'), StringArgument('from'), StringArgument('to'), + StringArgument('subject', required=False), StringArgument('body')] + + def transform_arguments(self, **kwargs): + from_jid = kwargs.pop('from_jid') + kwargs.update({ + 'from': from_jid + }) + return kwargs + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class SetLast(API): + method = "set_last" + arguments = [StringArgument('user'), StringArgument('host'), IntegerArgument('timestamp'), + StringArgument('status')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class SubscribeRoom(API): + method = "subscribe_room" + arguments = [StringArgument('user'), StringArgument('nick'), StringArgument('room'), + ListArgument('nodes', required=False)] + + def transform_arguments(self, **kwargs): + nodes = kwargs.pop("nodes") + if nodes is None or not isinstance(nodes, list): + nodes = [MUCNodes.mucsub_system, MUCNodes.mucsub_subscribers, MUCNodes.mucsub_subject, + MUCNodes.mucsub_config, MUCNodes.mucsub_presence, MUCNodes.mucsub_messages, + MUCNodes.mucsub_affiliations] + nd = [] + for v in nodes: + serializer_class = MUCNodesSerializer + t = serializer_class().to_api(v) + nd.append(t) + kwargs['nodes'] = nd + return kwargs + + def transform_response(self, api, arguments, response): + return response.get('nodes') + + +class UnSubscribeRoom(API): + method = "unsubscribe_room" + arguments = [StringArgument('user'), StringArgument('room')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class CheckPassword(API): + method = "check_password" + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('password')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class DeleteExpiredMessages(API): + method = "check_password" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class DeleteOldMessages(API): + method = "delete_old_messages" + arguments = [PositiveIntegerArgument('days')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class DeleteOldUsers(API): + method = "delete_old_users" + arguments = [PositiveIntegerArgument('days')] + + def transform_response(self, api, arguments, response): + return response.get('res') + + +class DeleteOldUsersVhost(API): + method = "delete_old_users_vhost" + arguments = [StringArgument('host'), PositiveIntegerArgument('days')] + + def transform_response(self, api, arguments, response): + return response.get('res') + + +class GetCookie(API): + method = "get_cookie" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get('cookie') + + +class GetLogLevel(API): + method = "get_loglevel" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get('levelatom ') + + +class GetVcard(API): + method = "get_vcard" + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('name')] + + def transform_response(self, api, arguments, response): + return response.get('content') + + +class GetVcard2(API): + method = "get_vcard2" + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('name'), StringArgument('subname')] + + def transform_response(self, api, arguments, response): + return response.get("content") + + +class GetVcard2Multi(API): + method = "get_vcard2_multi" + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('name'), StringArgument('subname')] + + def transform_response(self, api, arguments, response): + return response.get("contents") + + +class IncomingS2SNumber(API): + method = "incoming_s2s_number" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get("s2s_incoming") + + +class KickSession(API): + method = "kick_session" + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('resource'), StringArgument('reason')] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class ListCluster(API): + method = "list_cluster" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get("nodes") + + +class ModulesAvailable(API): + method = "modules_available" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get("modules") + + +class ModulesInstalled(API): + method = "modules_installed" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get("modules") + + +class NumResources(API): + method = "num_resources" + arguments = [StringArgument('user'), StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get("resources") + + +class OutgoingS2SNumber(API): + method = "outgoing_s2s_number" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get('s2s_outgoing') + + +class ProcessRosterItems(API): + method = "process_rosteritems" + arguments = [StringArgument('action'), StringArgument('subs'), StringArgument('asks'), StringArgument('users'), + StringArgument('contacts')] + + def transform_response(self, api, arguments, response): + return response.get("response") + + +class PushAllToAll(API): + method = "push_alltoall" + arguments = [StringArgument('host'), StringArgument('group')] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class RegisteredVhosts(API): + method = "registered_vhosts" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get("vhosts") + + +class ReloadConfig(API): + method = "reload_config" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class ReopenLog(API): + method = "reopen_log" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class ResourceNum(API): + method = "resource_num" + arguments = [StringArgument('user'), StringArgument('host'), PositiveIntegerArgument('num')] + + def transform_response(self, api, arguments, response): + return response.get("resource") + + +class Restart(API): + method = "restart" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class SendStanzaC2S(API): + method = "send_stanza_c2s" + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('resource'), StringArgument('stanza')] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class SetLogLevel(API): + method = "set_loglevel" + arguments = [LogLevelArgument('loglevel')] + + def transform_arguments(self, **kwargs): + option = kwargs.pop('loglevel') + assert isinstance(option, LogLevelOptions) + serializer_class = loglevel_options_serializers.get(option, StringSerializer) + kwargs['value'] = serializer_class().to_api(kwargs['value']) + return kwargs + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class SetMaster(API): + method = "set_master" + arguments = [StringArgument('nodename')] + + def transform_response(self, api, arguments, response): + return response.get("res") + + +class SetPresence(API): + method = "set_presence" + # TODO: some arguments is not required + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('resource'), StringArgument('type'), + StringArgument('show'), StringArgument('status'), StringArgument('priority')] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class SetVcard(API): + method = "set_vcard" + arguments = [StringArgument('user'), StringArgument('host'), StringArgument('name'), StringArgument('content')] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class SetVcard2(API): + method = "set_vcard2" + arguments = [StringArgument("user"), StringArgument("host"), StringArgument("name"), StringArgument("subname"), + StringArgument("content")] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class SetVcardMulti(API): + method = "set_vcard2_multi" + arguments = [StringArgument("user"), StringArgument("host"), StringArgument("name"), StringArgument("subname"), + StringArgument("contents")] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class SrgCreate(API): + method = "srg_create" + arguments = [StringArgument("group"), StringArgument("host"), StringArgument("name"), StringArgument("description"), + StringArgument("display")] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class SrgDelete(API): + method = "srg_delete" + arguments = [StringArgument("group"), StringArgument("host")] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class SrgGetInfo(object): + method = "srg_get_info" + arguments = [StringArgument("group"), StringArgument("host")] + + def transform_response(self, api, arguments, response): + return response.get("informations") + + +class SrgGetMembers(object): + method = "srg_get_members" + arguments = [StringArgument("group"), StringArgument("host")] + + def transform_response(self, api, arguments, response): + return response.get("members") + + +class SrgList(API): + method = "srg_list" + arguments = [StringArgument("host")] + + def transform_response(self, api, arguments, response): + return response.get("groups") + + +class SrgUserAdd(API): + method = "srg_user_add" + arguments = [StringArgument("user"), StringArgument("host"), StringArgument("group"), StringArgument("grouphost")] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class SrgUserDel(API): + method = "srg_user_del" + arguments = [StringArgument("user"), StringArgument("host"), StringArgument("group"), StringArgument("grouphost")] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class Stats(API): + method = "stats" + # TODO: name is between: registeredusers onlineusers onlineusersnode uptimeseconds processes + arguments = [StringArgument('name')] + + def transform_response(self, api, arguments, response): + return response.get("stat") + + +class StatsHost(API): + method = "stats_host" + # TODO: name is between: registeredusers onlineusers + arguments = [StringArgument('name'), StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get("stat") + + +class Status(API): + method = "status" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get("res") + + +class StatusList(API): + method = "status_list" + arguments = [StringArgument("status")] + + def transform_response(self, api, arguments, response): + return response.get("users") + + +class StatusListHost(API): + method = "status_list_host" + arguments = [StringArgument("host"), StringArgument("status")] + + def transform_response(self, api, arguments, response): + return response.get("users") + + +class StatusNum(API): + method = "status_num" + arguments = [StringArgument("status")] + + def transform_response(self, api, arguments, response): + return response.get("users") + + +class StatusNumHost(API): + method = "status_num_host" + arguments = [StringArgument("host"), StringArgument("status")] + + def transform_response(self, api, arguments, response): + return response.get("users") + + +class Stop(API): + method = "stop" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class StopKindly(API): + method = "stop_kindly" + arguments = [PositiveIntegerArgument("delay"), StringArgument("announcement")] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class Update(API): + method = "update" + arguments = [StringArgument("module")] + + def transform_response(self, api, arguments, response): + return response.get("res") + + +class UpdateList(API): + method = "update_list" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get("modules") + + +class UpdateSql(object): + method = "update_sql" + arguments = [] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class UserResources(API): + method = "update_sql" + arguments = [StringArgument("user"), StringArgument("host")] + + def transform_response(self, api, arguments, response): + return response.get("resources") + + +class KickUser(API): + method = "kick_user" + arguments = [StringArgument("user"), StringArgument("host")] + + def transform_response(self, api, arguments, response): + return response.get("num_resources") + + +class ConvertToYAML(API): + method = 'convert_to_yaml' + arguments = [StringArgument('in_file'), StringArgument('out_file')] + + def transform_arguments(self, **kwargs): + in_file = kwargs.pop('in_file') + out_file = kwargs.pop('out_file') + kwargs.update({ + 'in': in_file, + 'out': out_file, + }) + return kwargs + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class CreateRoomsFile(API): + method = "create_rooms_file" + arguments = [StringArgument('file')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class DeleteMnesia(API): + method = "delete_mnesia" + arguments = [StringArgument('host')] + + def transform_response(self, api, arguments, response): + return response.get("res") == 0 + + +class DeleteOldMAMMessages(API): + method = "delete_old_mam_messages" + arguments = [StringArgument('type'), StringArgument('days')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class DestroyRoomsFile(API): + method = "destroy_rooms_file" + arguments = [StringArgument('file')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 + + +class Dump(API): + method = "dump" + arguments = [StringArgument('file')] + + def transform_response(self, api, arguments, response): + return response.get('res') == 0 diff --git a/ejabberd_python3d/client.py b/ejabberd_python3d/client.py new file mode 100644 index 0000000..4d18914 --- /dev/null +++ b/ejabberd_python3d/client.py @@ -0,0 +1,1590 @@ +from __future__ import print_function + +import copy +from urllib.parse import urlparse +from xmlrpc import client as xmlrpc_client + +from ejabberd_python3d.abc import methods +from ejabberd_python3d.abc.api import API, APIArgument, EjabberdBaseAPI +from ejabberd_python3d.core.errors import MissingArguments +from ejabberd_python3d.defaults.constants import XMLRPC_API_PROTOCOL, XMLRPC_API_SERVER, \ + XMLRPC_API_PORT + + +# noinspection PyTypeChecker +class EjabberdAPIClient(EjabberdBaseAPI): + """ + Python client for Ejabberd XML-RPC Administration API. + """ + + def __init__(self, host, username, password, server=XMLRPC_API_SERVER, port=XMLRPC_API_PORT, + protocol=XMLRPC_API_PROTOCOL, admin=True, verbose=False): + """ + Init XML-RPC server proxy. + """ + self.host = host + self.username = username + self.password = password + self.server = server + self.port = port + self.admin = admin + self.protocol = protocol + self.verbose = verbose + self._server_proxy = None + + @staticmethod + def get_instance(service_url, verbose=False): + """ + Return a EjabberdAPIClient instance based on a '12factor app' compliant service_url + + :param service_url: A connection string in the format: + ://:@(:port)/user_domain + :type service_url: str|unicode + :param verbose: + :type verbose: bool + :return: EjabberdAPIClient instance + """ + format_error = "expects service_url like https://username:password@HOST:PORT/DOMAIN" + + o = urlparse(service_url) + protocol = o.scheme + assert protocol in ('http', 'https'), format_error + + netloc_parts = o.netloc.split('@') + assert len(netloc_parts) == 2, format_error + + auth, server = netloc_parts + auth_parts = auth.split(':') + assert len(auth_parts) == 2, format_error + + username, password = auth_parts + server_parts = server.split(':') + assert len(server_parts) <= 2, format_error + + if len(server_parts) == 2: + host, port = server_parts + port = int(port) + else: + host, port = server_parts[0], XMLRPC_API_PORT + path_parts = o.path.lstrip('/').split('/') + assert len(path_parts) == 1, format_error + + server = path_parts[0] + return EjabberdAPIClient(host, username, password, server, port, protocol=protocol, verbose=verbose) + + @property + def service_url(self): + """ + Returns the FQDN to the Ejabberd server's XML-RPC endpoint + """ + # TODO: add endpoint parameter + return "{}://{}:{}".format(self.protocol, self.host, self.port) + + @property + def server_proxy(self): + """ + Returns the proxy object that is used to perform the calls to the XML-RPC endpoint + """ + if self._server_proxy is None: + self._server_proxy = xmlrpc_client.ServerProxy(self.service_url, verbose=(1 if self.verbose else 0)) + return self._server_proxy + + @property + def auth(self): + """ + Returns a dictionary containing the basic authorization info + """ + return { + 'user': self.username, + 'server': self.server, + 'password': self.password, + 'admin': self.admin + } + + def _validate_and_serialize_arguments(self, api_class, arguments): + """ + Internal method to validate and serialize arguments + :param api_class: An instance of an API class + :type api_class: API + :param arguments: A dictionary of arguments that will be passed to the method + :type arguments: dict + :return: The serialized arguments + :rtype: dict + """ + ser_args = {} + + for i in range(len(api_class.arguments)): + arg_desc = api_class.arguments[i] + assert isinstance(arg_desc, APIArgument) + + # validate argument presence + arg_name = str(arg_desc.name) + if arg_desc.required and arg_name not in arguments: + raise MissingArguments("Missing required argument '%s'" % arg_name) + + # serialize argument value + ser_args[arg_desc.name] = arg_desc.serializer_class().to_api(arguments.get(arg_name)) + + return ser_args + + def _report_method_call(self, method, arguments): + """ + Internal method to print info about a method call + + :param method: The name of the method to call + :type method: str|unicode + :param arguments: A dictionary of arguments that will be passed to the method + :type: arguments: dict + """ + if self.verbose: + print("===> %s(%s)" % (method, ', '.join(['%s=%s' % (k, v) for k, v in arguments.items()]))) + + def _call_api(self, api_class, **kwargs): + """ + Internal method used to perform api calls + + :param api_class: + :type api_class: API + :param kwargs: + :type kwargs: dict + :rtype: object + :return: Return value of the XMLRPC Method call + """ + + # validate api_class + assert issubclass(api_class, API) + + # create api instance + api = api_class() + # copy arguments + args = copy.copy(kwargs) + + # transform arguments + args = api.transform_arguments(**args) + # validate and serialize arguments + args = self._validate_and_serialize_arguments(api, args) + # retrieve method + try: + method = getattr(self.server_proxy, str(api.method)) + except xmlrpc_client.Fault as e: + # TODO: add it to logger + raise Exception(f"{e.faultString} - code: {e.faultCode}") + + # print method call with arguments + self._report_method_call(api.method, args) + + # perform call + try: + if not api.authenticate: + response = method(args) + else: + response = method(self.auth, args) + except xmlrpc_client.Fault as e: + raise Exception(f"{e.faultString} - code: {e.faultCode}") + + # validate response + api.validate_response(api, args, response) + # transform response + result = api.transform_response(api, args, response) + return result + + def echo(self, sentence): + """Echo the input back""" + return self._call_api(methods.Echo, sentence=sentence) + + def registered_users(self, host): + """ + List all registered users in the host + + :param host: The XMPP_DOMAIN + :type host: str|unicode + :rtype: Iterable + :return: A List of registered accounts usernames + """ + return self._call_api(methods.RegisteredUsers, host=host) + + def register(self, user, host, password): + """ + Register a user to the ejabberd server + + :param user: The username for the new user + :type user: str|unicode + :param host: The XMPP_DOMAIN + :type host: str|unicode + :param password: The password for the new user + :type password: str|unicode + :rtype: bool + :return: A boolean indicating if the registration has succeeded + """ + return self._call_api(methods.Register, user=user, host=host, password=password) + + def unregister(self, user, host): + """ + Unregister a user from the ejabberd server + + :param user: The username for the new user + :type user: str|unicode + :param host: The XMPP_DOMAIN + :type host: str|unicode + :rtype: bool + :return: A boolean indicating if user unregistered + """ + return self._call_api(methods.Unregister, user=user, host=host) + + def change_password(self, user, host, newpass): + """ + Change the password for a given user + + :param user: The username for the user we want to change the password for + :type user: str|unicode + :param host: The XMPP_DOMAIN + :type host: str|unicode + :param newpass: The new password + :type newpass: str|unicode + :rtype: bool + :return: A boolean indicating if the password change has succeeded + """ + return self._call_api(methods.ChangePassword, user=user, host=host, newpass=newpass) + + def check_password_hash(self, user, host, password): + """ + Checks whether a password is correct for a given user. The used hash-method is fixed to sha1. + + :param user: The username for the user we want to check the password for + :type user: str|unicode + :param host: The XMPP_DOMAIN + :type host: str|unicode + :param password: The password we want to check for the user + :type password: str|unicode + :return: A boolean indicating if the given password matches the user's password + :rtype: bool + """ + return self._call_api(methods.CheckPasswordHash, user=user, host=host, password=password) + + def add_rosteritem(self, + localuser, localhost, + user, host, + nick="", group="", subs="to"): + """ + Add an item to a user's roster + Group can be several groups separated by ; for example: "g1;g2;g3" + + :param localuser: User name + :type localuser: str + :param localhost:Server name + :type localhost: str + :param user: Contact user name + :type user: str + :param host: Contact server name + :type host: str + :param nick: Nickname, default: "" + :type nick: str + :param group: Subscription, default: "" + :type group: str + :param subs: Subscription, default: "" + :type subs: str + :return: Status code + :rtype: bool + """ + return self._call_api(methods.AddRosterItem, localuser=localuser, + localhost=localhost, + user=user, + host=host, + nick=nick, + group=group, + subs=subs) + + def backup(self, file): + """ + Store the database to backup file + :param file: Full path for the destination backup file + :type file: str + :return: raw string result + :rtype: str + """ + return self._call_api(methods.Backup, file=file) + + def ban_account(self, user, host, reason): + """ + Ban an account: kick sessions and set random password + + :param user: User name to ban + :type user: str + :param host: Server name + :type host: str + :param reason: Reason for banning user + :type reason: str + :return: Status code + :rtype: True on success, False otherwise) + """ + return self._call_api(methods.BanAccount, user=user, + host=host, + reason=reason) + + def check_account(self, user, host): + """ + Check if an account exists or not + + :param user: User name to check + :type user: str + :param host: Server to check + :type host: str + :return: Status code + :rtype: True on success, False otherwise) + """ + return self._call_api(methods.CheckAccount, user=user, host=host) + + def check_password(self, user, host, password): + """ + Check if a password is correct + + :param user: User name to check + :type user: str + :param host: Server to check + :type host: str + :param password: Password to check + :type password: str + :return: Status code + :rtype: True on success, False otherwise) + """ + return self._call_api(methods.CheckPassword, user=user, + host=host, + password=password) + + def compile(self, file): + """ + Recompile and reload Erlang source code file + :param file: Filename of erlang source file to compile + :return: True on success, False otherwise + """ + return self._call_api(methods.Compile, file=file) + + def connected_users(self): + """ + List all established sessions + + :return: List of users sessions + :rtype: list + """ + return self._call_api(methods.ConnectedUsers) + + def connected_users_info(self): + """ + List all established sessions and their information + + :return: A dict with established connections + :rtype: list + """ + return self._call_api(methods.ConnectedUsersInfo) + + def connected_users_number(self): + """ + Get the number of established sessions + + :return: Number of established sessions + :rtype: int + """ + return self._call_api(methods.ConnectedUsersNumber) + + def connected_users_vhost(self, host): + """ + Get the list of established sessions in a vhost + + :param host: Server name + :return: List of established sessions + :rtype: list + """ + return self._call_api(methods.ConnectedUsersVhost, host=host) + + def convert_to_scram(self, host): + """ + Convert the passwords of users to SCRAM + :param host: Vhost which users' passwords will be scrammed + :return: + """ + return self._call_api(methods.ConvertToSCRAM, host=host) + + def convert_to_yaml(self, in_file, out_file): + """ + Convert the input file from Erlang to YAML format + :param in_file: Full path to the original configuration file + :param out_file: Full path to final file + :return: + """ + return self._call_api(methods.ConvertToYAML, in_file=in_file, out_file=out_file) + + def create_room_with_opts(self, name, service, host, options): + """ + Create a MUC room name@service in host with given options + + :param name: Room name + :type name: str + :param service: MUC service + :type service: str + :param host: Server host + :type host: str + :param options: Room options. Example: options = [{"name": "members_only","value": "False"}, + {"name": "moderated","value": "False"}] + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.CreateRoomWithOpts, name=name, service=service, host=host, options=options) + + def create_rooms_file(self, file): + """ + Create the rooms indicated in file + :param file: Path to the text file with one room JID per line + :return: True on success, False otherwise + """ + return self._call_api(methods.CreateRoomsFile, file=file) + + def delete_expired_messages(self): + """ + Delete expired offline messages from database + + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.DeleteExpiredMessages) + + def delete_mnesia(self, host): + """ + Delete elements in Mnesia database for a given vhost + :param host: Vhost which content will be deleted in Mnesia database + :return: True on success, False otherwise + """ + return self._call_api(methods.DeleteMnesia, host=host) + + def delete_old_mam_messages(self, type, days): + """ + Delete MAM messages older than DAYS + :param type: Type of messages to delete (chat, groupchat, all) + :param days: Days to keep messages + :return: True on success, False otherwise + """ + return self._call_api(methods.DeleteOldMAMMessages, type=type, days=days) + + def delete_old_messages(self, days): + """ + Delete offline messages older than DAYS + + :param days: Last login age in days of accounts that should be removed + :type days: int + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.DeleteOldMessages, days=days) + + def delete_old_users(self, days): + """ + Delete users that didn't log in last days, or that never logged + + To protect admin accounts, configure this in your ejabberd.yml + example: access_rules: protect_old_users: - allow: admin - deny: all + :param days: Last login age in days of accounts that should be removed + :type days: int + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.DeleteOldUsers, days=days) + + def delete_old_users_vhost(self, host, days): + """ + Delete users that didn't log in last days in vhost, or that never logged + + To protect admin accounts, configure this in your ejabberd.yml + for example: access_rules: delete_old_users: - deny: admin - allow: all + :param host: + :type host: str + :param days: Last login age in days of accounts that should be removed + :type days: int + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.DeleteOldUsersVhost, + host=host, days=days) + + def delete_rosteritem(self, localuser, localhost, user, host): + """ + Delete an item from a user's roster (supports ODBC) + + :param localuser: User name + :type localuser: str + :param localhost: Server name + :type localhost: str + :param user: Contact user name + :type user: str + :param host: Contact server name + :type host: str + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.DeleteRosterItem, localuser=localuser, + localserver=localhost, + user=user, + server=host) + + def destroy_rooms_file(self, file): + """ + Destroy the rooms indicated in file. Provide one room JID per line. + :param file: Path to the text file with one room JID per line + :return: + """ + return self._call_api(methods.DestroyRoomsFile, file=file) + + def dump(self, file): + """ + Dump the database to text file + :param file: Full path for the text file + :return: + """ + return self._call_api(methods.Dump, file=file) + + # TODO def dump_table(self, file, table): + # Dump a table to text file + + # TODO def export2sql(self, host, file): + # Export virtual host information from Mnesia tables to SQL files + + # TODO def export_piefxis(self, dir): + # Export data of all users in the server to PIEFXIS files (XEP-0227) + + # TODO def export_piefxis_host(self, dir, host): + # Export data of users in a host to PIEFXIS files (XEP-0227) + + # TODO def gen_html_doc_for_commands(self, file, regexp, examples): + # Generates html documentation for ejabberd_commands + + # TODO def gen_markdown_doc_for_commands(self, file, regexp, examples): + # Generates markdown documentation for ejabberd_commands + + def muc_online_rooms(self, service="global"): + """ + List existing rooms ('global' to get all vhosts) + + :param service: MUC service, default: 'global' for all + :type service: str + :return: List of rooms + :rtype: list + """ + return self._call_api(methods.MucOnlineRooms, service=service) + + def create_room(self, name, service, host): + """ + Create a MUC room name@service in host + + :param name: Room name + :type name: str + :param service: MUC service + :type service: str + :param host: Server host + :type host: str + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.CreateRoom, name=name, service=service, host=host) + + def destroy_room(self, name, service): + """ + Destroy a MUC room + + :param name: Room name + :type name: str + :param service: MUC service + :type service: str + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.DestroyRoom, name=name, service=service) + + def get_room_options(self, name, service): + """ + Get options from a MUC room + + :param name: Room name + :param service: MUC Service + :return: List of room options dict with name and value + :rtype: list + """ + return self._call_api(methods.GetRoomOptions, name=name, service=service) + + def change_room_option(self, name, service, option, value): + """ + Change an option in a MUC room + + :param name: Room name + :param service: MUC Service + :param option: Option name + :param value: Value to assign + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.ChangeRoomOption, name=name, service=service, option=option, value=value) + + def set_room_affiliation(self, name, service, jid, affiliation): + """ + Change an affiliation in a MUC room + + :param name: Room name + :type name: str + :param service: MUC Service + :type service: str + :param jid: User JID + :type jid: str + :param affiliation: Affiliation to set + :type affiliation: str + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.GetRoomAffiliation, name=name, service=service, jid=jid, affiliation=affiliation) + + def get_room_affiliations(self, name, service): + """ + Get the list of affiliations of a MUC room + + :param name: Room name + :type name: str + :param service: MUC Service + :type service: str + :return: The list of affiliations with username, domain, affiliation and reason + :rtype: list + """ + return self._call_api(methods.GetRoomAffiliations, name=name, service=service) + + def get_cookie(self): + """ + Get the Erlang cookie of this node + + :return: Erlang cookie used for authentication by ejabberd + :rtype: str + """ + return self._call_api(methods.GetCookie) + + def get_last(self, user, host): + """ + Get last activity information + + Timestamp is UTC and XEP-0082 format, for example: 2017-02-23T22:25:28.063062Z ONLINE + :param user: User name + :param host: Server name + :return: Last activity timestamp and status + :rtype: dict + """ + return self._call_api(methods.GetLast, user=user, host=host) + + def get_loglevel(self): + """ + Get the current loglevel + + :return: Tuple with the log level number, its keyword and description + :rtype: str + """ + return self._call_api(methods.GetLogLevel) + + # TODO def get_offline_count(self): + # Get the number of unread offline messages + + # TODO def get_room_affiliations(self, name, service): + # Get the list of affiliations of a MUC room + + def get_room_occupants(self, name, service): + # Get the list of occupants of a MUC room + return self._call_api(methods.GetRoomOccupants, name=name, service=service) + + # TODO def get_room_occupants_number(self, name, service): + # Get the number of occupants of a MUC room + + # TODO def get_room_options(self, name, service): + # Get options from a MUC room + + def get_roster(self, user, server): + """ + Get roster of a local user + + :param user: User name + :param server: Server name + :return: List of subscriptions + :rtype: list + """ + return self._call_api(methods.GetRoster, user=user, server=server) + + def get_subscribers(self, name, service): + """ + List subscribers of a MUC conference + + :param name: Room name + :type name: str + :param service: MUC service + :type service: str + :return: The list of users that are subscribed to that room + :rtype: list + """ + return self._call_api(methods.GetSubscribers, name=name, service=service) + + def get_user_rooms(self, user, host): + """ + Get the list of rooms where this user is occupant + + :param user: Username + :param host: Server host + :return: List of user rooms + :rtype: list + """ + return self._call_api(methods.GetUserRooms, user=user, host=host) + + def get_vcard(self, user, host, name): + """ + Get content from a vCard field + + Some vcard field names in get/set_vcard are: + FN - Full Name + NICKNAME - Nickname + BDAY - Birthday + TITLE - Work: Position + ROLE - Work: Role + For a full list of vCard fields check XEP-0054: vcard-temp at http://www.xmpp.org/extensions/xep-0054.html + + :param user: User name + :type user: str + :param host: Server name + :type host: str + :param name: Field name + :type name: str + :return: Field content + :rtype: str + """ + return self._call_api(methods.GetVcard, user=user, + host=host, + name=name) + + def get_vcard2(self, user, host, name, subname): + """ + Get content from a vCard subfield + + Some vcard field names and subnames in get/set_vcard2 are: + N FAMILY - Family name + N GIVEN - Given name + N MIDDLE - Middle name + ADR CTRY - Address: Country + ADR LOCALITY - Address: City + TEL HOME - Telephone: Home + TEL CELL - Telephone: Cellphone + TEL WORK - Telephone: Work + TEL VOICE - Telephone: Voice + EMAIL USERID - E-Mail Address + ORG ORGNAME - Work: Company + ORG ORGUNIT - Work: Department + For a full list of vCard fields check XEP-0054: vcard-temp at http://www.xmpp.org/extensions/xep-0054.html + + :param user: User name + :type user: str + :param host: Server name + :type host: str + :param name: Field name + :type name: str + :param subname: Subfield name + :type subname: str + :return: Field content + :rtype: str + """ + return self._call_api(methods.GetVcard2, user=user, + host=host, + name=name, + subname=subname) + + def get_vcard2_multi(self, user, host, name, subname): + """ + Get multiple contents from a vCard field + + Some vcard field names and subnames in get/set_vcard2 are: + N FAMILY - Family name + N GIVEN - Given name + N MIDDLE - Middle name + ADR CTRY - Address: Country + ADR LOCALITY - Address: City + TEL HOME - Telephone: Home + TEL CELL - Telephone: Cellphone + TEL WORK - Telephone: Work + TEL VOICE - Telephone: Voice + EMAIL USERID - E-Mail Address + ORG ORGNAME - Work: Company + ORG ORGUNIT - Work: Department + For a full list of vCard fields check XEP-0054: vcard-temp at http://www.xmpp.org/extensions/xep-0054.html + + :param user: User name + :type user: str + :param host: Server name + :type host: str + :param name: Field name + :type name: str + :param subname: Subfield name + :type subname: str + :return: Field content + :rtype: str + """ + return self._call_api(methods.GetVcard2Multi, user=user, + host=host, + name=name, + subname=subname) + + # TODO def import_dir(self, file): + # Import users data from jabberd14 spool dir + + # TODO def import_file(self, file): + # Import users data from jabberd14 spool file + + # TODO def import_piefxis(self, file): + # Import users data from a PIEFXIS file (XEP-0227) + + # TODO def import_prosody(self, dir) Import data from Prosody + + def incoming_s2s_number(self): + """ + Number of incoming s2s connections on the node + + :return: s2s number + :rtype: int + """ + return self._call_api(methods.IncomingS2SNumber) + + # TODO def install_fallback(self, file): + # Install the database from a fallback file + + # TODO def join_cluster(self, node): + # Join this node into the cluster handled by Node + + def kick_session(self, user, host, resource, reason): + """ + Kick a user session + """ + return self._call_api(methods.KickSession, user=user, + host=host, + resource=resource, + reason=reason) + + def kick_user(self, user, host): + """ + Disconnect user's active sessions + + :param user: User name + :type user: str + :param host: Server name + :type host: str + :return: Number of resources that were kicked + :rtype: int + """ + return self._call_api(methods.KickUser, user=user, host=host) + + # TODO def leave_cluster(self, node): + # Remove node handled by Node from the cluster + + def list_cluster(self): + """ + List nodes that are part of the cluster handled by Node + + :return: List of clusters + :rtype: list + """ + return self._call_api(methods.ListCluster) + + # TODO def load(self, file): + # Restore the database from text file + + # TODO def mnesia_change_nodename(self, + # oldnodename, + # newnodename, + # oldbackup, + # newbackup): + # Change the erlang node name in a backup file + + # TODO def module_check(self, module): + + # TODO def module_install(self, module): + + # TODO def module_uninstall(self, module): + + # TODO def module_upgrade(self, module): + + def modules_available(self): + """ + List the contributed modules available to install + + :return: List of dict with module name and description + :rtype: list + """ + return self._call_api(methods.ModulesAvailable) + + def modules_installed(self): + """ + List the contributed modules already installed + + :return: List of dict with module name and description + :rtype: list + """ + return self._call_api(methods.ModulesInstalled) + + # TODO def modules_update_specs(self): + + # TODO def muc_online_rooms(self, host): + # List existing rooms (‘global’ to get all vhosts) + + # TODO def muc_unregister_nick(self, nick): + # Unregister the nick in the MUC service + + def num_resources(self, user, host): + """ + Get the number of resources of a user + + :param user: User name + :type user: str + :param host: Server name + :type host: str + :return: Number of active resources for a user + :rtype: int + """ + return self._call_api(methods.NumResources, user=user, host=host) + + def outgoing_s2s_number(self): + """ + Number of outgoing s2s connections on the node + + :return: Number of outgoing s2s connections + :rtype: int + """ + return self._call_api(methods.OutgoingS2SNumber) + + # TODO def privacy_set(self, user, host, xmlquery): + # Send a IQ set privacy stanza for a local account + + # TODO def private_get(self, user, host, element, ns): + # Get some information from a user private storage + + # TODO def private_set(self, user, host, element): + # Set to the user private storage + + def process_rosteritems(self, action, subs, asks, users, contacts): + """ + List/delete rosteritems that match filter + + :param action: + :type action: str + :param subs: + :type subs: str + :param asks: + :type asks: str + :param users: + :type users: str + :param contacts: + :type contacts: str + :return: + """ + return self._call_api(methods.ProcessRosterItems, action=action, + subs=subs, + asks=asks, + users=users, + contacts=contacts) + + def push_alltoall(self, host, group): + """ + Add all the users to all the users of Host in Group + + :param host: Server name + :param group: Group name + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.PushAllToAll, host=host, group=group) + + # TODO def push_roster(self, file, user, host): + # Push template roster from file to a user + + # TODO def push_roster_all(self, file): + # Push template roster from file to all those users + + def registered_vhosts(self): + """ + List all registered vhosts in SERVER + + :return: List of available vhosts + :rtype: list + """ + return self._call_api(methods.RegisteredVhosts) + + def reload_config(self): + """ + Reload config file in memory + (only affects ACL and Access) + + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.ReloadConfig) + + def reopen_log(self): + """ + Reopen the log files + + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.ReopenLog) + + def resource_num(self, user, host, num): + """ + Resource string of a session number + + :param user: User name + :type user: str + :param host: Server name + :type host: str + :param num: ID of resource to return + :type num: int + :return: Name of user resource + :rtype: str + """ + return self._call_api(methods.ResourceNum, user=user, + host=host, + num=num) + + def restart(self): + """ + Restart ejabberd gracefully + + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.Restart) + + # TODO def restore(self, file): + # Restore the database from backup file + + # TODO def rooms_unused_destroy(self, host, days): + # Destroy the rooms that are unused for many days in host + + # TODO def rooms_unused_list(self, host, days): + # List the rooms that are unused for many days in host + + # TODO def rotate_log(self): + # Rotate the log files + + # TODO def send_direct_invitation(self, + # name, + # service, + # password, + # reason, + # users): + # Send a direct invitation to several destinations + + def send_message(self, type, from_jid, to, body, subject=""): + """ + Send a message to a local or remote bare of full JID + + When sending a groupchat message to a MUC room, FROM must be the full JID of a room occupant, + or the bare JID of a MUC service admin, or the bare JID of a MUC/Sub subscribed user. + + :param type: Message type: normal, chat, headline, groupchat + :type type: str + :param from_jid: Sender JID + :type from_jid: str + :param to: Receiver JID + :type to: str + :param body: Body + :type body: str + :param subject: Subject, or empty string + :type subject: str + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.SendMessage, type=type, + from_jid=from_jid, to=to, + subject=subject, + body=body) + + # TODO def send_stanza(self, from, to, stanza): + # Send a stanza; provide From JID and valid To JID + + def send_stanza_c2s(self, user, host, resource, stanza): + """ + Send a stanza as if sent from a c2s session + + :param user: Username + :param host: Server name + :param resource: Resource + :param stanza: Stanza + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.SendStanzaC2S, user=user, + host=host, + resource=resource, + stanza=stanza) + + def set_last(self, user, host, timestamp, status): + """ + Set last activity information + Timestamp is the seconds since 1970-01-01 00:00:00 UTC, for example: date +%s + + :param user: User name + :param host: Server name + :param timestamp: Number of seconds since epoch + :param status: Status message + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.SetLast, user=user, + host=host, + timestamp=timestamp, + status=status) + + def set_loglevel(self, loglevel): + """ + Set the loglevel + + :param loglevel: Desired logging level: none | emergency | alert | critical | error + | warning | notice | info | debug + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + try: + return self._call_api(methods.SetLogLevel, loglevel=loglevel) + except xmlrpc_client.Fault as e: + msg = 'set_loglevel is NOT available in your version of ejabberd' + raise Exception('{}\n{} - code: {}\n '.format(msg, e.faultString, e.faultCode)) + + def set_master(self, nodename): + """ + Set master node of the clustered Mnesia tables + If you provide as nodename "self", this node will be set as its own master. + + :param nodename: Name of the erlang node that will be considered master of this node + :type nodename: str + :return: Raw result string + :rtype: bool + """ + return self._call_api(methods.SetMaster, nodename=nodename) + + def set_nickname(self, user, host, nickname): + """ + Set nickname in a user's vCard + + :param user: Username + :type user: str + :param host: Server name + :type host: str + :param nickname: Nickname + :type nickname: str + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.SetNickname, user=user, + host=host, + nickname=nickname) + + def set_presence(self, user, host, resource, type, show, status, priority): + """ + Set presence of a session + + :param user: Username + :type user: str + :param host: Server name + :type host: str + :param resource: Resource + :type resource: str + :param type: Type: available, error, probe.. + :type type: str + :param show: Show: away, chat, dnd, xa + :type show: str + :param status: Status text + :type status: str + :param priority: Priority, provide this value as an integer + :type priority: str + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.SetPresence, user=user, + host=host, + resource=resource, + type=type, + show=show, + status=status, + priority=priority) + + # TODO def set_room_affiliation(self, name, service, jid, affiliation): + # Change an affiliation in a MUC room + + def set_vcard(self, user, host, name, content): + """ + Set content in a vCard field + + Some vcard field names in get/set_vcard are: + FN - Full Name + NICKNAME - Nickname + BDAY - Birthday + TITLE - Work: Position + ROLE - Work: Role + For a full list of vCard fields check XEP-0054: vcard-temp at http://www.xmpp.org/extensions/xep-0054.html + + :param user: User name + :param host: Server name + :param name: Field name + :param content: Value + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.SetVcard, user=user, + host=host, + name=name, + content=content) + + def set_vcard2(self, user, host, name, subname, content): + """ + Set content in a vCard subfield + + :param user: User name + :param host: Server name + :param name: Field name + :param subname: Subfield name + :param content: Value + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.SetVcard2, user=user, + host=host, + name=name, + subname=subname, + content=content) + + def set_vcard2_multi(self, user, host, name, subname, contents): + """ + Set multiple contents in a vCard subfield + + :param user: User name + :type user: str + :param host: Server name + :type host: str + :param name: Field name + :type name: str + :param subname: Subfield name + :type subname: str + :param contents: Contents + :type contents: dict + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.SetVcardMulti, user=user, + host=host, + name=name, + subname=subname, + contents=contents) + + def srg_create(self, group, host, name, description, display): + """ + Create a Shared Roster Group + + If you want to specify several group identifiers in the Display argument, + put \ " around the argument and separate the identifiers with \ \ n + For example: ejabberdctl srg_create group3 myserver.com name desc \"group1\ngroup2\" + + :param group: Group identifier + :type group: str + :param host: Group server name + :type host: str + :param name: Group name + :type name: str + :param description: Group description + :type description: str + :param display: Groups to display + :type display: str + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.SrgCreate, group=group, + host=host, + name=name, + description=description, + display=display) + + def srg_delete(self, group, host): + """ + Delete a Shared Roster Group + + :param group: Group identifier + :type group: str + :param host: Group server name + :type host: str + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.SrgDelete, group=group, host=host) + + def srg_get_info(self, group, host): + """ + Get info of a Shared Roster Group + + :param group: Group identifier + :type group: str + :param host: Group server name + :type host: str + :return: List of group informations, as key and value + :rtype: list + """ + return self._call_api(methods.SrgGetInfo, group=group, host=host) + + def srg_get_members(self, group, host): + """ + Get members of a Shared Roster Group + + :param group: Group identifier + :type group: str + :param host: Group server name + :type host: str + :return: List of group identifiers + :rtype: list + """ + return self._call_api(methods.SrgGetMembers, group=group, host=host) + + def srg_list(self, host): + """ + List the Shared Roster Groups in Host + + :param host: Server name + :type host: str + :return: List of group identifiers + :rtype: list + """ + return self._call_api(methods.SrgList, host=host) + + def srg_user_add(self, user, host, group, grouphost): + """ + Add the JID user@host to the Shared Roster Group + + :param user: User name + :type user: str + :param host: User server name + :type host: str + :param group: Group identifier + :type group: str + :param grouphost: Group server name + :type grouphost: str + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.SrgUserAdd, user=user, + host=host, + group=group, + grouphost=grouphost) + + def srg_user_del(self, user, host, group, grouphost): + """ + Delete this JID user@host from the Shared Roster Group + + :param user: User name + :type user: str + :param host: User server name + :type host: str + :param group: Group identifier + :type group: str + :param grouphost: Group server name + :type grouphost: str + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.SrgUserDel, user=user, + host=host, + group=group, + grouphost=grouphost) + + def stats(self, name): + """ + Get statistical value + + :param name: Statistic name: + * ``registeredusers`` + * ``onlineusers`` + * ``onlineusersnode`` + * ``uptimeseconds`` + * ``processes`` + :type name: str + :return: Integer statistic value + """ + return self._call_api(methods.Stats, name=name) + + def stats_host(self, name, host): + """ + Get statistical value + + :param name: Statistic name: + * ``registeredusers`` + * ``onlineusers`` + + :type name: str + :param host: Server JID + :type host: str + :return: Integer statistic value + :rtype: int + """ + return self._call_api(methods.StatsHost, name=name, host=host) + + def status(self): + """ + Get status of the ejabberd server + + :return: Raw result string + :rtype: str + """ + return self._call_api(methods.Status) + + def status_list(self, status): + """ + List of logged users with this status + + :param status: Status type to check + :type status: str + :return: List of users with this `status` + :rtype: list + """ + return self._call_api(methods.StatusList, status=status) + + def status_list_host(self, host, status): + """ + List of users logged in host with their statuses + + :param host: Server name + :type host: str + :param status: Status type to check + :type status: str + :return: List of users with this `status` in `host` + :rtype: list + """ + return self._call_api(methods.StatusListHost, host=host, status=status) + + def status_num(self, status): + """ + Number of logged users with this status + + :param status: Status type to check + :type status: str + :return: Number of connected sessions with given status type + :rtype: list + """ + return self._call_api(methods.StatusNum, status=status) + + def status_num_host(self, host, status): + """ + Number of logged users with this status in host + + :param host: Server name + :type host: str + :param status: Status type to check + :type status: str + :return: Number of connected sessions with given status type + :rtype: list + """ + return self._call_api(methods.StatusNumHost, host=host, status=status) + + def stop(self): + """ + Stop ejabberd gracefully + + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.Stop) + + def stop_kindly(self, delay, announcement): + """ + Inform users and rooms, wait, and stop the server + Provide the delay in seconds, and the announcement quoted, + for example: ejabberdctl stop_kindly 60 \"The server will stop in one minute.\" + + :param delay: Seconds to wait + :param announcement: Announcement to send, with quotes + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.StopKindly, + delay=delay, announcement=announcement) + + def subscribe_room(self, user, nick, room, nodes=None): + """ + Subscribe to a MUC conference + + :param user: User JID + :type user: str + :param nick: A user's nick + :type nick: str + :param room: The room to subscribe + :type room: str + :param nodes: List of nodes + :type nodes: list + :return: The list of nodes that has subscribed + :rtype: list + """ + return self._call_api(methods.SubscribeRoom, user=user, nick=nick, room=room, nodes=nodes) + + def unsubscribe_room(self, user, room): + """ + Unsubscribe from a MUC conference + + :param user: User JID + :type user: str + :param room: The room to subscribe + :type room: str + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.UnSubscribeRoom, user=user, room=room) + + def update(self, module): + """ + Update the given module, or use the keyword: all + + :param module: Module to update + :return: Raw result string + :rtype: str + """ + return self._call_api(methods.Update, module=module) + + def update_list(self): + """ + List modified modules that can be updated + + :return: List of modules + :rtype: list + """ + return self._call_api(methods.UpdateList) + + def update_sql(self): + """ + Convert SQL DB to the new format + + :return: Status code (True if success, False otherwise) + :rtype: bool + """ + return self._call_api(methods.UpdateSql) + + def user_resources(self, user, host): + """ + List user's connected resources + + :param user: User name + :type user: str + :param host: Server name + :type host: str + :return: List of resources + :rtype: list + """ + return self._call_api(methods.UserResources, user=user, host=host) + + def user_sessions_info(self, user, host): + """ + Get information about all sessions of a user + + :param user: User name + :type user: str + :param host: Server name + :type host: str + :return: A List with user sessions + :rtype: list + """ + return self._call_api(methods.UserSessionInfo, user=user, host=host) diff --git a/ejabberd_python3d/core/__init__.py b/ejabberd_python3d/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ejabberd_python3d/core/errors.py b/ejabberd_python3d/core/errors.py new file mode 100644 index 0000000..07d379e --- /dev/null +++ b/ejabberd_python3d/core/errors.py @@ -0,0 +1,23 @@ +class ConnectionError(Exception): + """Error when connecting to API""" + pass + + +class AccessDeniedError(Exception): + """Access denied, account unprivileged""" + pass + + +class MissingArguments(ValueError): + """Missing arguments in call""" + pass + + +class BadArgument(ValueError): + """Wrong Argument""" + pass + + +class UserAlreadyRegisteredError(Exception): + """User already registered""" + pass diff --git a/ejabberd_python3d/core/utils.py b/ejabberd_python3d/core/utils.py new file mode 100644 index 0000000..7059fd2 --- /dev/null +++ b/ejabberd_python3d/core/utils.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import hashlib +from builtins import range, int + +from six import b + + +def _format_digest(hexdigest): # pragma: no cover + parts = [] + hexdigest_size = int(len(hexdigest) / 2) + for i in range(hexdigest_size): + part = hexdigest[i * 2:(i * 2) + 2] + if part == '00': + part = '' + elif part.startswith('0'): + part = part[1:] + parts.append(part) + return (''.join(parts)).upper() + + +def _format_password_hash(password, hash_method): # pragma: no cover + hash_method.update(b(password)) + return _format_digest(hash_method.hexdigest()) + + +def format_password_hash_sha(password): + return _format_password_hash(password, hashlib.sha1()) + + +def format_password_hash_md5(password): + return _format_password_hash(password, hashlib.md5()) diff --git a/ejabberd_python3d/defaults/__init__.py b/ejabberd_python3d/defaults/__init__.py new file mode 100644 index 0000000..fbc1372 --- /dev/null +++ b/ejabberd_python3d/defaults/__init__.py @@ -0,0 +1,15 @@ +from ejabberd_python3d.serializers import StringSerializer + +from .enums import LogLevelOptions + +loglevel_options_serializers = { + LogLevelOptions.none: StringSerializer, + LogLevelOptions.alert: StringSerializer, + LogLevelOptions.emergency: StringSerializer, + LogLevelOptions.error: StringSerializer, + LogLevelOptions.critical: StringSerializer, + LogLevelOptions.debug: StringSerializer, + LogLevelOptions.info: StringSerializer, + LogLevelOptions.notice: StringSerializer, + LogLevelOptions.warning: StringSerializer, +} diff --git a/ejabberd_python3d/defaults/arguments.py b/ejabberd_python3d/defaults/arguments.py new file mode 100644 index 0000000..4a1aef9 --- /dev/null +++ b/ejabberd_python3d/defaults/arguments.py @@ -0,0 +1,33 @@ +from __future__ import unicode_literals + +from ..abc.api import APIArgument +from ..serializers import StringSerializer, IntegerSerializer, PositiveIntegerSerializer, BooleanSerializer, \ + LogLevelSerializer, ListSerializer, GenericSerializer + + +class GenericArgument(APIArgument): + serializer_class = GenericSerializer + + +class StringArgument(APIArgument): + serializer_class = StringSerializer + + +class IntegerArgument(APIArgument): + serializer_class = IntegerSerializer + + +class PositiveIntegerArgument(APIArgument): + serializer_class = PositiveIntegerSerializer + + +class BooleanArgument(APIArgument): + serializer_class = BooleanSerializer + + +class LogLevelArgument(APIArgument): + serializer_class = LogLevelSerializer + + +class ListArgument(APIArgument): + serializer_class = ListSerializer diff --git a/ejabberd_python3d/defaults/constants.py b/ejabberd_python3d/defaults/constants.py new file mode 100644 index 0000000..299b2f5 --- /dev/null +++ b/ejabberd_python3d/defaults/constants.py @@ -0,0 +1,3 @@ +XMLRPC_API_PROTOCOL = 'http' +XMLRPC_API_PORT = 4560 +XMLRPC_API_SERVER = 'localhost' diff --git a/ejabberd_python3d/defaults/enums.py b/ejabberd_python3d/defaults/enums.py new file mode 100644 index 0000000..fce9021 --- /dev/null +++ b/ejabberd_python3d/defaults/enums.py @@ -0,0 +1,13 @@ +from ejabberd_python3d.abc.api import Enum + + +class LogLevelOptions(Enum): + none = 1 + emergency = 2 + alert = 3 + critical = 4 + error = 5 + warning = 6 + notice = 7 + info = 8 + debug = 9 diff --git a/ejabberd_python3d/ejabberd_auth/__init__.py b/ejabberd_python3d/ejabberd_auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ejabberd_python3d/ejabberd_auth/management/__init__.py b/ejabberd_python3d/ejabberd_auth/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ejabberd_python3d/ejabberd_auth/management/commands/__init__.py b/ejabberd_python3d/ejabberd_auth/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ejabberd_python3d/ejabberd_auth/management/commands/ejabberd_auth.py b/ejabberd_python3d/ejabberd_auth/management/commands/ejabberd_auth.py new file mode 100644 index 0000000..cf63c31 --- /dev/null +++ b/ejabberd_python3d/ejabberd_auth/management/commands/ejabberd_auth.py @@ -0,0 +1,99 @@ +import logging +import struct +import sys +from django.contrib.auth import authenticate, get_user_model +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + + + +class Command(BaseCommand): + logger = logging.getLogger(__name__) + + def from_ejabberd(self, encoding="utf-8"): + """ + Reads data from stdin as passed by eJabberd + """ + input_length = sys.stdin.read(2).encode(encoding) + (size,) = struct.unpack(">h", input_length) + return sys.stdin.read(size).split(":") + + def to_ejabberd(self, answer=False): + """ + Converts the response into eJabberd format + """ + b = struct.pack('>hh', + 2, + 1 if answer else 0) + self.logger.debug("To jabber: %s" % b) + sys.stdout.write(b.decode("utf-8")) + sys.stdout.flush() + + def auth(self, username=None, server="localhost", password=None): + self.logger.debug("Authenticating %s with password %s on server %s" % (username, password, server)) + #TODO: would be nice if this could take server into account + user = authenticate(username=username, password=password) + return user and user.is_active + + def isuser(self, username=None, server="localhost"): + """ + Checks if the user exists and is active + """ + self.logger.debug("Validating %s on server %s" % (username, server)) + #TODO: would be nice if this could take server into account + try: + user = get_user_model().objects.get(username=username) + if user.is_active: + return True + else: + self.logger.warning("User %s is disabled" % username) + return False + except User.DoesNotExist: + return False + + def setpass(self, username=None, server="localhost", password=None): + """ + Handles password change + """ + self.logger.debug("Changing password to %s with new password %s on server %s" % (username, password, server)) + #TODO: would be nice if this could take server into account + try: + user = get_user_model().objects.get(username=username) + user.set_password(password) + user.save() + return True + except User.DoesNotExist: + return False + + def handle(self, *args, **options): + """ + Gathers parameters from eJabberd and executes authentication + against django backend + """ + #logging.basicConfig( + # level="DEBUG", + # format='%(asctime)s %(levelname)s %(message)s', + # filename="/usr/local/var/log/ejabberd/django-bridge.log", + # filemode='a') + + self.logger.debug("Starting serving authentication requests for eJabberd") + print("Starting serving authentication requests for eJabberd") + success = False + try: + while True: + data = self.from_ejabberd() + self.logger.debug("Command is %s" % data[0]) + print("Command is %s" % data[0]) + if data[0] == "auth": + success = self.auth(data[1], data[2], data[3]) + elif data[0] == "isuser": + success = self.isuser(data[1], data[2]) + elif data[0] == "setpass": + success = self.setpass(data[1], data[2], data[3]) + self.to_ejabberd(success) + if not options.get("run_forever", True): + break + except Exception as e: + self.logger.error("An error has occurred during eJabberd external authentication: %s" % e) + print("An error has occurred during eJabberd external authentication: %s" % e) + self.to_ejabberd(success) diff --git a/ejabberd_python3d/muc/__init__.py b/ejabberd_python3d/muc/__init__.py new file mode 100644 index 0000000..ad1e715 --- /dev/null +++ b/ejabberd_python3d/muc/__init__.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from ..serializers import BooleanSerializer, StringSerializer, PositiveIntegerSerializer +from .serializers import AllowVisitorPrivateMessageSerializer +from .enums import MUCRoomOption, MUCNodes + +muc_room_options_serializers = { + MUCRoomOption.allow_change_subj: BooleanSerializer, + MUCRoomOption.allow_private_messages: BooleanSerializer, + MUCRoomOption.allow_private_messages_from_visitors: AllowVisitorPrivateMessageSerializer, + MUCRoomOption.allow_query_users: BooleanSerializer, + MUCRoomOption.allow_user_invites: BooleanSerializer, + MUCRoomOption.allow_visitor_nickchange: BooleanSerializer, + MUCRoomOption.allow_visitor_status: BooleanSerializer, + MUCRoomOption.anonymous: BooleanSerializer, + MUCRoomOption.captcha_protected: BooleanSerializer, + MUCRoomOption.logging: BooleanSerializer, + MUCRoomOption.max_users: PositiveIntegerSerializer, + MUCRoomOption.members_by_default: BooleanSerializer, + MUCRoomOption.members_only: BooleanSerializer, + MUCRoomOption.moderated: BooleanSerializer, + MUCRoomOption.password: StringSerializer, + MUCRoomOption.password_protected: BooleanSerializer, + MUCRoomOption.persistent: BooleanSerializer, + MUCRoomOption.public: BooleanSerializer, + MUCRoomOption.public_list: BooleanSerializer, + MUCRoomOption.title: StringSerializer +} + +mucsub_room_nodes = { + MUCNodes.mucsub_affiliations: StringSerializer, + MUCNodes.mucsub_messages: StringSerializer, + MUCNodes.mucsub_presence: StringSerializer, + MUCNodes.mucsub_config: StringSerializer, + MUCNodes.mucsub_subject: StringSerializer, + MUCNodes.mucsub_subscribers: StringSerializer, + MUCNodes.mucsub_system: StringSerializer +} diff --git a/ejabberd_python3d/muc/arguments.py b/ejabberd_python3d/muc/arguments.py new file mode 100644 index 0000000..dabd1c5 --- /dev/null +++ b/ejabberd_python3d/muc/arguments.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from ..abc.api import APIArgument +from .serializers import MUCRoomOptionSerializer, AffiliationSerializer, MUCNodesSerializer + + +class MUCRoomArgument(APIArgument): + serializer_class = MUCRoomOptionSerializer + + +class AffiliationArgument(APIArgument): + serializer_class = AffiliationSerializer + + +class MUCNodesArgument(APIArgument): + serializer_class = MUCNodesSerializer diff --git a/ejabberd_python3d/muc/enums.py b/ejabberd_python3d/muc/enums.py new file mode 100644 index 0000000..e5e27b9 --- /dev/null +++ b/ejabberd_python3d/muc/enums.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from ejabberd_python3d.abc.api import Enum + + +class MUCRoomOption(Enum): + allow_change_subj = 'allow_change_subj' + allow_private_messages = 'allow_private_messages' + allow_private_messages_from_visitors = 3 + allow_query_users = 4 + allow_user_invites = 5 + allow_visitor_nickchange = 6 + allow_visitor_status = 7 + anonymous = 8 + captcha_protected = 9 + logging = 10 + max_users = 11 + members_by_default = 12 + members_only = 13 + moderated = 14 + password = 15 + password_protected = 16 + persistent = 17 + public = 18 + public_list = 19 + title = 20 + + +class AllowVisitorPrivateMessage(Enum): + anyone = 1 + moderators = 2 + nobody = 3 + + +class Affiliation(Enum): + outcast = 1 + none = 2 + member = 3 + admin = 4 + owner = 5 + + +class MUCNodes(Enum): + mucsub_presence = "urn:xmpp:mucsub:nodes:presence" + mucsub_messages = "urn:xmpp:mucsub:nodes:messages" + mucsub_affiliations = "urn:xmpp:mucsub:nodes:affiliations" + mucsub_subscribers = "urn:xmpp:mucsub:nodes:subscribers" + mucsub_config = "urn:xmpp:mucsub:nodes:config" + mucsub_subject = "urn:xmpp:mucsub:nodes:subject" + mucsub_system = "urn:xmpp:mucsub:nodes:system" diff --git a/ejabberd_python3d/muc/serializers.py b/ejabberd_python3d/muc/serializers.py new file mode 100644 index 0000000..e77175a --- /dev/null +++ b/ejabberd_python3d/muc/serializers.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# from __future__ import unicode_literals +from six import string_types + +from ..serializers import EnumSerializer +from .enums import MUCRoomOption, AllowVisitorPrivateMessage, Affiliation, MUCNodes + + +class MUCRoomOptionSerializer(EnumSerializer): + enum_class = MUCRoomOption + + +class AllowVisitorPrivateMessageSerializer(EnumSerializer): + enum_class = AllowVisitorPrivateMessage + + +class AffiliationSerializer(EnumSerializer): + enum_class = Affiliation + + +class MUCNodesSerializer(EnumSerializer): + enum_class = MUCNodes + + def to_api(self, value): + if isinstance(value, self.enum_class): + return value.value + if isinstance(value, string_types): + return value + elif isinstance(value, int): + return self.enum_class.get_by_value(value).name + raise ValueError("Invalid value for enum %s: %s" % (self.enum_class, value)) + + def to_builtin(self, value): + if not isinstance(value, string_types): + raise ValueError("Expects str or unicode , but got {}".format(type(value))) + res = self.enum_class.get_by_name(value) + if res is None: + raise ValueError("Expects enum value for {}, but got {}".format(self.enum_class, type(value))) diff --git a/ejabberd_python3d/serializers.py b/ejabberd_python3d/serializers.py new file mode 100644 index 0000000..96c751b --- /dev/null +++ b/ejabberd_python3d/serializers.py @@ -0,0 +1,106 @@ +from abc import abstractmethod + +from ejabberd_python3d.abc.api import Enum, APIArgumentSerializer +from ejabberd_python3d.defaults.enums import LogLevelOptions +from six import string_types + + +class GenericSerializer(APIArgumentSerializer): + def to_api(self, value): + return value + + def to_builtin(self, value): + return value + + +class StringSerializer(APIArgumentSerializer): + def to_api(self, value): + if not isinstance(value, string_types): + raise ValueError("Expects str or unicode, but got {}".format(type(value))) + return value + + def to_builtin(self, value): + if not isinstance(value, string_types): + raise ValueError("Expects str or unicode, but got {}".format(type(value))) + return value + + +class IntegerSerializer(APIArgumentSerializer): + def to_api(self, value): + if not isinstance(value, int): + raise ValueError("Expects int or long, but got {}".format(type(value))) + return str(value) + + def to_builtin(self, value): + return int(value) + + +class PositiveIntegerSerializer(IntegerSerializer): + def to_api(self, value): + if not isinstance(value, int) or value < 0: + raise ValueError("Expects positive int or long, but got {}".format(type(value))) + return super(PositiveIntegerSerializer, self).to_api(value) + + def to_builtin(self, value): + res = super(PositiveIntegerSerializer, self).to_builtin(value) + if res < 0: + raise ValueError("Expects positive int or long, but got {}".format(type(value))) + return res + + +class BooleanSerializer(APIArgumentSerializer): + def to_api(self, value): + if not isinstance(value, bool): + raise ValueError("Expects boolean") + return 'true' if value else 'false' + + def to_builtin(self, value): + if value not in ('true', 'false'): + raise ValueError("Expects true|false, but got {}".format(type(value))) + return value == 'true' + + +class EnumSerializer(StringSerializer): + @abstractmethod + def enum_class(self): + pass + + def to_api(self, value): + assert issubclass(self.enum_class, Enum) + if isinstance(value, self.enum_class): + return value.name + elif isinstance(value, string_types): + return value + elif isinstance(value, int): + return self.enum_class.get_by_value(value).name + raise ValueError("Invalid value for enum %s: %s" % (self.enum_class, value)) + + def to_builtin(self, value): + assert issubclass(self.enum_class, Enum) + if not isinstance(value, string_types): + raise ValueError("Expects str or unicode , but got {}".format(type(value))) + res = self.enum_class.get_by_name(value) + if res is None: + raise ValueError("Expects enum value for {}, but got {}".format(self.enum_class, type(value))) + + +class ListSerializer(APIArgumentSerializer): + def to_api(self, value): + if not isinstance(value, list): + raise ValueError("Expects list, but got {}".format(type(value))) + vl = "" + for v in range(len(value)): + if v == 0: + vl += value[v] + else: + vl += "," + value[v] + return vl + + def to_builtin(self, value): + if not isinstance(value, list): + raise ValueError("Expects list, but got {}".format(type(value))) + return value + + +class LogLevelSerializer(EnumSerializer): + enum_class = LogLevelOptions diff --git a/ejabberd_python3d/testing.py b/ejabberd_python3d/testing.py new file mode 100644 index 0000000..afd52b3 --- /dev/null +++ b/ejabberd_python3d/testing.py @@ -0,0 +1,14 @@ +from ejabberd_python3d import client + +conn = client.EjabberdAPIClient("localhost", "dedaldino", "Dedaldino18", "localhost") +groups = conn.muc_online_rooms() + +for g in groups: + # TODO: add get_room_occupants in ejabberd_python3d lib + room_name = g['room'].split('@')[0] + subs = [u.split('@')[0] for u in + conn.get_subscribers(room_name, 'groupchat.localhost')] + print("subs: ", subs) + options = conn.get_room_options(room_name, 'groupchat.localhost') + print("muc options: ", options) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6fa36df --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +coverage>=5.3 +Django>=3.0 +requests>=2.24.0 +urllib3>=1.25.11 diff --git a/scripts/extract_todos.py b/scripts/extract_todos.py new file mode 100644 index 0000000..f2311ae --- /dev/null +++ b/scripts/extract_todos.py @@ -0,0 +1,55 @@ +import os + +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MAIN_PACKAGE_DIRECTORY = os.path.join(ROOT_DIR, 'ejabberd_python3d') +WRITE_FILE_NAME = os.path.join(ROOT_DIR, "TODOS.md") +DEBUG = False + + +def create_file(): + f = open(WRITE_FILE_NAME, 'w') + return f + + +def report_callee(filename, filewrite, line): + print("==> Writing Todos in {} from: {}\nTODO ==> {}".format(filewrite, filename, line)) + + +skip_dirs = ['build', 'dist', '__pycache__', '.git', '.idea', 'ejabberd_python3d.egg-info'] + +file2write = create_file() + + +def extract_todos(dir): + if os.path.isdir(dir): + for root, dirs, files in os.walk((dir if os.path.isabs(dir) else os.path.abspath(dir))): + for d in skip_dirs: + if d in dirs: + dirs.remove(d) + _extract_todos(files, root) + else: + raise ValueError("Enter a valid dir name") + + +def _extract_todos(files, root): + if len(files) == 0: + return + try: + file = files.pop() + _extract_todos2(file, root) + return _extract_todos(files, root) + except (IndexError, PermissionError,): + pass + + +def _extract_todos2(file, root, ): + with open(os.path.join(root, file), 'r') as fr: + for line in fr: + if "# TODO" in line: + file2write.writelines(line) + if DEBUG: + report_callee(file, file2write.name, line) + + +if __name__ == '__main__': + extract_todos(MAIN_PACKAGE_DIRECTORY) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a12f9b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,41 @@ +[bdist_wheel] +universal = 1 + +[aliases] +release = register clean --all sdist bdist_wheel +publish = register clean --all sdist bdist_wheel upload + +[flake8] +max-line-length = 160 +exclude = tests/*,*/migrations/*,*/south_migrations/* + +[pytest] +norecursedirs = + .git + .tox + .env + .eggs + venv + dist + build + south_migrations + migrations +python_files = + test_*.py + *_test.py + tests.py +addopts = + -rxEfs + --strict + --ignore=docs/conf.py + --ignore=setup.py + --ignore=ci + --doctest-modules + --doctest-glob=\*.rst + --tb=short + +[isort] +force_single_line=True +line_length=120 +known_first_party=ejabberd_python3d +default_section=THIRDPARTY diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5b0d68b --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +from glob import glob +from os.path import basename, splitext + +import setuptools +from pip._internal.req import parse_requirements + +__version__ = "0.2.3" + +with open("README.md", "r") as fh: + long_description = fh.read() + +# parse_requirements() returns generator of pip.req.InstallRequirement objects +requirements = [str(i.requirement) for i in parse_requirements('./requirements.txt', session=False)] + +setuptools.setup( + name="ejabberd_python3d", + version=__version__, + author="Dedaldino Antonio", + author_email="dedaldinoantonio7@gmail.com", + description="A library to make XML-RPC calls to ejabberd", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/Dedaldino3D/ejabberd-python3d", + license="MIT", + packages=setuptools.find_packages(exclude=("scripts",)), + py_modules=[splitext(basename(path))[0] for path in glob('*.py')], + include_package_data=True, + zip_safe=False, + classifiers=[ + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + ], + python_requires='>=3.6', keywords=[ + 'python', 'mix', 'django-ejabberd', 'django-auth', 'ejabberd', 'xmlrpc', 'api', 'client', 'xmpp', 'chat', 'muc' + + ], + install_requires=requirements, + extras_require={ + } +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29