File: app.py

package info (click to toggle)
python-django-push-notifications 1.6.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 352 kB
  • sloc: python: 2,694; makefile: 4
file content (313 lines) | stat: -rw-r--r-- 10,547 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
from django.core.exceptions import ImproperlyConfigured
from django.utils.six import string_types
from .base import BaseConfig, check_apns_certificate
from ..settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS

SETTING_MISMATCH = (
	"Application '{application_id}' ({platform}) does not support the setting '{setting}'."
)


# code can be "missing" or "invalid"
BAD_PLATFORM = (
	"PUSH_NOTIFICATIONS_SETTINGS.APPLICATIONS['{application_id}']['PLATFORM']"
	" is {code}. Must be one of: {platforms}."
)

UNKNOWN_PLATFORM = (
	"Unknown Platform: {platform}. Must be one of: {platforms}."
)

MISSING_SETTING = (
	"PUSH_NOTIFICATIONS_SETTINGS.APPLICATIONS['{application_id}']['{setting}'] is missing."
)

PLATFORMS = [
	"APNS",
	"FCM",
	"GCM",
	"WNS",
	"WP"
]

# Settings that all applications must have
REQUIRED_SETTINGS = [
	"PLATFORM",
]

# Settings that an application may have to enable optional features
# these settings are stubs for registry support and have no effect on the operation
# of the application at this time.
OPTIONAL_SETTINGS = [
	"APPLICATION_GROUP",
	"APPLICATION_SECRET",
]

APNS_REQUIRED_SETTINGS = ["CERTIFICATE"]

APNS_OPTIONAL_SETTINGS = [
	"USE_SANDBOX", "USE_ALTERNATIVE_PORT", "TOPIC"
]

FCM_REQUIRED_SETTINGS = GCM_REQUIRED_SETTINGS = ["API_KEY"]
FCM_OPTIONAL_SETTINGS = GCM_OPTIONAL_SETTINGS = [
	"POST_URL", "MAX_RECIPIENTS", "ERROR_TIMEOUT"
]

WNS_REQUIRED_SETTINGS = ["PACKAGE_SECURITY_ID", "SECRET_KEY"]

WNS_OPTIONAL_SETTINGS = ["WNS_ACCESS_URL"]

WP_REQUIRED_SETTINGS = ["PRIVATE_KEY", "CLAIMS"]

WP_OPTIONAL_SETTINGS = ["ERROR_TIMEOUT", "POST_URL"]


class AppConfig(BaseConfig):
	"""Supports any number of push notification enabled applications."""

	def __init__(self, settings=None):
		# supports overriding the settings to be loaded. Will load from ..settings by default.
		self._settings = settings or SETTINGS

		# initialize APPLICATIONS to an empty collection
		self._settings.setdefault("APPLICATIONS", {})

		# validate application configurations
		self._validate_applications(self._settings["APPLICATIONS"])

	def _validate_applications(self, apps):
		"""Validate the application collection"""
		for application_id, application_config in apps.items():
			self._validate_config(application_id, application_config)

			application_config["APPLICATION_ID"] = application_id

	def _validate_config(self, application_id, application_config):
		platform = application_config.get("PLATFORM", None)

		# platform is not present
		if platform is None:
			raise ImproperlyConfigured(
				BAD_PLATFORM.format(
					application_id=application_id,
					code="required",
					platforms=", ".join(PLATFORMS)
				)
			)

		# platform is not a valid choice from PLATFORMS
		if platform not in PLATFORMS:
			raise ImproperlyConfigured(
				BAD_PLATFORM.format(
					application_id=application_id,
					code="invalid",
					platforms=", ".join(PLATFORMS)
				)
			)

		validate_fn = "_validate_{platform}_config".format(platform=platform).lower()

		if hasattr(self, validate_fn):
			getattr(self, validate_fn)(application_id, application_config)
		else:
			raise ImproperlyConfigured(
				UNKNOWN_PLATFORM.format(
					platform=platform,
					platforms=", ".join(PLATFORMS)
				)
			)

	def _validate_apns_config(self, application_id, application_config):
		allowed = REQUIRED_SETTINGS + OPTIONAL_SETTINGS + APNS_REQUIRED_SETTINGS + \
			APNS_OPTIONAL_SETTINGS

		self._validate_allowed_settings(application_id, application_config, allowed)
		self._validate_required_settings(
			application_id, application_config, APNS_REQUIRED_SETTINGS
		)

		# determine/set optional values
		application_config.setdefault("USE_SANDBOX", False)
		application_config.setdefault("USE_ALTERNATIVE_PORT", False)
		application_config.setdefault("TOPIC", None)

		self._validate_apns_certificate(application_config["CERTIFICATE"])

	def _validate_apns_certificate(self, certfile):
		"""Validate the APNS certificate at startup."""

		try:
			with open(certfile, "r") as f:
				content = f.read()
				check_apns_certificate(content)
		except Exception as e:
			msg = "The APNS certificate file at %r is not readable: %s" % (certfile, e)
			raise ImproperlyConfigured(msg)

	def _validate_fcm_config(self, application_id, application_config):
		allowed = REQUIRED_SETTINGS + OPTIONAL_SETTINGS + FCM_REQUIRED_SETTINGS + \
			FCM_OPTIONAL_SETTINGS

		self._validate_allowed_settings(application_id, application_config, allowed)
		self._validate_required_settings(
			application_id, application_config, FCM_REQUIRED_SETTINGS
		)

		application_config.setdefault("POST_URL", "https://fcm.googleapis.com/fcm/send")
		application_config.setdefault("MAX_RECIPIENTS", 1000)
		application_config.setdefault("ERROR_TIMEOUT", None)

	def _validate_gcm_config(self, application_id, application_config):
		allowed = REQUIRED_SETTINGS + OPTIONAL_SETTINGS + GCM_REQUIRED_SETTINGS + \
			GCM_OPTIONAL_SETTINGS

		self._validate_allowed_settings(application_id, application_config, allowed)
		self._validate_required_settings(
			application_id, application_config, GCM_REQUIRED_SETTINGS
		)

		application_config.setdefault("POST_URL", "https://android.googleapis.com/gcm/send")
		application_config.setdefault("MAX_RECIPIENTS", 1000)
		application_config.setdefault("ERROR_TIMEOUT", None)

	def _validate_wns_config(self, application_id, application_config):
		allowed = REQUIRED_SETTINGS + OPTIONAL_SETTINGS + WNS_REQUIRED_SETTINGS + \
			WNS_OPTIONAL_SETTINGS

		self._validate_allowed_settings(application_id, application_config, allowed)
		self._validate_required_settings(
			application_id, application_config, WNS_REQUIRED_SETTINGS
		)

		application_config.setdefault("WNS_ACCESS_URL", "https://login.live.com/accesstoken.srf")

	def _validate_wp_config(self, application_id, application_config):
		allowed = REQUIRED_SETTINGS + OPTIONAL_SETTINGS + WP_REQUIRED_SETTINGS + \
			WP_OPTIONAL_SETTINGS

		self._validate_allowed_settings(application_id, application_config, allowed)
		self._validate_required_settings(
			application_id, application_config, WP_REQUIRED_SETTINGS
		)
		application_config.setdefault("POST_URL", {
			"CHROME": 'https://fcm.googleapis.com/fcm/send',
			"OPERA": 'https://fcm.googleapis.com/fcm/send',
			"FIREFOX": 'https://updates.push.services.mozilla.com/wpush/v2'
		})


	def _validate_allowed_settings(self, application_id, application_config, allowed_settings):
		"""Confirm only allowed settings are present."""

		for setting_key in application_config.keys():
			if setting_key not in allowed_settings:
				raise ImproperlyConfigured(
					"Platform {}, app {} does not support the setting: {}.".format(
						application_config["PLATFORM"], application_id, setting_key
					)
				)

	def _validate_required_settings(
		self, application_id, application_config, required_settings
	):
		"""All required keys must be present"""

		for setting_key in required_settings:
			if setting_key not in application_config.keys():
				raise ImproperlyConfigured(
					MISSING_SETTING.format(
						application_id=application_id, setting=setting_key
					)
				)

	def _get_application_settings(self, application_id, platform, settings_key):
		"""Walks through PUSH_NOTIFICATIONS_SETTINGS to find the correct setting
		value or dies trying"""

		if not application_id:
			conf_cls = "push_notifications.conf.AppConfig"
			raise ImproperlyConfigured(
				"{} requires the application_id be specified at all times.".format(conf_cls)
			)

		# verify that the application config exists
		app_config = self._settings.get("APPLICATIONS").get(application_id, None)
		if app_config is None:
			raise ImproperlyConfigured(
				"No application configured with application_id: {}.".format(application_id)
			)

		# fetch a setting for the incorrect type of platform
		if app_config.get("PLATFORM") != platform:
			raise ImproperlyConfigured(
				SETTING_MISMATCH.format(
					application_id=application_id,
					platform=app_config.get("PLATFORM"),
					setting=settings_key
				)
			)

		# finally, try to fetch the setting
		if settings_key not in app_config:
			raise ImproperlyConfigured(
				MISSING_SETTING.format(
					application_id=application_id, setting=settings_key
				)
			)

		return app_config.get(settings_key)

	def get_gcm_api_key(self, application_id=None):
		return self._get_application_settings(application_id, "GCM", "API_KEY")

	def get_fcm_api_key(self, application_id=None):
		return self._get_application_settings(application_id, "FCM", "API_KEY")

	def get_post_url(self, cloud_type, application_id=None):
		return self._get_application_settings(application_id, cloud_type, "POST_URL")

	def get_error_timeout(self, cloud_type, application_id=None):
		return self._get_application_settings(application_id, cloud_type, "ERROR_TIMEOUT")

	def get_max_recipients(self, cloud_type, application_id=None):
		return self._get_application_settings(application_id, cloud_type, "MAX_RECIPIENTS")

	def get_apns_certificate(self, application_id=None):
		r = self._get_application_settings(application_id, "APNS", "CERTIFICATE")
		if not isinstance(r, string_types):
			# probably the (Django) file, and file path should be got
			if hasattr(r, "path"):
				return r.path
			elif (hasattr(r, "has_key") or hasattr(r, "__contains__")) and "path" in r:
				return r["path"]
			else:
				raise ImproperlyConfigured(
					"The APNS certificate settings value should be a string, or "
					"should have a 'path' attribute or key"
				)
		return r

	def get_apns_use_sandbox(self, application_id=None):
		return self._get_application_settings(application_id, "APNS", "USE_SANDBOX")

	def get_apns_use_alternative_port(self, application_id=None):
		return self._get_application_settings(application_id, "APNS", "USE_ALTERNATIVE_PORT")

	def get_apns_topic(self, application_id=None):
		return self._get_application_settings(application_id, "APNS", "TOPIC")

	def get_wns_package_security_id(self, application_id=None):
		return self._get_application_settings(application_id, "WNS", "PACKAGE_SECURITY_ID")

	def get_wns_secret_key(self, application_id=None):
		return self._get_application_settings(application_id, "WNS", "SECRET_KEY")

	def get_wp_post_url(self, application_id, browser):
		return self._get_application_settings(application_id, "WP", "POST_URL")[browser]

	def get_wp_private_key(self, application_id=None):
		return self._get_application_settings(application_id, "WP", "PRIVATE_KEY")

	def get_wp_claims(self, application_id=None):
		return self._get_application_settings(application_id, "WP", "CLAIMS")