cone.firebase 0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,87 @@
1
+ from cone.app import main_hook
2
+ from cone.app.interfaces import IAuthenticator
3
+ from cone.firebase.authentication import FirebaseAuthenticator
4
+ from cone.ugm.browser.principal import user_field
5
+ from yafowil.base import factory
6
+ import firebase_admin
7
+ import json
8
+ import logging
9
+
10
+
11
+ logger = logging.getLogger('cone.firebase')
12
+
13
+
14
+ class FirebaseConfig(object):
15
+ web_api_key = None
16
+ service_account_json = None
17
+
18
+ def __init__(self, web_api_key, service_account_json_file):
19
+ self.web_api_key = web_api_key
20
+ with open(service_account_json_file, 'r') as f:
21
+ self.service_account_json = json.loads(f.read())
22
+
23
+
24
+ # FirebaseConfig singleton
25
+ config = None
26
+
27
+
28
+ def load_firebase_config(settings):
29
+ global config
30
+ config = FirebaseConfig(
31
+ settings['firebase.web_api_key'],
32
+ settings['firebase.service_account_json_file']
33
+ )
34
+
35
+
36
+ def initialize_firebase_admin():
37
+ service_account_json = config.service_account_json
38
+ cred = firebase_admin.credentials.Certificate(service_account_json)
39
+ if not firebase_admin._apps:
40
+ firebase_admin.initialize_app(cred)
41
+
42
+
43
+ @user_field('firebase_user')
44
+ def firebase_user_field_factory(form, label, value):
45
+ return factory(
46
+ 'field:label:help:error:checkbox',
47
+ value=value,
48
+ props={
49
+ 'label': label,
50
+ 'datatype': bool,
51
+ 'help': "shall this user also be managed in Firebase?",
52
+ })
53
+
54
+
55
+ @user_field('email_verified')
56
+ def email_verified(form, label, value):
57
+ return factory(
58
+ 'field:label:help:error:checkbox',
59
+ value=value,
60
+ props={
61
+ 'label': label,
62
+ 'datatype': bool,
63
+ 'help': "is user verified by email?",
64
+ })
65
+
66
+
67
+ @user_field('fullname')
68
+ def fullname_field_factory(form, label, value):
69
+ return factory(
70
+ 'field:label:error:text',
71
+ value=value,
72
+ props={
73
+ 'label': label,
74
+ 'datatype': str,
75
+ 'required': "fullname not given"
76
+ })
77
+
78
+
79
+ @main_hook
80
+ def initialize_firebase(config, global_config, settings):
81
+ load_firebase_config(settings)
82
+ initialize_firebase_admin()
83
+ config.registry.registerUtility(
84
+ FirebaseAuthenticator,
85
+ IAuthenticator,
86
+ 'firebase'
87
+ )
cone/firebase/api.py ADDED
@@ -0,0 +1,5 @@
1
+ from cone.firebase.management import get_device_tokens_for_user # noqa
2
+ from cone.firebase.management import register_device_token_for_user # noqa
3
+ from cone.firebase.messaging import send_message # noqa
4
+ from cone.firebase.messaging import send_message_to_user # noqa
5
+ from cone.firebase.messaging import send_messages # noqa
@@ -0,0 +1,76 @@
1
+ from cone.app import security
2
+ from cone.app import ugm_backend
3
+ from cone.app.interfaces import IAuthenticator
4
+ from zope.interface import implementer
5
+ import cone.firebase
6
+ import json
7
+ import requests
8
+
9
+
10
+ REST_API_URL_LOGIN = f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword"
11
+
12
+
13
+ def sign_in_with_email_and_password(email, password, api_key, return_secure_token = True):
14
+ """
15
+ borrowed from here:
16
+ https://github.com/billydh/python-firebase-admin-sdk-demo/blob/master/sign_in_with_email_and_password.py
17
+ """
18
+
19
+ try:
20
+ payload = json.dumps({
21
+ "email": email or "",
22
+ "password": password or "",
23
+ "returnSecureToken": return_secure_token
24
+ })
25
+ except Exception as ex:
26
+ cone.firebase.logger.error(f"error encoding email: {email} and password: {password}")
27
+ raise
28
+
29
+ r = requests.post(REST_API_URL_LOGIN,
30
+ params={"key": api_key},
31
+ data=payload)
32
+
33
+ return r.json()
34
+
35
+
36
+ @implementer(IAuthenticator)
37
+ class FirebaseAuthenticator:
38
+
39
+ @classmethod
40
+ def authenticate(cls, uid: str, pwd: str) -> str:
41
+ """
42
+ does firebase authentication and in the case of success
43
+ checks if the uid exists in self.users, if not it creates the user there
44
+ when fb login is unsuccessful delegates it to
45
+ :return: login of logged in user
46
+ """
47
+
48
+ res = sign_in_with_email_and_password(uid, pwd, cone.firebase.config.web_api_key)
49
+
50
+ ugm = ugm_backend.ugm
51
+ users = ugm.users
52
+
53
+ if res.get("kind") == 'identitytoolkit#VerifyPasswordResponse':
54
+ id = res["localId"]
55
+ if id not in users:
56
+ users.create(
57
+ id,
58
+ login="email",
59
+ email=res["email"],
60
+ fullname=res.get("displayName"),
61
+ registered=res["registered"],
62
+ idtoken=res["idToken"]
63
+ )
64
+ # else:
65
+ # user email aktualisieren
66
+
67
+ user = users[id]
68
+ if hasattr(users, "on_authenticated"):
69
+ users.on_authenticated(res["localId"])
70
+ # user.passwd(None, pwd) # it is to decide if we need local pwds
71
+ return id
72
+ else:
73
+ # if not found in firebase login locally
74
+ auth = users.authenticate(uid, pwd)
75
+ if auth:
76
+ return uid
@@ -0,0 +1,181 @@
1
+ from cone.app import ugm_backend
2
+ from cone.ugm.events import UserCreatedEvent
3
+ from cone.ugm.events import UserDeletedEvent
4
+ from cone.ugm.events import UserModifiedEvent
5
+ from firebase_admin import auth
6
+ from firebase_admin.auth import UserNotFoundError
7
+ from pyramid.security import remember
8
+ from typing import List
9
+ from typing import Tuple
10
+ from yafowil.base import ExtractionError
11
+ from zope.event import classhandler
12
+ import cone.firebase
13
+
14
+
15
+ FIREBASE_DEVICE_TOKENS = "firebase_device_tokens"
16
+
17
+
18
+ @classhandler.handler(UserCreatedEvent)
19
+ def on_user_created(event: UserCreatedEvent):
20
+ user = event.principal
21
+ email = user.attrs["mail"]
22
+ uid = user.attrs["id"]
23
+ # if firebase_user checkbox is not installed, we want to add all users to FB
24
+ if "firebase_user" not in user.attrs or user.attrs["firebase_user"]:
25
+ if email:
26
+ user_record = create_firebase_user(user, event.password)
27
+ cone.firebase.logger.info(f"user {uid} added to firebase with email {email} -> {user_record.__dict__}")
28
+ else:
29
+ cone.firebase.logger.warn(f"user {uid} has no email -> not added to firebase")
30
+
31
+
32
+ def create_firebase_user(user, password):
33
+ fullname = user.attrs["fullname"]
34
+ user_record = auth.create_user(
35
+ uid=user.attrs["id"],
36
+ email=user.attrs["mail"],
37
+ # phone_number='+15555550100',
38
+ # email_verified=True,
39
+ password=password,
40
+ display_name=fullname,
41
+ # photo_url='http://www.example.com/12345678/photo.png',
42
+ disabled=False
43
+ )
44
+ user.attrs["login"] = "mail"
45
+ return user_record
46
+
47
+
48
+ @classhandler.handler(UserModifiedEvent)
49
+ def on_user_modified(event: UserModifiedEvent):
50
+ user = event.principal
51
+ email = user.attrs["mail"]
52
+ uid = user.attrs["id"]
53
+ try:
54
+ fbuser = auth.get_user(uid)
55
+ except UserNotFoundError:
56
+ # user does not exist in firebase, lets push it to fb, the password has to be specified by hand, otherwise the
57
+ # hashed password will be set in fb!
58
+ if user.attrs.get("firebase_user", False):
59
+ cone.firebase.logger.warn(f"user wth id {uid} not found in firebase, creating it in fb")
60
+ if not user.attrs["fullname"]:
61
+ raise ExtractionError("Fullname not given")
62
+ fbuser = create_firebase_user(user, event.password)
63
+ cone.firebase.logger.warn(f"created user wth id {uid} in fb")
64
+ else:
65
+ fbuser = None
66
+ cone.firebase.logger.warn(f"user wth id {uid} not found in firebase")
67
+
68
+ if email and fbuser:
69
+ fullname = user.attrs["fullname"]
70
+ password = event.password
71
+
72
+ params = dict(
73
+ email=email,
74
+ # phone_number='+15555550100',
75
+ display_name=fullname,
76
+ disabled=False)
77
+
78
+ if event.password:
79
+ params["password"] = event.password
80
+
81
+ res = auth.update_user(
82
+ uid,
83
+ **params
84
+ )
85
+ cone.firebase.logger.info(f"user {uid} changes promoted to firebase with email {email} -> {res}")
86
+ else:
87
+ cone.firebase.logger.warn(f"user {uid} has no email -> not added to firebase")
88
+
89
+
90
+ @classhandler.handler(UserDeletedEvent)
91
+ def on_user_deleted(event):
92
+ user = event.principal
93
+ uid = user.attrs["id"]
94
+ try:
95
+ fbuser = auth.get_user(uid)
96
+ auth.delete_user(uid)
97
+ cone.firebase.logger.info(f"user with id {uid} deleted in firebase")
98
+ except UserNotFoundError:
99
+ cone.firebase.logger.warn(f"user with id {uid} not found in firebase -> user not deleted in fb")
100
+
101
+
102
+ def authenticate_with_id_token(request, id_token: str) -> Tuple[str, str]:
103
+ """
104
+ uses the firebase ID token to login without password
105
+ needs installed UGM user folder located at AppRoot()["users"]
106
+
107
+ :return: tuple (user_id, auth token)
108
+ """
109
+ # users = AppRoot()["users"]
110
+ cone.firebase.logger.info(f"loggin in with firebase id token: {id_token}")
111
+ ugm = ugm_backend.ugm
112
+ users = ugm.users
113
+ res = auth.verify_id_token(id_token)
114
+ user_id = res["user_id"]
115
+ if user_id not in users:
116
+ # TODO: creation shall take place in security.authenticate()
117
+ cone.firebase.logger.info(f"user with id {user_id} not stored locally - creating")
118
+ users.create(
119
+ user_id,
120
+ login="email",
121
+ email=res["email"],
122
+ fullname=res.get("name", ""),
123
+ email_verified=res["email_verified"],
124
+ phone=res.get("phone_number", ""),
125
+ idtoken=id_token
126
+ )
127
+ cone.firebase.logger.info(f"user with {user_id} successfully created")
128
+
129
+ return user_id, remember(request, user_id)
130
+
131
+
132
+ def register_device_token_for_user(login: str, token: str) -> List[str]:
133
+ """
134
+ registers a device token for a given user
135
+ :param login: email or uid
136
+ :param token: firebase device token
137
+ """
138
+ users = ugm_backend.ugm.users
139
+ if login not in users:
140
+ uid = users.id_for_login(login)
141
+ else:
142
+ uid = login
143
+
144
+ user = users[uid]
145
+ tokens = user.attrs.get(FIREBASE_DEVICE_TOKENS, []) or []
146
+ if token not in tokens:
147
+ user.attrs[FIREBASE_DEVICE_TOKENS] = list(tokens) + [token]
148
+
149
+ return user.attrs[FIREBASE_DEVICE_TOKENS]
150
+
151
+
152
+ def unregister_device_token_for_user(login: str, token: str):
153
+ """
154
+ registers a device token for a given user
155
+ :param login: email or uid
156
+ :param token: firebase device token
157
+ """
158
+ users = ugm_backend.ugm.users
159
+ if login not in users:
160
+ uid = users.id_for_login(login)
161
+ else:
162
+ uid = login
163
+
164
+ user = users[uid]
165
+ tokens = user.attrs.get(FIREBASE_DEVICE_TOKENS, []) or []
166
+ if token in tokens:
167
+ user.attrs[FIREBASE_DEVICE_TOKENS] = [tok for tok in tokens if token != tok]
168
+
169
+
170
+ def get_device_tokens_for_user(login: str) -> List[str]:
171
+ users = ugm_backend.ugm.users
172
+ if login not in users:
173
+ uid = users.id_for_login(login)
174
+ else:
175
+ uid = login
176
+
177
+ if uid in users:
178
+ user = users[uid]
179
+ return user.attrs.get(FIREBASE_DEVICE_TOKENS, []) or []
180
+ else:
181
+ return []
@@ -0,0 +1,52 @@
1
+ """
2
+ Firebase Messaging with cone
3
+
4
+ docs see:
5
+ https://firebase.google.com/docs/cloud-messaging/send-message
6
+ """
7
+ from cone.app import ugm_backend
8
+ from cone.firebase.management import get_device_tokens_for_user
9
+ from cone.firebase.management import unregister_device_token_for_user
10
+ from firebase_admin import messaging
11
+ from firebase_admin.exceptions import InvalidArgumentError
12
+ from firebase_admin.messaging import UnregisteredError
13
+ from typing import Any
14
+ from typing import Dict
15
+ from typing import List
16
+ import firebase_admin
17
+ import logging
18
+
19
+
20
+ logger = logging.getLogger('cone.firebase')
21
+
22
+
23
+ def send_message(data: Dict[str, Any], token: str) -> str:
24
+ messaging = firebase_admin.messaging
25
+ message = messaging.Message(data=data, token=token)
26
+ response = messaging.send(message)
27
+ return response
28
+
29
+
30
+ def send_messages(data: Dict[str, Any], tokens: List[str]):
31
+ messaging = firebase_admin.messaging
32
+ message = messaging.MulticastMessage(data=data, tokens=tokens)
33
+ response = messaging.send_multicast(message)
34
+ return response
35
+
36
+
37
+ def send_message_to_user(login: str, data: Dict[str, Any]) -> List[str]:
38
+ tokens = get_device_tokens_for_user(login)
39
+ results = []
40
+ # return send_messages(data, tokens)
41
+ for token in tokens:
42
+ try:
43
+ res = send_message(data, token)
44
+ results.append(res)
45
+ except (UnregisteredError, InvalidArgumentError) as ex:
46
+ unregister_device_token_for_user(login, token)
47
+ logger.exception(f"error sending message to:{token}. the token is not registered in firebase, will be automatically unregistered")
48
+ except Exception as ex:
49
+ logger.exception(f"message to token {token} failed")
50
+ raise
51
+
52
+ return results
@@ -0,0 +1,133 @@
1
+ from cone import firebase
2
+ from cone.app.ugm import ugm_backend
3
+ from cone.firebase import api
4
+ from cone.firebase import authentication
5
+ from cone.firebase import messaging
6
+ from cone.firebase.testing import firebase_admin
7
+ from cone.firebase.testing.firebase_admin import messaging as fb_fake_messaging
8
+ from cone.ugm.events import UserCreatedEvent
9
+ from cone.ugm.events import UserDeletedEvent
10
+ from cone.ugm.events import UserModifiedEvent
11
+ from cone.ugm.testing import localmanager_config
12
+ from cone.ugm.testing import ugm_config
13
+ from cone.ugm.testing import UGMLayer
14
+ from zope.event import classhandler
15
+ import json
16
+ import os
17
+ import shutil
18
+ import tempfile
19
+
20
+
21
+ service_account_json = {
22
+ 'auth_provider_x509_cert_url': 'https://www.googleapis.com/oauth2/v1/certs',
23
+ 'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
24
+ 'client_email': 'firebase-adminsdk-foo@example.iam.gserviceaccount.com',
25
+ 'client_id': 'xxx',
26
+ 'client_x509_cert_url': 'https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-foo@example.iam.gserviceaccount.com',
27
+ 'private_key': '-----BEGIN PRIVATE KEY-----\nxxx\n-----END PRIVATE KEY-----\n',
28
+ 'private_key_id': 'xxx',
29
+ 'project_id': 'example',
30
+ 'token_uri': 'https://oauth2.googleapis.com/token',
31
+ 'type': 'service_account'
32
+ }
33
+
34
+
35
+ def fake_sign_in_with_email_and_password(
36
+ email,
37
+ password,
38
+ api_key,
39
+ return_secure_token=True
40
+ ):
41
+ try:
42
+ res = firebase_admin.get_user_by_email(email)
43
+ res['kind'] = 'identitytoolkit#VerifyPasswordResponse'
44
+ except KeyError:
45
+ res = {
46
+ 'error': {
47
+ 'code': 400,
48
+ 'message': 'INVALID_EMAIL',
49
+ 'errors': [{
50
+ 'message': 'INVALID_EMAIL',
51
+ 'domain': 'global',
52
+ 'reason': 'invalid'
53
+ }]
54
+ }
55
+ }
56
+ return res
57
+
58
+
59
+ class FirebaseLayer(UGMLayer):
60
+
61
+ def make_app(self):
62
+ ugm_users_file = os.path.join(self.ugm_dir, 'users')
63
+ ugm_groups_file = os.path.join(self.ugm_dir, 'groups')
64
+ ugm_roles_file = os.path.join(self.ugm_dir, 'roles')
65
+ ugm_datadir = os.path.join(self.ugm_dir, 'data')
66
+ # call direct to grandparent
67
+ super(UGMLayer, self).make_app(**{
68
+ 'cone.plugins': '\n'.join([
69
+ 'cone.ugm',
70
+ 'cone.firebase'
71
+ ]),
72
+ 'ugm.backend': 'file',
73
+ 'ugm.config': ugm_config,
74
+ 'ugm.localmanager_config': localmanager_config,
75
+ 'ugm.users_file': ugm_users_file,
76
+ 'ugm.groups_file': ugm_groups_file,
77
+ 'ugm.roles_file': ugm_roles_file,
78
+ 'ugm.datadir': ugm_datadir,
79
+ 'firebase.web_api_key': 'xxxxxxxxxx',
80
+ 'firebase.service_account_json_file': os.path.join(
81
+ self.tempdir,
82
+ 'service_account.json'
83
+ )
84
+ })
85
+ ugm_backend.initialize()
86
+
87
+ def setUp(self, args=None):
88
+ self.patch_modules()
89
+ self.tempdir = tempfile.mkdtemp()
90
+ with open(os.path.join(self.tempdir, 'service_account.json'), 'w') as f:
91
+ f.write(json.dumps(service_account_json))
92
+ super(FirebaseLayer, self).setUp()
93
+ # create a firebase only user
94
+ firebase_admin.create_user(id="donald", login="email", email="donald@duck.com")
95
+ # create a local only user
96
+ users = ugm_backend.ugm.users
97
+ users.create(
98
+ "donald_local",
99
+ login="email",
100
+ email="donald_local@duck.com",
101
+ fullname="Donald Duck local",
102
+ )
103
+ users["donald_local"].passwd(None, "daisy1")
104
+ assert "donald_local" in users
105
+
106
+ def tearDown(self):
107
+ self.unpatch_modules()
108
+ self.unregister_handlers()
109
+ super(FirebaseLayer, self).tearDown()
110
+ shutil.rmtree(self.tempdir)
111
+ firebase_admin.delete_user("donald")
112
+
113
+ def patch_modules(self):
114
+ self.firebase_admin_orgin = firebase.firebase_admin
115
+ firebase.firebase_admin = firebase_admin
116
+ firebase.firebase_admin.messaging = fb_fake_messaging
117
+ api.firebase_admin = firebase_admin
118
+ messaging.firebase_admin = firebase_admin
119
+ self.sign_in_with_email_and_password = authentication.sign_in_with_email_and_password
120
+ authentication.sign_in_with_email_and_password = fake_sign_in_with_email_and_password
121
+
122
+ def unpatch_modules(self):
123
+ firebase.firebase_admin = self.firebase_admin_orgin
124
+ api.firebase_admin = self.firebase_admin_orgin
125
+ authentication.sign_in_with_email_and_password = self.sign_in_with_email_and_password
126
+
127
+ def unregister_handlers(self):
128
+ del classhandler.registry[UserCreatedEvent]
129
+ del classhandler.registry[UserDeletedEvent]
130
+ del classhandler.registry[UserModifiedEvent]
131
+
132
+
133
+ firebase_layer = FirebaseLayer()
@@ -0,0 +1,54 @@
1
+ # faking the firebase auth api
2
+
3
+ from . import credentials # noqua
4
+ from . import messaging # noqa
5
+
6
+ _apps = None
7
+
8
+ users_by_email = {}
9
+ users_by_id = {}
10
+
11
+
12
+ def delete_user(uid):
13
+ user = get_user(uid)
14
+ del users_by_email[user["email"]]
15
+ del users_by_id[uid]
16
+
17
+
18
+ def get_user(uid):
19
+ return users_by_id[uid]
20
+
21
+
22
+ def get_user_by_email(email):
23
+ return users_by_email[email]
24
+
25
+
26
+ def update_user(uid, **kw):
27
+ user = users_by_id[uid]
28
+ user.update(**kw)
29
+
30
+
31
+ def create_user(**kw):
32
+ id = kw.pop("id")
33
+ email = kw.pop("email")
34
+ if not id:
35
+ id = email
36
+
37
+ new_user = {
38
+ "localId": id,
39
+ "email": email,
40
+ "idToken": "xxx",
41
+ "profilePicture": "",
42
+ "refreshToken": "string",
43
+ "expiresIn": "",
44
+ "registered":True
45
+ }
46
+ new_user.update(**kw)
47
+ users_by_id[new_user["localId"]] = new_user
48
+ users_by_email[new_user["email"]] = new_user
49
+ return new_user
50
+
51
+
52
+ def initialize_app(cred):
53
+ global _apps
54
+ _apps = True
@@ -0,0 +1,4 @@
1
+ class Certificate(object):
2
+
3
+ def __init__(self, service_account_json):
4
+ self.service_account_json = service_account_json
@@ -0,0 +1,121 @@
1
+ # Fake api for cloud messaging
2
+
3
+ class Message:
4
+ """A message that can be sent via Firebase Cloud Messaging.
5
+
6
+ Contains payload information as well as recipient information. In particular, the message must
7
+ contain exactly one of token, topic or condition fields.
8
+
9
+ Args:
10
+ data: A dictionary of data fields (optional). All keys and values in the dictionary must be
11
+ strings.
12
+ notification: An instance of ``messaging.Notification`` (optional).
13
+ android: An instance of ``messaging.AndroidConfig`` (optional).
14
+ webpush: An instance of ``messaging.WebpushConfig`` (optional).
15
+ apns: An instance of ``messaging.ApnsConfig`` (optional).
16
+ fcm_options: An instance of ``messaging.FCMOptions`` (optional).
17
+ token: The registration token of the device to which the message should be sent (optional).
18
+ topic: Name of the FCM topic to which the message should be sent (optional). Topic name
19
+ may contain the ``/topics/`` prefix.
20
+ condition: The FCM condition to which the message should be sent (optional).
21
+ """
22
+
23
+ def __init__(self, data=None, notification=None, android=None, webpush=None, apns=None,
24
+ fcm_options=None, token=None, topic=None, condition=None):
25
+ self.data = data
26
+ self.notification = notification
27
+ self.android = android
28
+ self.webpush = webpush
29
+ self.apns = apns
30
+ self.fcm_options = fcm_options
31
+ self.token = token
32
+ self.topic = topic
33
+ self.condition = condition
34
+
35
+ def __str__(self):
36
+ return json.dumps(self, cls=MessageEncoder, sort_keys=True)
37
+
38
+ class MulticastMessage:
39
+ """A message that can be sent to multiple tokens via Firebase Cloud Messaging.
40
+
41
+ Args:
42
+ tokens: A list of registration tokens of targeted devices.
43
+ data: A dictionary of data fields (optional). All keys and values in the dictionary must be
44
+ strings.
45
+ notification: An instance of ``messaging.Notification`` (optional).
46
+ android: An instance of ``messaging.AndroidConfig`` (optional).
47
+ webpush: An instance of ``messaging.WebpushConfig`` (optional).
48
+ apns: An instance of ``messaging.ApnsConfig`` (optional).
49
+ fcm_options: An instance of ``messaging.FCMOptions`` (optional).
50
+ """
51
+ def __init__(self, tokens, data=None, notification=None, android=None, webpush=None, apns=None,
52
+ fcm_options=None):
53
+ # _Validators.check_string_list('MulticastMessage.tokens', tokens)
54
+ if len(tokens) > 500:
55
+ raise ValueError('MulticastMessage.tokens must not contain more than 500 tokens.')
56
+ self.tokens = tokens
57
+ self.data = data
58
+ self.notification = notification
59
+ self.android = android
60
+ self.webpush = webpush
61
+ self.apns = apns
62
+ self.fcm_options = fcm_options
63
+
64
+
65
+ class BatchResponse:
66
+ """The response received from a batch request to the FCM API."""
67
+
68
+ def __init__(self, responses):
69
+ self._responses = responses
70
+ self._success_count = len([resp for resp in responses if resp.success])
71
+
72
+ @property
73
+ def responses(self):
74
+ """A list of ``messaging.SendResponse`` objects (possibly empty)."""
75
+ return self._responses
76
+
77
+ @property
78
+ def success_count(self):
79
+ return self._success_count
80
+
81
+ @property
82
+ def failure_count(self):
83
+ return len(self.responses) - self.success_count
84
+
85
+
86
+ class SendResponse:
87
+ """The response received from an individual batched request to the FCM API."""
88
+
89
+ def __init__(self, resp, exception):
90
+ self._exception = exception
91
+ self._message_id = None
92
+ if resp:
93
+ self._message_id = resp.get('name', None)
94
+
95
+ @property
96
+ def message_id(self):
97
+ """A message ID string that uniquely identifies the message."""
98
+ return self._message_id
99
+
100
+ @property
101
+ def success(self):
102
+ """A boolean indicating if the request was successful."""
103
+ return self._message_id is not None and not self._exception
104
+
105
+ @property
106
+ def exception(self):
107
+ """A ``FirebaseError`` if an error occurs while sending the message to the FCM service."""
108
+ return self._exception
109
+
110
+
111
+ def send(message: Message, dry_run=False):
112
+ return 'projects/willholzen-293208/messages/0:1612781129630326%d758af2bf9fd7ecd'
113
+
114
+
115
+ def send_multicast(message: Message, dry_run=False):
116
+ return BatchResponse(
117
+ [
118
+ SendResponse(
119
+ dict(name='projects/willholzen-293208/messages/0:1612781129630326%d758af2bf9fd7ecd'), None)
120
+ ]
121
+ )
cone/firebase/tests.py ADDED
@@ -0,0 +1,112 @@
1
+ from cone import firebase
2
+ from cone.app.ugm import ugm_backend
3
+ from cone.firebase import messaging
4
+ from cone.firebase import testing
5
+ from cone.firebase.api import get_device_tokens_for_user
6
+ from cone.firebase.api import register_device_token_for_user
7
+ from cone.firebase.testing.firebase_admin import messaging as fb_fake_messaging
8
+ from node.tests import NodeTestCase
9
+ import sys
10
+ import unittest
11
+
12
+
13
+ EXAMPLE_DEVICE_TOKEN = (
14
+ 'dtt9cGrcSXicn8mW0tvcTQ:APA91bHlcidOIQwXoXVa3p22fBDvgeu'
15
+ '1kUwElEKpdVcliODGAbtjviOV7Ruls2h__enWF1P_gZApIVOOfHKGl'
16
+ 'Tft0vWuzzwGapsXbZIIH9s7-rbpilV4Hu_JzoLBYAwpCoP3Nkf3foPv'
17
+ )
18
+
19
+
20
+ class TestFirebase(NodeTestCase):
21
+ layer = testing.firebase_layer
22
+
23
+ def test_initialize_firebase(self):
24
+ self.assertIsInstance(firebase.config, firebase.FirebaseConfig)
25
+ self.assertEqual(firebase.config.web_api_key, 'xxxxxxxxxx')
26
+ self.assertEqual(
27
+ firebase.config.service_account_json,
28
+ testing.service_account_json
29
+ )
30
+
31
+ def test_firebase_authentication(self):
32
+ """Tests a login situation where only a firebase user exists and after
33
+ authentication is added to the local UGM database.
34
+ """
35
+ from cone.app import security
36
+ users = ugm_backend.ugm.users
37
+ request = self.layer.new_request()
38
+ security.AUTHENTICATOR = "firebase"
39
+ security.authenticate(request, "donald@duck.com", "daisy1")
40
+
41
+ # during the authentication the user should be added to UGM
42
+ self.assertTrue("donald" in users)
43
+
44
+ def test_local_authentication(self):
45
+ """Tests a local only login for the situation that a certain user is
46
+ only given locally and login should the fall back to standard auth
47
+ """
48
+ from cone.app import security
49
+ users = ugm_backend.ugm.users
50
+ self.assertTrue("donald_local" in users)
51
+
52
+ request = self.layer.new_request()
53
+ security.AUTHENTICATOR = "firebase"
54
+ security.authenticate(request, "donald_local", "daisy1")
55
+
56
+ def test_management(self):
57
+ """Test management of device tokens for firebase messaging
58
+ """
59
+ register_device_token_for_user("donald", EXAMPLE_DEVICE_TOKEN)
60
+ self.assertTrue(EXAMPLE_DEVICE_TOKEN in get_device_tokens_for_user("donald"))
61
+
62
+ def test_send_message(self):
63
+ registration_token = EXAMPLE_DEVICE_TOKEN
64
+ message = fb_fake_messaging.Message(
65
+ data={
66
+ 'score': '850',
67
+ 'time': '2:45',
68
+ },
69
+ token=registration_token,
70
+ )
71
+ res = fb_fake_messaging.send(message, dry_run=True)
72
+ assert res == 'projects/willholzen-293208/messages/0:1612781129630326%d758af2bf9fd7ecd'
73
+
74
+ def test_send_multicast_message(self):
75
+ registration_tokens = [EXAMPLE_DEVICE_TOKEN]
76
+ message = fb_fake_messaging.MulticastMessage(
77
+ data={'score': '850', 'time': '2:45'},
78
+ tokens=registration_tokens,
79
+ )
80
+ fb_fake_messaging.send_multicast(message)
81
+
82
+ def test_send_message_to_user(self):
83
+ data = {'score': '850', 'time': '2:45'}
84
+ res = messaging.send_message_to_user("donald", data)
85
+ self.assertTrue(res[0].startswith('projects/willholzen-293208/messages/'))
86
+
87
+
88
+ def run_tests():
89
+ from cone.firebase import tests
90
+ from zope.testrunner.runner import Runner
91
+
92
+ suite = unittest.TestSuite()
93
+ suite.addTest(unittest.findTestCases(tests))
94
+
95
+ runner = Runner(found_suites=[suite])
96
+ runner.run()
97
+ sys.exit(int(runner.failed))
98
+
99
+
100
+ # def test_run_tests():
101
+ # from cone.firebase import tests
102
+ # from zope.testrunner.runner import Runner
103
+ #
104
+ # suite = unittest.TestSuite()
105
+ # suite.addTest(unittest.findTestCases(tests))
106
+ #
107
+ # runner = Runner(found_suites=[suite])
108
+ # runner.run()
109
+
110
+
111
+ if __name__ == '__main__':
112
+ run_tests()
@@ -0,0 +1 @@
1
+ import sys, types, os;p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('cone',));importlib = __import__('importlib.util');__import__('importlib.machinery');m = sys.modules.setdefault('cone', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('cone', [os.path.dirname(p)])));m = m or sys.modules.setdefault('cone', types.ModuleType('cone'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p)
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: cone.firebase
3
+ Version: 0.1
4
+ Summary: Firebase integration for cone.app
5
+ Home-page: http://github.com/conestack/cone.firebase
6
+ Author: Cone Contributors
7
+ Author-email: dev@conestack.org
8
+ License: Simplified BSD
9
+ Keywords: node pyramid cone web
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
13
+ License-File: LICENSE.rst
14
+ Requires-Dist: proto-plus==1.18.1
15
+ Requires-Dist: google-api-core==1.26.3
16
+ Requires-Dist: google-api-python-client==2.2.0
17
+ Requires-Dist: google-auth==1.29.0
18
+ Requires-Dist: google-auth-httplib2==0.1.0
19
+ Requires-Dist: google-auth-oauthlib==0.4.6
20
+ Requires-Dist: google-cloud-core==1.6.0
21
+ Requires-Dist: google-cloud-firestore==2.1.0
22
+ Requires-Dist: google-cloud-storage==1.37.1
23
+ Requires-Dist: google-crc32c==1.1.2
24
+ Requires-Dist: google-pasta==0.2.0
25
+ Requires-Dist: google-resumable-media==1.2.0
26
+ Requires-Dist: googleapis-common-protos==1.53.0
27
+ Requires-Dist: setuptools
28
+ Requires-Dist: cone.app<1.1.0
29
+ Requires-Dist: requests
30
+ Requires-Dist: firebase_admin==4.5.3
31
+ Provides-Extra: test
32
+ Requires-Dist: zope.testrunner; extra == "test"
33
+ Dynamic: author
34
+ Dynamic: author-email
35
+ Dynamic: classifier
36
+ Dynamic: description
37
+ Dynamic: home-page
38
+ Dynamic: keywords
39
+ Dynamic: license
40
+ Dynamic: license-file
41
+ Dynamic: provides-extra
42
+ Dynamic: requires-dist
43
+ Dynamic: summary
44
+
45
+ .. image:: https://img.shields.io/pypi/v/cone.firebase.svg
46
+ :target: https://pypi.python.org/pypi/cone.firebase
47
+ :alt: Latest PyPI version
48
+
49
+ .. image:: https://img.shields.io/pypi/dm/cone.firebase.svg
50
+ :target: https://pypi.python.org/pypi/cone.firebase
51
+ :alt: Number of PyPI downloads
52
+
53
+ .. image:: https://travis-ci.org/bluedynamics/cone.firebase.svg?branch=master
54
+ :target: https://travis-ci.org/bluedynamics/cone.firebase
55
+
56
+ .. image:: https://coveralls.io/repos/github/bluedynamics/cone.firebase/badge.svg?branch=master
57
+ :target: https://coveralls.io/github/bluedynamics/cone.firebase?branch=master
58
+
59
+
60
+ This package provides a firebase integration in to cone.app.
61
+
62
+
63
+ Installation
64
+ ------------
65
+
66
+ Include ``cone.firebase`` to install dependencies in your application's
67
+ ``setup.py``.
68
+
69
+
70
+ Configuration
71
+ -------------
72
+
73
+ Adopt your application config ini file to define firebase related API keys.
74
+
75
+ .. code-block:: ini
76
+
77
+ [app:my_app]
78
+ use = egg:cone.app#main
79
+
80
+ cone.plugins =
81
+ cone.firebase
82
+
83
+ cone.authenticator = firebase
84
+
85
+ firebase.web_api_key = xxx
86
+ firebase.service_account_json_file = path/to/service_account.json
87
+
88
+ also in your ugm.xml you can optionally add the `firebase_user` checkbox:
89
+
90
+ .. code-block:: xml
91
+
92
+ <users_form_attrmap>
93
+ ...
94
+ <elem>
95
+ <key>firebase_user</key>
96
+ <value>Firebase User</value>
97
+ </elem>
98
+ </users_form_attrmap>
99
+
100
+ When you want to upgrade an existing user to FB, you can do this by checking
101
+ `firebase_user` in the user edit form, butt attention: you have to manually
102
+ enter the password since the firebase API expects the password in cleartext.
103
+
104
+ Contributors
105
+ ============
106
+
107
+ - Robert Niederreiter
108
+ - Phil Auersperg
109
+
110
+
111
+ Changes
112
+ =======
113
+
114
+ 0.1 (2025-10-25)
115
+ ----------------
116
+
117
+ - Initial
118
+
119
+
120
+ License
121
+ =======
122
+
123
+ Copyright (c) 2021, BlueDynamics Alliance, Austria
124
+ Copyright (c) 2021, Cone Contributors
125
+ All rights reserved.
126
+
127
+ Redistribution and use in source and binary forms, with or without
128
+ modification, are permitted provided that the following conditions are met:
129
+
130
+ * Redistributions of source code must retain the above copyright notice, this
131
+ list of conditions and the following disclaimer.
132
+
133
+ * Redistributions in binary form must reproduce the above copyright notice, this
134
+ list of conditions and the following disclaimer in the documentation and/or
135
+ other materials provided with the distribution.
136
+
137
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
138
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
139
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
140
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
141
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
142
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
143
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
144
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
145
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
146
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,17 @@
1
+ cone.firebase-0.1-py3.9-nspkg.pth,sha256=WmSYeXqiFe1Qae-WQFmtUgaaYvduormqDUjrRgs1ASs,457
2
+ cone/firebase/__init__.py,sha256=8Y5bVP_QZ-wPY8raovTb_df_cVEmOn5Gr0Lt5x76MfU,2242
3
+ cone/firebase/api.py,sha256=XGsOZOnDwKkO5_SjNrUiKPPSE4ptTLZY0hv5v5wyEkw,328
4
+ cone/firebase/authentication.py,sha256=e1bBvMBodSngpoXBM9_6t2_XEq8qOWKRXB3l0bxU16w,2433
5
+ cone/firebase/management.py,sha256=EmbqLIJ0W57EHKG96SN_f7FgFwIygCegmhlr398bMs4,6044
6
+ cone/firebase/messaging.py,sha256=9_HJ5rd60EZr-6LgubMi1B_1i_P_k51XD5Xl4JAlyXM,1733
7
+ cone/firebase/tests.py,sha256=255kdmM2cwa8Rt2RCCtP316i_ipLnANLElnV0QQniY8,3830
8
+ cone/firebase/testing/__init__.py,sha256=GFML_Gqtfw73XlAkaBdtXNMz1kL_BhLeuBkv2ivZ9Uk,4838
9
+ cone/firebase/testing/firebase_admin/__init__.py,sha256=VaekqatvJ2iE1M0H0-mq8rREGf22847nMzYTzTDk72E,983
10
+ cone/firebase/testing/firebase_admin/credentials.py,sha256=dWr3nlUtuiFuGN4WaqocNvbxjuS5CSw5KHxwutSOHws,131
11
+ cone/firebase/testing/firebase_admin/messaging.py,sha256=l09gMD02BZvpxMrwaw37vhfZ4UWLfiFJq-lGFdbZVHI,4589
12
+ cone_firebase-0.1.dist-info/licenses/LICENSE.rst,sha256=zaJbEi2UOhNjFLC1d2qra_JVlZceavFtt9yOXoM37Ng,1368
13
+ cone_firebase-0.1.dist-info/METADATA,sha256=6Ghw_bY-Lm4UMjdIXZ0DYiSSQgRRllfhvHDG1kza8_0,4527
14
+ cone_firebase-0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ cone_firebase-0.1.dist-info/namespace_packages.txt,sha256=lu11VEz--UhjAtw-HG4CejJNFf-TZlq7z1O6wZ7Vrjk,5
16
+ cone_firebase-0.1.dist-info/top_level.txt,sha256=lu11VEz--UhjAtw-HG4CejJNFf-TZlq7z1O6wZ7Vrjk,5
17
+ cone_firebase-0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,27 @@
1
+ License
2
+ =======
3
+
4
+ Copyright (c) 2021, BlueDynamics Alliance, Austria
5
+ Copyright (c) 2021, Cone Contributors
6
+ All rights reserved.
7
+
8
+ Redistribution and use in source and binary forms, with or without
9
+ modification, are permitted provided that the following conditions are met:
10
+
11
+ * Redistributions of source code must retain the above copyright notice, this
12
+ list of conditions and the following disclaimer.
13
+
14
+ * Redistributions in binary form must reproduce the above copyright notice, this
15
+ list of conditions and the following disclaimer in the documentation and/or
16
+ other materials provided with the distribution.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ cone