diff --git a/docs/api.rst b/docs/api.rst index afebc78..3c9202a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -31,14 +31,3 @@ A good tip is to write ``from fbchat.models import *`` at the start of your sour .. automodule:: fbchat.models :members: :undoc-members: - - -.. _api_utils: - -Utils ------ - -These functions and values are used internally by fbchat, and are subject to change. Do **NOT** rely on these to be backwards compatible! - -.. automodule:: fbchat.utils - :members: diff --git a/fbchat/__init__.py b/fbchat/__init__.py index b7aaf85..1a1ba87 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -7,11 +7,10 @@ from __future__ import unicode_literals # These imports are far too general, but they're needed for backwards compatbility. -from .utils import * from .models import * -from ._graphql import graphql_queries_to_json, graphql_response_to_json, GraphQL from ._client import Client +from ._util import log # TODO: Remove this (from examples too) __title__ = "fbchat" __version__ = "1.6.5" diff --git a/fbchat/_client.py b/fbchat/_client.py index 69358bd..b1cc69a 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -59,7 +59,7 @@ class Client(object): :param email: Facebook `email`, `id` or `phone number` :param password: Facebook account password - :param user_agent: Custom user agent to use when sending requests. If `None`, user agent will be chosen from a premade list (see :any:`utils.USER_AGENTS`) + :param user_agent: Custom user agent to use when sending requests. If `None`, user agent will be chosen from a premade list :param max_tries: Maximum number of times to try logging in :param session_cookies: Cookies from a previous session (Will default to login if these are invalid) :param logging_level: Configures the `logging level `_. Defaults to `INFO` @@ -76,7 +76,7 @@ class Client(object): self._payload_default = OrderedDict() self._default_thread_id = None self._default_thread_type = None - self.req_url = ReqUrl() + self._req_url = ReqUrl() self._markAlive = True self._buddylist = dict() @@ -85,8 +85,8 @@ class Client(object): self._header = { "Content-Type": "application/x-www-form-urlencoded", - "Referer": self.req_url.BASE, - "Origin": self.req_url.BASE, + "Referer": self._req_url.BASE, + "Origin": self._req_url.BASE, "User-Agent": user_agent, "Connection": "keep-alive", } @@ -276,7 +276,7 @@ class Client(object): "queries": graphql_queries_to_json(*queries), } return tuple( - self._post(self.req_url.GRAPHQL, data, fix_request=True, as_graphql=True) + self._post(self._req_url.GRAPHQL, data, fix_request=True, as_graphql=True) ) def graphql_request(self, query): @@ -310,7 +310,7 @@ class Client(object): raise FBchatException("Could not find c_user cookie") self._uid = str(self._uid) - r = self._get(self.req_url.BASE) + r = self._get(self._req_url.BASE) soup = bs(r.text, "html.parser") fb_dtsg_element = soup.find("input", {"name": "fb_dtsg"}) @@ -337,7 +337,7 @@ class Client(object): self._payload_default["fb_dtsg"] = fb_dtsg def _login(self, email, password): - soup = bs(self._get(self.req_url.MOBILE).text, "html.parser") + soup = bs(self._get(self._req_url.MOBILE).text, "html.parser") data = dict( (elem["name"], elem["value"]) for elem in soup.findAll("input") @@ -347,7 +347,7 @@ class Client(object): data["pass"] = password data["login"] = "Log In" - r = self._cleanPost(self.req_url.LOGIN, data) + r = self._cleanPost(self._req_url.LOGIN, data) # Usually, 'Checkpoint' will refer to 2FA if "checkpoint" in r.url and ('id="approvals_code"' in r.text.lower()): @@ -355,7 +355,7 @@ class Client(object): # Sometimes Facebook tries to show the user a "Save Device" dialog if "save-device" in r.url: - r = self._cleanGet(self.req_url.SAVE_DEVICE) + r = self._cleanGet(self._req_url.SAVE_DEVICE) if "home" in r.url: self._postLogin() @@ -376,7 +376,7 @@ class Client(object): data["codes_submitted"] = 0 log.info("Submitting 2FA code.") - r = self._cleanPost(self.req_url.CHECKPOINT, data) + r = self._cleanPost(self._req_url.CHECKPOINT, data) if "home" in r.url: return r @@ -390,7 +390,7 @@ class Client(object): log.info( "Saving browser." ) # At this stage, we have dtsg, nh, name_action_selected, submit[Continue] - r = self._cleanPost(self.req_url.CHECKPOINT, data) + r = self._cleanPost(self._req_url.CHECKPOINT, data) if "home" in r.url: return r @@ -399,7 +399,7 @@ class Client(object): log.info( "Starting Facebook checkup flow." ) # At this stage, we have dtsg, nh, submit[Continue] - r = self._cleanPost(self.req_url.CHECKPOINT, data) + r = self._cleanPost(self._req_url.CHECKPOINT, data) if "home" in r.url: return r @@ -409,7 +409,7 @@ class Client(object): log.info( "Verifying login attempt." ) # At this stage, we have dtsg, nh, submit[This was me] - r = self._cleanPost(self.req_url.CHECKPOINT, data) + r = self._cleanPost(self._req_url.CHECKPOINT, data) if "home" in r.url: return r @@ -420,7 +420,7 @@ class Client(object): log.info( "Saving device again." ) # At this stage, we have dtsg, nh, submit[Continue], name_action_selected - r = self._cleanPost(self.req_url.CHECKPOINT, data) + r = self._cleanPost(self._req_url.CHECKPOINT, data) return r def isLoggedIn(self): @@ -431,7 +431,7 @@ class Client(object): :rtype: bool """ # Send a request to the login url, to see if we're directed to the home page - r = self._cleanGet(self.req_url.LOGIN, allow_redirects=False) + r = self._cleanGet(self._req_url.LOGIN, allow_redirects=False) return "Location" in r.headers and "home" in r.headers["Location"] def getSession(self): @@ -512,12 +512,12 @@ class Client(object): :rtype: bool """ if not hasattr(self, "_fb_h"): - h_r = self._post(self.req_url.MODERN_SETTINGS_MENU, {"pmid": "4"}) + h_r = self._post(self._req_url.MODERN_SETTINGS_MENU, {"pmid": "4"}) self._fb_h = re.search(r'name=\\"h\\" value=\\"(.*?)\\"', h_r.text).group(1) data = {"ref": "mb", "h": self._fb_h} - r = self._get(self.req_url.LOGOUT, data) + r = self._get(self._req_url.LOGOUT, data) self._resetValues() @@ -663,7 +663,7 @@ class Client(object): """ data = {"viewer": self._uid} j = self._post( - self.req_url.ALL_USERS, query=data, fix_request=True, as_json=True + self._req_url.ALL_USERS, query=data, fix_request=True, as_json=True ) if j.get("payload") is None: raise FBchatException("Missing payload while fetching users: {}".format(j)) @@ -777,7 +777,7 @@ class Client(object): "thread_fbid": thread_id, } j = self._post( - self.req_url.SEARCH_MESSAGES, data, fix_request=True, as_json=True + self._req_url.SEARCH_MESSAGES, data, fix_request=True, as_json=True ) result = j["payload"]["search_snippets"][query] @@ -824,7 +824,7 @@ class Client(object): """ data = {"query": query, "snippetLimit": thread_limit} j = self._post( - self.req_url.SEARCH_MESSAGES, data, fix_request=True, as_json=True + self._req_url.SEARCH_MESSAGES, data, fix_request=True, as_json=True ) result = j["payload"]["search_snippets"][query] @@ -843,7 +843,7 @@ class Client(object): def _fetchInfo(self, *ids): data = {"ids[{}]".format(i): _id for i, _id in enumerate(ids)} - j = self._post(self.req_url.INFO, data, fix_request=True, as_json=True) + j = self._post(self._req_url.INFO, data, fix_request=True, as_json=True) if j.get("payload") is None or j["payload"].get("profiles") is None: raise FBchatException("No users/pages returned: {}".format(j)) @@ -1114,7 +1114,7 @@ class Client(object): # 'last_action_timestamp': 0 } j = self._post( - self.req_url.UNREAD_THREADS, form, fix_request=True, as_json=True + self._req_url.UNREAD_THREADS, form, fix_request=True, as_json=True ) payload = j["payload"]["unread_thread_fbids"][0] @@ -1129,7 +1129,7 @@ class Client(object): :raises: FBchatException if request failed """ j = self._post( - self.req_url.UNSEEN_THREADS, None, fix_request=True, as_json=True + self._req_url.UNSEEN_THREADS, None, fix_request=True, as_json=True ) payload = j["payload"]["unseen_thread_fbids"][0] @@ -1179,7 +1179,7 @@ class Client(object): """ data = {"question_id": poll_id} j = self._post( - self.req_url.GET_POLL_OPTIONS, data, fix_request=True, as_json=True + self._req_url.GET_POLL_OPTIONS, data, fix_request=True, as_json=True ) return [PollOption._from_graphql(m) for m in j["payload"]] @@ -1193,7 +1193,7 @@ class Client(object): :raises: FBchatException if request failed """ data = {"event_reminder_id": plan_id} - j = self._post(self.req_url.PLAN_INFO, data, fix_request=True, as_json=True) + j = self._post(self._req_url.PLAN_INFO, data, fix_request=True, as_json=True) return Plan._from_fetch(j["payload"]) def _getPrivateData(self): @@ -1318,7 +1318,7 @@ class Client(object): def _doSendRequest(self, data, get_thread_id=False): """Sends the data to `SendURL`, and returns the message ID or None on failure""" - j = self._post(self.req_url.SEND, data, fix_request=True, as_json=True) + j = self._post(self._req_url.SEND, data, fix_request=True, as_json=True) # update JS token if received in response fb_dtsg = get_jsmods_require(j, 2) @@ -1453,7 +1453,7 @@ class Client(object): :param mid: :ref:`Message ID ` of the message to unsend """ data = {"message_id": mid} - r = self._post(self.req_url.UNSEND, data) + r = self._post(self._req_url.UNSEND, data) r.raise_for_status() def _sendLocation( @@ -1529,7 +1529,7 @@ class Client(object): data = {"voice_clip": voice_clip} j = self._postFile( - self.req_url.UPLOAD, + self._req_url.UPLOAD, files=file_dict, query=data, fix_request=True, @@ -1713,7 +1713,7 @@ class Client(object): "recipient_map[{}]".format(generateOfflineThreadingID()): thread_id, } j = self._post( - self.req_url.FORWARD_ATTACHMENT, data, fix_request=True, as_json=True + self._req_url.FORWARD_ATTACHMENT, data, fix_request=True, as_json=True ) def createGroup(self, message, user_ids): @@ -1780,7 +1780,7 @@ class Client(object): thread_id, thread_type = self._getThread(thread_id, None) data = {"uid": user_id, "tid": thread_id} - j = self._post(self.req_url.REMOVE_USER, data, fix_request=True, as_json=True) + j = self._post(self._req_url.REMOVE_USER, data, fix_request=True, as_json=True) def _adminStatus(self, admin_ids, admin, thread_id=None): thread_id, thread_type = self._getThread(thread_id, None) @@ -1792,7 +1792,7 @@ class Client(object): for i, admin_id in enumerate(admin_ids): data["admin_ids[{}]".format(i)] = str(admin_id) - j = self._post(self.req_url.SAVE_ADMINS, data, fix_request=True, as_json=True) + j = self._post(self._req_url.SAVE_ADMINS, data, fix_request=True, as_json=True) def addGroupAdmins(self, admin_ids, thread_id=None): """ @@ -1825,7 +1825,9 @@ class Client(object): thread_id, thread_type = self._getThread(thread_id, None) data = {"set_mode": int(require_admin_approval), "thread_fbid": thread_id} - j = self._post(self.req_url.APPROVAL_MODE, data, fix_request=True, as_json=True) + j = self._post( + self._req_url.APPROVAL_MODE, data, fix_request=True, as_json=True + ) def _usersApproval(self, user_ids, approve, thread_id=None): thread_id, thread_type = self._getThread(thread_id, None) @@ -1876,7 +1878,7 @@ class Client(object): data = {"thread_image_id": image_id, "thread_id": thread_id} - j = self._post(self.req_url.THREAD_IMAGE, data, fix_request=True, as_json=True) + j = self._post(self._req_url.THREAD_IMAGE, data, fix_request=True, as_json=True) return image_id def changeGroupImageRemote(self, image_url, thread_id=None): @@ -1923,7 +1925,7 @@ class Client(object): ) data = {"thread_name": title, "thread_id": thread_id} - j = self._post(self.req_url.THREAD_NAME, data, fix_request=True, as_json=True) + j = self._post(self._req_url.THREAD_NAME, data, fix_request=True, as_json=True) def changeNickname( self, nickname, user_id, thread_id=None, thread_type=ThreadType.USER @@ -1946,7 +1948,7 @@ class Client(object): "thread_or_other_fbid": thread_id, } j = self._post( - self.req_url.THREAD_NICKNAME, data, fix_request=True, as_json=True + self._req_url.THREAD_NICKNAME, data, fix_request=True, as_json=True ) def changeThreadColor(self, color, thread_id=None): @@ -1964,7 +1966,7 @@ class Client(object): "color_choice": color.value if color != ThreadColor.MESSENGER_BLUE else "", "thread_or_other_fbid": thread_id, } - j = self._post(self.req_url.THREAD_COLOR, data, fix_request=True, as_json=True) + j = self._post(self._req_url.THREAD_COLOR, data, fix_request=True, as_json=True) def changeThreadEmoji(self, emoji, thread_id=None): """ @@ -1979,7 +1981,7 @@ class Client(object): thread_id, thread_type = self._getThread(thread_id, None) data = {"emoji_choice": emoji, "thread_or_other_fbid": thread_id} - j = self._post(self.req_url.THREAD_EMOJI, data, fix_request=True, as_json=True) + j = self._post(self._req_url.THREAD_EMOJI, data, fix_request=True, as_json=True) def reactToMessage(self, message_id, reaction): """ @@ -1998,7 +2000,7 @@ class Client(object): "reaction": reaction.value if reaction else None, } data = {"doc_id": 1491398900900362, "variables": json.dumps({"data": data})} - self._post(self.req_url.MESSAGE_REACTION, data, fix_request=True, as_json=True) + self._post(self._req_url.MESSAGE_REACTION, data, fix_request=True, as_json=True) def createPlan(self, plan, thread_id=None): """ @@ -2020,7 +2022,7 @@ class Client(object): "location_name": plan.location or "", "acontext": ACONTEXT, } - j = self._post(self.req_url.PLAN_CREATE, data, fix_request=True, as_json=True) + j = self._post(self._req_url.PLAN_CREATE, data, fix_request=True, as_json=True) def editPlan(self, plan, new_plan): """ @@ -2040,7 +2042,7 @@ class Client(object): "title": new_plan.title, "acontext": ACONTEXT, } - j = self._post(self.req_url.PLAN_CHANGE, data, fix_request=True, as_json=True) + j = self._post(self._req_url.PLAN_CHANGE, data, fix_request=True, as_json=True) def deletePlan(self, plan): """ @@ -2050,7 +2052,7 @@ class Client(object): :raises: FBchatException if request failed """ data = {"event_reminder_id": plan.uid, "delete": "true", "acontext": ACONTEXT} - j = self._post(self.req_url.PLAN_CHANGE, data, fix_request=True, as_json=True) + j = self._post(self._req_url.PLAN_CHANGE, data, fix_request=True, as_json=True) def changePlanParticipation(self, plan, take_part=True): """ @@ -2066,7 +2068,7 @@ class Client(object): "acontext": ACONTEXT, } j = self._post( - self.req_url.PLAN_PARTICIPATION, data, fix_request=True, as_json=True + self._req_url.PLAN_PARTICIPATION, data, fix_request=True, as_json=True ) def eventReminder(self, thread_id, time, title, location="", location_id=""): @@ -2100,7 +2102,7 @@ class Client(object): data["option_text_array[{}]".format(i)] = option.text data["option_is_selected_array[{}]".format(i)] = str(int(option.vote)) - j = self._post(self.req_url.CREATE_POLL, data, fix_request=True, as_json=True) + j = self._post(self._req_url.CREATE_POLL, data, fix_request=True, as_json=True) def updatePollVote(self, poll_id, option_ids=[], new_options=[]): """ @@ -2122,7 +2124,7 @@ class Client(object): for i, option_text in enumerate(new_options): data["new_options[{}]".format(i)] = option_text - j = self._post(self.req_url.UPDATE_VOTE, data, fix_request=True, as_json=True) + j = self._post(self._req_url.UPDATE_VOTE, data, fix_request=True, as_json=True) def setTypingStatus(self, status, thread_id=None, thread_type=None): """ @@ -2143,7 +2145,7 @@ class Client(object): "to": thread_id if thread_type == ThreadType.USER else "", "source": "mercury-chat", } - j = self._post(self.req_url.TYPING, data, fix_request=True, as_json=True) + j = self._post(self._req_url.TYPING, data, fix_request=True, as_json=True) """ END SEND METHODS @@ -2163,7 +2165,7 @@ class Client(object): "thread_ids[%s][0]" % thread_id: message_id, } - r = self._post(self.req_url.DELIVERED, data) + r = self._post(self._req_url.DELIVERED, data) return r.ok def _readStatus(self, read, thread_ids): @@ -2174,7 +2176,7 @@ class Client(object): for thread_id in thread_ids: data["ids[{}]".format(thread_id)] = "true" if read else "false" - r = self._post(self.req_url.READ_STATUS, data) + r = self._post(self._req_url.READ_STATUS, data) return r.ok def markAsRead(self, thread_ids=None): @@ -2204,7 +2206,7 @@ class Client(object): .. todo:: Documenting this """ - r = self._post(self.req_url.MARK_SEEN, {"seen_timestamp": now()}) + r = self._post(self._req_url.MARK_SEEN, {"seen_timestamp": now()}) return r.ok def friendConnect(self, friend_id): @@ -2214,7 +2216,7 @@ class Client(object): """ data = {"to_friend": friend_id, "action": "confirm"} - r = self._post(self.req_url.CONNECT, data) + r = self._post(self._req_url.CONNECT, data) return r.ok def removeFriend(self, friend_id=None): @@ -2225,7 +2227,7 @@ class Client(object): :return: Returns error if the removing was unsuccessful, returns True when successful. """ payload = {"friend_id": friend_id, "unref": "none", "confirm": "Confirm"} - r = self._post(self.req_url.REMOVE_FRIEND, payload) + r = self._post(self._req_url.REMOVE_FRIEND, payload) query = parse_qs(urlparse(r.url).query) if "err" not in query: log.debug("Remove was successful!") @@ -2243,7 +2245,7 @@ class Client(object): :raises: FBchatException if request failed """ data = {"fbid": user_id} - r = self._post(self.req_url.BLOCK_USER, data) + r = self._post(self._req_url.BLOCK_USER, data) return r.ok def unblockUser(self, user_id): @@ -2255,7 +2257,7 @@ class Client(object): :raises: FBchatException if request failed """ data = {"fbid": user_id} - r = self._post(self.req_url.UNBLOCK_USER, data) + r = self._post(self._req_url.UNBLOCK_USER, data) return r.ok def moveThreads(self, location, thread_ids): @@ -2278,14 +2280,14 @@ class Client(object): for thread_id in thread_ids: data_archive["ids[{}]".format(thread_id)] = "true" data_unpin["ids[{}]".format(thread_id)] = "false" - r_archive = self._post(self.req_url.ARCHIVED_STATUS, data_archive) - r_unpin = self._post(self.req_url.PINNED_STATUS, data_unpin) + r_archive = self._post(self._req_url.ARCHIVED_STATUS, data_archive) + r_unpin = self._post(self._req_url.PINNED_STATUS, data_unpin) return r_archive.ok and r_unpin.ok else: data = dict() for i, thread_id in enumerate(thread_ids): data["{}[{}]".format(location.name.lower(), i)] = thread_id - r = self._post(self.req_url.MOVE_THREAD, data) + r = self._post(self._req_url.MOVE_THREAD, data) return r.ok def deleteThreads(self, thread_ids): @@ -2303,8 +2305,8 @@ class Client(object): for i, thread_id in enumerate(thread_ids): data_unpin["ids[{}]".format(thread_id)] = "false" data_delete["ids[{}]".format(i)] = thread_id - r_unpin = self._post(self.req_url.PINNED_STATUS, data_unpin) - r_delete = self._post(self.req_url.DELETE_THREAD, data_delete) + r_unpin = self._post(self._req_url.PINNED_STATUS, data_unpin) + r_delete = self._post(self._req_url.DELETE_THREAD, data_delete) return r_unpin.ok and r_delete.ok def markAsSpam(self, thread_id=None): @@ -2316,7 +2318,7 @@ class Client(object): :raises: FBchatException if request failed """ thread_id, thread_type = self._getThread(thread_id, None) - r = self._post(self.req_url.MARK_SPAM, {"id": thread_id}) + r = self._post(self._req_url.MARK_SPAM, {"id": thread_id}) return r.ok def deleteMessages(self, message_ids): @@ -2331,7 +2333,7 @@ class Client(object): data = dict() for i, message_id in enumerate(message_ids): data["message_ids[{}]".format(i)] = message_id - r = self._post(self.req_url.DELETE_MESSAGES, data) + r = self._post(self._req_url.DELETE_MESSAGES, data) return r.ok def muteThread(self, mute_time=-1, thread_id=None): @@ -2343,7 +2345,7 @@ class Client(object): """ thread_id, thread_type = self._getThread(thread_id, None) data = {"mute_settings": str(mute_time), "thread_fbid": thread_id} - content = self._post(self.req_url.MUTE_THREAD, data, fix_request=True) + content = self._post(self._req_url.MUTE_THREAD, data, fix_request=True) def unmuteThread(self, thread_id=None): """ @@ -2362,7 +2364,7 @@ class Client(object): """ thread_id, thread_type = self._getThread(thread_id, None) data = {"reactions_mute_mode": int(mute), "thread_fbid": thread_id} - r = self._post(self.req_url.MUTE_REACTIONS, data, fix_request=True) + r = self._post(self._req_url.MUTE_REACTIONS, data, fix_request=True) def unmuteThreadReactions(self, thread_id=None): """ @@ -2381,7 +2383,7 @@ class Client(object): """ thread_id, thread_type = self._getThread(thread_id, None) data = {"mentions_mute_mode": int(mute), "thread_fbid": thread_id} - r = self._post(self.req_url.MUTE_MENTIONS, data, fix_request=True) + r = self._post(self._req_url.MUTE_MENTIONS, data, fix_request=True) def unmuteThreadMentions(self, thread_id=None): """ @@ -2407,7 +2409,7 @@ class Client(object): "viewer_uid": self._uid, "state": "active", } - self._get(self.req_url.PING, data, fix_request=True, as_json=False) + self._get(self._req_url.PING, data, fix_request=True, as_json=False) def _pullMessage(self): """Call pull api with seq value to get message data.""" @@ -2418,7 +2420,7 @@ class Client(object): "clientid": self._client_id, "state": "active" if self._markAlive else "offline", } - return self._get(self.req_url.STICKY, data, fix_request=True, as_json=True) + return self._get(self._req_url.STICKY, data, fix_request=True, as_json=True) def _parseDelta(self, m): def getThreadIdAndThreadType(msg_metadata): @@ -3110,7 +3112,7 @@ class Client(object): except FBchatFacebookError as e: # Fix 502 and 503 pull errors if e.request_status_code in [502, 503]: - self.req_url.change_pull_channel() + self._req_url.change_pull_channel() self.startListening() else: raise e diff --git a/fbchat/client.py b/fbchat/client.py deleted file mode 100644 index 9244327..0000000 --- a/fbchat/client.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: UTF-8 -*- -"""This file is here to maintain backwards compatability.""" -from __future__ import unicode_literals - -from .utils import * -from .models import * -from ._client import Client diff --git a/fbchat/utils.py b/fbchat/utils.py deleted file mode 100644 index dc3385e..0000000 --- a/fbchat/utils.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: UTF-8 -*- -"""This file is here to maintain backwards compatability.""" -from __future__ import unicode_literals - -from .models import * -from ._util import ( - log, - handler, - USER_AGENTS, - ReqUrl, - facebookEncoding, - now, - strip_to_json, - get_decoded_r, - get_decoded, - parse_json, - get_json, - digitToChar, - str_base, - generateMessageID, - getSignatureID, - generateOfflineThreadingID, - check_json, - check_request, - get_jsmods_require, - require_list, - mimetype_to_key, - get_files_from_urls, - get_files_from_paths, - get_url_parameters, - get_url_parameter, -)