├── MANIFEST.in
├── funkapi
├── __init__.py
├── graphql_schema.py
└── api.py
├── .github
└── FUNDING.yml
├── .gitignore
├── requirements.txt
├── setup.py
├── LICENSE.md
└── README.md
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.txt *.md
2 |
--------------------------------------------------------------------------------
/funkapi/__init__.py:
--------------------------------------------------------------------------------
1 | from funkapi.api import FunkAPI
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: ['lagmoellertim']
4 | ko_fi: lagmoellertim
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | \.idea/
3 | venv/
4 | dist/
5 | build/
6 | funkapi\.egg-info/
7 |
8 | test\.py
9 |
10 | funkapi/__pycache__/
11 | freenet_funk_api\.egg-info/
12 | \.vscode/
13 |
14 | freenet_funk_api\.egg-info/
15 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | boto3==1.9.145
2 | botocore==1.12.145
3 | certifi==2022.12.7
4 | chardet==3.0.4
5 | docutils==0.14
6 | ecdsa==0.13.3
7 | envs==1.3
8 | future==0.17.1
9 | idna==2.8
10 | jmespath==0.9.4
11 | pycryptodome==3.8.1
12 | python-dateutil==2.8.0
13 | python-jose-cryptodome==1.3.2
14 | requests==2.21.0
15 | s3transfer==0.2.0
16 | six==1.12.0
17 | urllib3==1.26.5
18 | warrant==0.6.1
19 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as f:
4 | long_description = f.read()
5 |
6 | with open('requirements.txt') as f:
7 | requirements = f.read().splitlines()
8 |
9 | setuptools.setup(
10 | name='funkapi',
11 | version='0.1.7',
12 | install_requires=requirements,
13 | license='MIT License',
14 | author='Tim-Luca Lagmöller',
15 | author_email='hello@lagmoellertim.de',
16 | description='Reverse engineered API of Freenet FUNK',
17 | long_description=long_description,
18 | long_description_content_type="text/markdown",
19 | url='https://github.com/lagmoellertim/freenet-funk-api',
20 | packages=setuptools.find_packages(),
21 | classifiers=[
22 | "Programming Language :: Python :: 3",
23 | "License :: OSI Approved :: MIT License",
24 | "Operating System :: OS Independent",
25 | ],
26 | )
27 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 |
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2019 Tim-Luca Lagmöller
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Freenet FUNK API
2 |
3 | ## Archived
4 |
5 | Since I don't use freenet funk anymore, I am not able to maintain this api any longer. If you want to become the new maintainer, feel free to fork this repo.
6 |
7 | ## Introduction
8 |
9 | Freenet FUNK is cellphone plan that offers **unlimited** (or 1 GB of) 4G data. The plan is can be started, stopped and paused **daily**.
10 |
11 | To make the most out of this flexibility, I reverse engineered the **API** to give anyone the ability to develop amazing apps on their own!
12 |
13 | ## Prerequisites
14 |
15 | - Python >=3.2
16 | - pip3 (or just pip on windows)
17 |
18 | ## Installation
19 | ### Install from pypi
20 | ```sh
21 | pip3 install funkapi
22 | ```
23 |
24 | ### Install from source
25 | ```sh
26 | git clone https://github.com/lagmoellertim/freenet-funk-api.git
27 |
28 | cd freenet-funk-api
29 |
30 | pip3 install -r requirements.txt
31 |
32 | python3 setup.py install
33 | ```
34 |
35 | ## Build
36 |
37 | ```sh
38 | git clone https://github.com/lagmoellertim/freenet-funk-api.git
39 |
40 | cd freenet-funk-api
41 |
42 | pip3 install -r requirements.txt
43 |
44 | python3 setup.py sdist bdist_wheel
45 | ```
46 |
47 | ## Usage
48 |
49 | ### Initialize the API
50 |
51 | ```python3
52 | from funkapi import FunkAPI
53 | api = FunkAPI("*username*", "*password*")
54 | ```
55 |
56 | ### Get a Token (not really necessary to do that manually but I left the option)
57 |
58 | ```python3
59 | token = api.getToken()
60 | ```
61 |
62 | ### Initialize the API with a predefined Token
63 |
64 | ```python3
65 | from funkapi import FunkAPI
66 | api = FunkAPI("", "", token="*token*")
67 | ```
68 |
69 | ### Check the validity of a Token (also not really necessary to do manually)
70 |
71 | ```python3
72 | isValid = api.testToken("*token*")
73 | ```
74 |
75 | ### Get Dashboard Data (includes every piece of data FUNK stores of you)
76 |
77 | ```python3
78 | data = api.getData()
79 | ```
80 |
81 | ### Get Personal Information (email, name, birthday, …)
82 |
83 | ```python3
84 | personalInfo = api.getPersonalInfo()
85 | ```
86 |
87 | ### Get a List of your ordered Products
88 |
89 | ```python3
90 | products = api.getOrderedProducts()
91 | ```
92 |
93 | ### Get the currently active Plan
94 |
95 | ```python3
96 | currentPlan = api.getCurrentPlan()
97 | ```
98 |
99 | ### Order the 1GB Plan
100 |
101 | ```python3
102 | status = api.order1GBPlan()
103 | ```
104 |
105 | ### Order the unlimited Plan
106 |
107 | ```python3
108 | status = api.orderUnlimitedPlan()
109 | ```
110 |
111 | ### Start a Pause
112 |
113 | ```python3
114 | status = api.startPause()
115 | ```
116 |
117 | ### Stop the latest Product (includes stopping a break)
118 |
119 | ```python3
120 | status = api.stopLatestPlan()
121 | ```
122 |
123 | ### Documentation
124 |
125 | If you get stuck at some point while trying to use the API, take a look the code. It is fully commented and well-labeled,
126 | so that should help you understand what's going on.
127 |
128 | ## Contributing
129 |
130 | If you are missing a feature or have new idea, go for it! That is what open-source is for!
131 |
132 | ## Author
133 |
134 | **Tim-Luca Lagmöller** ([@lagmoellertim](https://github.com/lagmoellertim))
135 |
136 | ## Donations / Sponsors
137 |
138 | I'm part of the official GitHub Sponsors program where you can support me on a monthly basis.
139 |
140 |
141 |
142 | You can also contribute by buying me a coffee (this is a one-time donation).
143 |
144 |
145 |
146 | Thank you for your support!
147 |
148 | ## License
149 |
150 | [MIT License](https://github.com/lagmoellertim/cryption/blob/master/LICENSE)
151 |
152 | Copyright © 2019-present, [Tim-Luca Lagmöller](https://en.lagmoellertim.de)
153 |
154 | ## Have fun :tada:
155 |
--------------------------------------------------------------------------------
/funkapi/graphql_schema.py:
--------------------------------------------------------------------------------
1 | schema = {
2 | "get_data":"""
3 | query CustomerForDashboardQuery {
4 | me {
5 | ...CustomerForDashboardFragment
6 | __typename
7 | }
8 | }
9 |
10 | fragment CustomerForDashboardFragment on Customer {
11 | id
12 | details {
13 | ...DetailsFragment
14 | __typename
15 | }
16 | customerProducts {
17 | ...ProductFragment
18 | __typename
19 | }
20 | __typename
21 | }
22 |
23 | fragment DetailsFragment on Details {
24 | firstName
25 | lastName
26 | dateOfBirth
27 | contactEmail
28 | __typename
29 | }
30 |
31 | fragment ProductFragment on FUNKCustomerProduct {
32 | id
33 | state
34 | paymentMethods {
35 | ...PaymentMethodFragment
36 | __typename
37 | }
38 | mobileNumbers {
39 | ...MobileNumberFragment
40 | __typename
41 | }
42 | sims {
43 | ...SIMFragment
44 | __typename
45 | }
46 | tariffs: tariffCustomerProductServices {
47 | ...TariffFragment
48 | __typename
49 | }
50 | __typename
51 | }
52 |
53 | fragment PaymentMethodFragment on PaymentMethod {
54 | id
55 | state
56 | approvalChallenge {
57 | approvalURL
58 | __typename
59 | }
60 | agreement {
61 | state
62 | payerInfo {
63 | payerID
64 | email
65 | __typename
66 | }
67 | __typename
68 | }
69 | __typename
70 | }
71 |
72 | fragment MobileNumberFragment on MobileNumberCPS {
73 | id
74 | number
75 | state
76 | usage {
77 | usedDataPercentage
78 | __typename
79 | }
80 | productServiceId
81 | productServiceInfo {
82 | id
83 | label
84 | __typename
85 | }
86 | ... on MNPImportCustomerProductService {
87 | otherProviderShortcut
88 | otherProviderCustomName
89 | otherContract {
90 | contractType
91 | mobileNumber
92 | mobileNumberIsVerified
93 | __typename
94 | }
95 | mnpInfos {
96 | confirmedPortingDate
97 | lastPortingResult
98 | problemCode
99 | problemReason
100 | __typename
101 | }
102 | __typename
103 | }
104 | __typename
105 | }
106 |
107 | fragment SIMFragment on SIMCustomerProductService {
108 | id
109 | networkState
110 | state
111 | iccid
112 | delivery {
113 | state
114 | trackingDetails {
115 | stateId
116 | stateLabel
117 | trackingURL
118 | __typename
119 | }
120 | deliveryProvider
121 | address {
122 | city
123 | additionalInfo
124 | __typename
125 | }
126 | __typename
127 | }
128 | __typename
129 | }
130 |
131 | fragment TariffFragment on TariffCustomerProductService {
132 | id
133 | booked
134 | starts
135 | state
136 | productServiceId
137 | productServiceInfo {
138 | id
139 | label
140 | follower {
141 | id
142 | label
143 | __typename
144 | }
145 | marketingInfo {
146 | name
147 | __typename
148 | }
149 | __typename
150 | }
151 | __typename
152 | }
153 | """,
154 | "order_plan":"""
155 | mutation AddTariffToProductMutation($productID: String!, $tariffID: String!) {
156 | tariffAddToCustomerProduct(customerProductId: $productID, productServiceId: $tariffID) {
157 | ...TariffFragment
158 | __typename
159 | }
160 | }
161 |
162 | fragment TariffFragment on TariffCustomerProductService {
163 | id
164 | booked
165 | starts
166 | state
167 | productServiceId
168 | productServiceInfo {
169 | id
170 | label
171 | follower {
172 | id
173 | label
174 | __typename
175 | }
176 | marketingInfo {
177 | name
178 | __typename
179 | }
180 | __typename
181 | }
182 | __typename
183 | }
184 | """,
185 | "remove_product":"""
186 | mutation TerminateTariffMutation($tariffID: String!) {
187 | tariffTerminate(customerProductServiceId: $tariffID) {
188 | ...TariffFragment
189 | __typename
190 | }
191 | }
192 |
193 | fragment TariffFragment on TariffCustomerProductService {
194 | id
195 | booked
196 | starts
197 | state
198 | productServiceId
199 | productServiceInfo {
200 | id
201 | label
202 | follower {
203 | id
204 | label
205 | __typename
206 | }
207 | marketingInfo {
208 | name
209 | __typename
210 | }
211 | __typename
212 | }
213 | __typename
214 | }
215 | """
216 | }
217 |
--------------------------------------------------------------------------------
/funkapi/api.py:
--------------------------------------------------------------------------------
1 | import boto3
2 | import requests
3 | import datetime
4 | from warrant.aws_srp import AWSSRP
5 |
6 | from funkapi.graphql_schema import schema
7 |
8 | class FunkAPI:
9 | """
10 | Freenet FUNK API helper library
11 | """
12 |
13 | API_ENDPOINT = "https://appapi.funk.services/"
14 | API_KEY = "FZ3OkFFfdMahh4a1xagOnaon39pUpml732kkb2Aw"
15 | AWS_REGION = "eu-central-1"
16 | AWS_POOL_ID = "eu-central-1_ZPDpzBJy4"
17 | AWS_CLIENT_ID = "3asd34f9vfrg6pd2mrbqhn3g3r"
18 |
19 | def __init__(self, username, password, token=None, always_test_token=False,
20 | ignore_token_check=False, ignore_token_retry=True, autoload_data=True):
21 | """
22 | Initialise API.
23 |
24 | :param username: Account E-Mail
25 | :param password: Account Password
26 | :param token: AWS Cognito JWT token
27 | :param always_test_token: Whether the validity of a token should be tested after each request
28 | :param ignore_token_check: Whether a token should be checked at all (when supplied via the token param)
29 | :param ignore_token_retry: Whether to stop using the user/pass params if the supplied token is wrong
30 | :param autoload_data: Whether to automatically load the data on initialization
31 | """
32 |
33 | self.username = username
34 | self.password = password
35 | self.always_test_token = always_test_token
36 | self.ignore_token_check = ignore_token_check
37 | self.ignore_token_retry = ignore_token_retry
38 |
39 | self.client = boto3.client('cognito-idp', region_name=self.AWS_REGION,
40 | aws_access_key_id="", aws_secret_access_key="")
41 |
42 | self.aws = AWSSRP(username=self.username, password=self.password,
43 | pool_id=self.AWS_POOL_ID,
44 | client_id=self.AWS_CLIENT_ID, client=self.client)
45 |
46 | self.token = None
47 | self.getToken(token=token)
48 |
49 | self.data = None
50 | if autoload_data:
51 | self.getData()
52 |
53 | def apiRequest(self, json, token=None):
54 | """
55 | Trigger API endpoint and request data.
56 |
57 | :param json: graphQL request schema
58 | :param token: AWS cognito JWT token
59 | :return: API response
60 | """
61 |
62 | token = token if token is not None else self.getToken()
63 |
64 | req = requests.post(self.API_ENDPOINT, json=json,
65 | headers={
66 | "x-api-key": self.API_KEY,
67 | "Authorization": "Bearer " + token,
68 | "apollographql-client-version": "1.0.1 (1143)",
69 | "apollographql-client-name": "freenet FUNK iOS"
70 | })
71 | return req.json()
72 |
73 | def getToken(self, refresh=False, token=None):
74 | """
75 | Returns a valid aws cognito JWT token.
76 |
77 | :param refresh: Disallow caching
78 | :param token: AWS cognito JWT token
79 | :return: AWS cognito JWT token
80 | """
81 |
82 | if token is not None:
83 | if self.testToken(token) or self.ignore_token_retry:
84 | self.token = token
85 | return self.token
86 |
87 | self.getToken(refresh=True)
88 |
89 | if self.token is None or refresh or (
90 | False if not self.always_test_token else not self.testToken(self.token)):
91 | self.token = self.aws.authenticate_user()["AuthenticationResult"]["AccessToken"]
92 |
93 | return self.token
94 |
95 | def testToken(self, token):
96 | """
97 | Test validity of authorization token.
98 |
99 | :param token: AWS cognito JWT token
100 | :return: Boolean whether the token is valid (True) or not (False)
101 | """
102 |
103 | if token is None:
104 | return False
105 |
106 | if not self.ignore_token_check:
107 | json = {"operationName": "CustomerForDashboardQuery", "variables": {},
108 | "query": "query CustomerForDashboardQuery { me { id } }"}
109 |
110 | result = self.apiRequest(json, token=token)
111 |
112 | if "errors" in result.keys():
113 | return False
114 |
115 | return True
116 |
117 | def getData(self, refresh=False):
118 | """
119 | Get data from endpoint.
120 |
121 | :param refresh: Ignore cache / Force Refresh
122 | :return: Data
123 | """
124 |
125 | json = {"operationName": "CustomerForDashboardQuery", "variables": {},
126 | "query": schema["get_data"]}
127 | if self.data is None or refresh:
128 | self.data = self.apiRequest(json)
129 |
130 | return self.data
131 |
132 | def getPersonalInfo(self, refresh_data=False):
133 | """
134 | Get personal data.
135 |
136 | :param refresh_data: Ignore cache / Force Refresh
137 | :return: Personal data
138 | """
139 |
140 | data = self.getData(refresh=refresh_data)["data"]["me"]
141 | personal_info = {"id": data["id"], **data["details"]}
142 | del personal_info["__typename"]
143 |
144 | return personal_info
145 |
146 | def getOrderedProducts(self, refresh_data=False):
147 | """
148 | Get ordered products (e.g. sim cards).
149 |
150 | :param refresh_data: Ignore cache / Force Refresh
151 | :return: A list of all ordered Products
152 | """
153 |
154 | return self.getData(refresh=refresh_data)["data"]["me"]["customerProducts"]
155 |
156 | def getCurrentPlan(self, refresh_data=False):
157 | """
158 | Get current plan.
159 |
160 | :param refresh_data: Ignore cache / Force Refresh
161 | :return: Current plan
162 | """
163 |
164 | now = datetime.datetime.now(datetime.timezone.utc)
165 | currentPlan = None
166 | for plan in self.getData(refresh=refresh_data)["data"]["me"]["customerProducts"][0]["tariffs"]:
167 | planStart = datetime.datetime.strptime(plan["starts"], "%Y-%m-%dT%H:%M:%S.%f%z")
168 | if planStart > now:
169 | continue
170 | currentPlan = plan
171 |
172 | return currentPlan
173 |
174 | def orderPlan(self, plan_id, product_id=None, refresh_data=True):
175 | """
176 | Order a plan.
177 |
178 | :param plan_id: Id of plan which should be ordered
179 | :param product_id: The sim card id the plan should be applied to
180 | :param refresh_data: Ignore cache / Force Refresh
181 | :return: Result of order
182 | """
183 |
184 | if product_id is None:
185 | product_id = self.getOrderedProducts()[0]["id"]
186 |
187 | json = {"operationName": "AddTariffToProductMutation",
188 | "variables": {"productID": product_id, "tariffID": str(plan_id)},
189 | "query": schema["order_plan"]}
190 | result = self.apiRequest(json)
191 |
192 | self.getData(refresh=refresh_data)
193 |
194 | return result
195 |
196 | def removeProduct(self, personal_plan_id, refresh_data=True):
197 | """
198 | Remove/Deactivate an active plan.
199 |
200 | :param personal_plan_id: Id of current plan
201 | :param refresh_data: Ignore cache / Force Refresh
202 | :return: Result of removal
203 | """
204 |
205 | json = {"operationName": "TerminateTariffMutation",
206 | "variables": {"tariffID": personal_plan_id},
207 | "query": schema["remove_product"]}
208 |
209 | result = self.apiRequest(json)
210 |
211 | self.getData(refresh=refresh_data)
212 |
213 | return result
214 |
215 | def order1GBPlan(self, **kwargs):
216 | """
217 | Order 1GB plan.
218 |
219 | :param kwargs: More details at FunkAPI.orderPlan
220 | :return: Result of order
221 | """
222 |
223 | return self.orderPlan(9, **kwargs)
224 |
225 | def orderUnlimitedPlan(self, **kwargs):
226 | """
227 | Order unlimited plan.
228 |
229 | :param kwargs: More details at FunkAPI.orderPlan
230 | :return: Result of order
231 | """
232 |
233 | return self.orderPlan(8, **kwargs)
234 |
235 | def startPause(self, **kwargs):
236 | """
237 | Start pause mode.
238 |
239 | :param kwargs: More details at FunkAPI.orderPlan
240 | :return: Result of order
241 | """
242 |
243 | return self.orderPlan(42, **kwargs)
244 |
245 | def stopLatestPlan(self, product_index=0, **kwargs):
246 | """
247 | Stop current plan.
248 |
249 | :param product_index: The sim card id of which the plan should be stopped
250 | :param kwargs: More details at FunkAPI.removeProduct
251 | :return: Result of order
252 | """
253 |
254 | personal_plan_id = self.getOrderedProducts()[product_index]["tariffs"][-1]["id"]
255 | return self.removeProduct(personal_plan_id, **kwargs)
256 |
--------------------------------------------------------------------------------