diff --git a/README.md b/README.md
index bf9b703..f4c9129 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
# TU Wien Addressbook (unofficial)
+
An unofficial Android and iOS App to search [TU Wien](https://www.tuwien.at/en/) employees and students.
![Screenshot](screenshot.png)
@@ -9,129 +10,143 @@ An unofficial Android and iOS App to search [TU Wien](https://www.tuwien.at/en/)
API, the app itself is not official!
## Features
-* Search [TU Wien](https://www.tuwien.at/en/) employees and students wihout opening TISS
-* Filter search results
-* Optional login (needed to get student information)
-* Dark Mode
+
+- Search [TU Wien](https://www.tuwien.at/en/) employees and students wihout opening TISS
+- Search by matriculation number, or name
+- Filter search results
+- Optional login (needed to get student information)
+- Dark Mode
## Future Features
+
Things I should implement, but propably won't.
-* Find students by their mattriculation number
-* Support english
-* Indicate if the user is logged in while searching
-* Indicate that there are no student information without logging in
-* Improve Settings UI
+- Support english
+- Indicate if the user is logged in while searching
+- Indicate that there are no student information without logging in
+- Improve Settings UI
## Build it yourself
-1) [Install Flutter](https://flutter.dev/docs/get-started/install)
-2) Clone this repository or download it.
-3) Connect your phone to your computer.
-4) Open the repository in your terminal.
-5) For Android run: `flutter build apk --release && flutter install`
-For iOS you need to sign the App yourself to use it. Here is a
-[Guide](https://medium.com/front-end-weekly/how-to-test-your-flutter-ios-app-on-your-ios-device-75924bfd75a8)
-on how to do this.
+
+1. [Install Flutter](https://flutter.dev/docs/get-started/install)
+2. Clone this repository or download it.
+3. Connect your phone to your computer.
+4. Open the repository in your terminal.
+5. For Android run: `flutter build apk --release && flutter install`
+ For iOS you need to sign the App yourself to use it. Here is a
+ [Guide](https://medium.com/front-end-weekly/how-to-test-your-flutter-ios-app-on-your-ios-device-75924bfd75a8)
+ on how to do this.
## Frequently asked Question
### Why is the App in German?
-Many of the TISS API responses are in German, so in the spirit of consistency
+
+Many of the TISS API responses are in German, so in the spirit of consistency
I just designed the whole App in German.
### The search results are bad!
+
I know :pensive:
-There is just nothing I can do about it, because the App gets them from the TISS
+There is just nothing I can do about it, because the App gets them from the TISS
API.
### Why is the search so slow?
-Again, there is nothing I can do about it as the API response just take a long
-time. However, I figured out, the more specific you are, the faster the
+
+Again, there is nothing I can do about it as the API response just take a long
+time. However, I figured out, the more specific you are, the faster the
responses are.
### Where do you get the data from?
-Actually, there is an official
-[TISS REST API](https://tiss.tuwien.ac.at/api/dokumentation). While the API is
-public (or semi-public, but I will come back to this later), the documentation
+
+Actually, there is an official
+[TISS REST API](https://tiss.tuwien.ac.at/api/dokumentation). While the API is
+public (or semi-public, but I will come back to this later), the documentation
is not, so you need to be able to login to [TISS](https://tiss.tuwien.ac.at/).
-However, if you don't have access to [TISS](https://tiss.tuwien.ac.at/), don't worry as
-many aspects of the API are not documented anyway. For example, there is no
-documentation about what the response json looks like or that you can set
+However, if you don't have access to [TISS](https://tiss.tuwien.ac.at/), don't worry as
+many aspects of the API are not documented anyway. For example, there is no
+documentation about what the response json looks like or that you can set
the language via a query parameter or that you will only receive student
information if you are logged in.
To search people on the API, I use the `/api/person/v22/psuche` endpoint.
Here is a full example:
+
```
https://tiss.tuwien.ac.at/api/person/v22/psuche?q=Panholz&max_treffer=50
```
Here is a list of all query parameters:
-| Name | Type | Datatype | Description |
+| Name | Type | Datatype | Description |
|-------------|:-----:|---------:|----------------------------------------------------------------------------------------------------------------|
-| q | path | string | Searchterm |
-| intern | query | bool | If internal persons(like students) should be included. This only works if you are logged in. Default is false. |
-| max_treffer | query | int | Upper limit of results. Limit is 100, Default is 15. |
-| locale | query | string | The language of (some?) fields of the result. It uses 2 letter codes like "de" or "en". Default is ???? |
+| q | path | string | Searchterm |
+| intern | query | bool | If internal persons(like students) should be included. This only works if you are logged in. Default is false. |
+| max_treffer | query | int | Upper limit of results. Limit is 100, Default is 15. |
+| locale | query | string | The language of (some?) fields of the result. It uses 2 letter codes like "de" or "en". Default is ???? |
### How does the app log in, to get student information?
-So most sane developers, would design the authentication process of an API with an
-API-token or maybe just let you send the username and password with every
+
+So most sane developers, would design the authentication process of an API with an
+API-token or maybe just let you send the username and password with every
request. (API-token is the way better solution)
-Unfortunately, I couldn't find anything like that, so we will do it like the
-Webinterface with cookies, parsing HTML, parsing URLs, following redirects and
+Unfortunately, I couldn't find anything like that, so we will do it like the
+Webinterface with cookies, parsing HTML, parsing URLs, following redirects and
many more cookies.
Sounds fun? I guarantee you **IT IS NOT**!
#### STEP 0: Requirements
-Many requests will set you a cookie, so just save them and if you do a request
+
+Many requests will set you a cookie, so just save them and if you do a request
in the future to the same domain just send them with the request.
#### STEP 1: Create a session and collect the AuthState
-First start with a `GET` request to
+
+First start with a `GET` request to
`https://tiss.tuwien.ac.at/admin/authentifizierung`. The server will answer with
-the status 302 (Redirect). Now you follow multiple redirects (like 5 or 6)
+the status 302 (Redirect). Now you follow multiple redirects (like 5 or 6)
until the server finally answers with 200 (OK).
Now parse the URL on which you finally landed on, because in the URL should be
a query parameter called `AuthState` which we need to save for the next request.
#### STEP 2: Get the first login cookie and parsing HTML
-Next, we need to make a `POST` request to
+
+Next, we need to make a `POST` request to
`https://idp.zid.tuwien.ac.at/simplesaml/module.php/core/loginuserpass.php`.
The data we send with that request is form-encoded and there are the following
fields:
|Name|Content|
|--|--|
-| username | *your TU Wien username* |
-| password | *your TU Wien password* |
-| totp | *empty (This field is for Two Factor Authentication*|
-| AuthState | *the AuthState we collected in Step 1* |
+| username | _your TU Wien username_ |
+| password | _your TU Wien password_ |
+| totp | _empty (This field is for Two Factor Authentication_|
+| AuthState | _the AuthState we collected in Step 1_ |
The server should now respond with HTML. In this HTML is a Form with two hidden
fields: `SAMLResponse` and `RelayState`. You now need to parse the HTML and get
the content of both fields and save them.
#### Step 3: Get the second login cookie
+
Make a `POST` request to `https://login.tuwien.ac.at/auth/postResponse` with
the following form-encoded data:
|Name|Content|
|--|--|
-| SAMLResponse | *the SAMLResponse from Step 2* |
-| RelayState | *the RelayState from Step 2* |
+| SAMLResponse | _the SAMLResponse from Step 2_ |
+| RelayState | _the RelayState from Step 2_ |
The server will respond with status 303 (Redirect), save that url.
#### Step 4: Get the final TISS cookie
-With `GET` requests follow the redirects until the server responds with
+
+With `GET` requests follow the redirects until the server responds with
200 (OK), starting with the location we got at the end of Step 3.
-While following these redirects the server will eventually set you cookie called
-`TISS_AUTH`. This is the one you ~wanted~ needed all along. Now, all you have
-to do is make requests to the TISS API and send this cookie with them.
+While following these redirects the server will eventually set you cookie called
+`TISS_AUTH`. This is the one you ~wanted~ needed all along. Now, all you have
+to do is make requests to the TISS API and send this cookie with them.
🥳 🎉
diff --git a/lib/main.dart b/lib/main.dart
index 05e6f6b..037b56e 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -16,6 +16,7 @@ class App extends StatelessWidget {
title: 'TU Addressbuch',
locale: Locale('de', 'AT'),
theme: ThemeData(
+ brightness: Brightness.light,
accentColor: Colors.indigo[400],
primarySwatch: Colors.blueGrey,
scaffoldBackgroundColor: Colors.blueGrey[50],
@@ -94,73 +95,69 @@ class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SafeArea(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- //Padding(padding: EdgeInsets.only(top: 100)),
- Expanded(child: Container()),
- Center(
- child: Text("Suche", style: Theme.of(context).textTheme.headline3),
- ),
- Padding(
- padding: EdgeInsets.all(16),
- child: ElevatedButton(
- /*
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(100.0),
- ),
- color: Theme.of(context).cardColor,
- */
- onPressed: () {
- showSearch(context: context, delegate: PersonSearch());
- },
- child: Padding(
- padding: EdgeInsets.all(8),
- child: Row(
- children: [
- Icon(
- Icons.search,
- size: 24,
- ),
- Padding(
- padding: EdgeInsets.only(left: 10),
- ),
- Text(
- "Studierende, Angestellte",
- style: TextStyle(fontSize: 20),
- ),
- ],
- ),
+ child: Column(mainAxisSize: MainAxisSize.min, children: [
+ //Padding(padding: EdgeInsets.only(top: 100)),
+ Expanded(child: Container()),
+ Center(
+ child: Text("Suche", style: Theme.of(context).textTheme.headline3),
+ ),
+ Padding(
+ padding: EdgeInsets.all(16),
+ child: ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ shape: new RoundedRectangleBorder(
+ borderRadius: new BorderRadius.circular(30.0),
+ )),
+ onPressed: () {
+ showSearch(context: context, delegate: PersonSearch());
+ },
+ child: Padding(
+ padding: EdgeInsets.all(8),
+ child: Row(
+ children: [
+ Icon(
+ Icons.search,
+ size: 24,
+ ),
+ Padding(
+ padding: EdgeInsets.only(left: 10),
+ ),
+ Text(
+ "Studierende, Angestellte",
+ style: TextStyle(fontSize: 20),
+ ),
+ ],
),
),
),
- Expanded(
- child: Container(),
- flex: 2,
- ),
- Padding(
- padding: EdgeInsets.all(8),
- child: RichText(
- text: TextSpan(
- style: DefaultTextStyle.of(context).style.copyWith(
- fontSize: Theme.of(context).textTheme.caption!.fontSize),
- children: [
- TextSpan(
- text: "Made with ❤️ by ",
- ),
- TextSpan(
- text: "flofriday",
- //style: TextStyle(decoration: TextDecoration.underline),
- recognizer: TapGestureRecognizer()
- ..onTap = () {
- print("tap");
- launchInBrowser("https://github.com/flofriday");
- }),
- ]),
- ),
+ ),
+
+ Expanded(
+ child: Container(),
+ flex: 2,
+ ),
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: RichText(
+ text: TextSpan(
+ style: DefaultTextStyle.of(context).style.copyWith(
+ fontSize: Theme.of(context).textTheme.caption!.fontSize),
+ children: [
+ TextSpan(
+ text: "Made with ❤️ by ",
+ ),
+ TextSpan(
+ text: "flofriday",
+ //style: TextStyle(decoration: TextDecoration.underline),
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ print("tap");
+ launchInBrowser("https://github.com/flofriday");
+ }),
+ ]),
),
- ],
- ),
+ ),
+ ]),
);
}
}
diff --git a/lib/screens/person_search.dart b/lib/screens/person_search.dart
index 88bce3a..414b506 100644
--- a/lib/screens/person_search.dart
+++ b/lib/screens/person_search.dart
@@ -14,7 +14,7 @@ import 'package:http/http.dart' as http;
class PersonSearch extends SearchDelegate {
TissLoginManager _tissManager = TissLoginManager();
SuggestionManager _suggestionManager = SuggestionManager();
- Map _cache = Map();
+ Map> _cache = Map();
bool _employeeFilter = false;
bool _studentFilter = false;
bool _femaleFilter = false;
@@ -34,7 +34,8 @@ class PersonSearch extends SearchDelegate {
return emojis[(_rng.nextInt(emojis.length))];
}
- Future _makeRequest(String query) async {
+ /// Make a request to search for the name of a person
+ Future _makePersonRequest(String query) async {
// Get the Cookies
String cookies = await _tissManager.getCookies()!;
var headers = {"Cookie": cookies};
@@ -52,31 +53,77 @@ class PersonSearch extends SearchDelegate {
return res;
}
+ /// Make a request to search for a person by their matriculation number
+ Future _makeMnrRequest(String query) async {
+ // Get the Cookies
+ String cookies = await _tissManager.getCookies()!;
+ var headers = {"Cookie": cookies};
+
+ // Make the request
+ Uri apiSearchUri =
+ Uri.https("tiss.tuwien.ac.at", "/api/person/v22/mnr/$query", {
+ "preview_picture_uri": "true",
+ "intern": "true",
+ "locale": "de",
+ });
+ http.Response res = await http.get(apiSearchUri, headers: headers);
+ return res;
+ }
+
// Tries to download the results from TISS, returns null if the mission went
// sideways.
- Future _getData(String query) async {
+ Future?> _getData(String query) async {
// Check the cache
if (_cache.containsKey(query)) return _cache[query];
// Since we get new data now, we should reset the position of the scroll
_scrollOffset = 0;
- // Make the request
- http.Response resp = await _makeRequest(query);
- if (resp.statusCode != 200) return null;
+ // Check if the query is a matriculation number
+ RegExp mrnRegex = RegExp("[01]?[0-9]{7}");
+ if (mrnRegex.hasMatch(query)) {
+ // Load by matriculation number
+
+ // Make the request
+ http.Response resp = await _makeMnrRequest(query);
+ if (resp.statusCode == 404) return [];
+ if (resp.statusCode != 200) return null;
+
+ // Parse the json
+ var map = jsonDecode(resp.body);
+ Person data = Person.fromJson(map);
- // Parse the json
- var map = jsonDecode(resp.body);
- Tiss data = Tiss.fromJson(map);
+ // Add the data to the cache and return
+ _cache[query] = [data];
+ return [data];
+ } else {
+ // Load by name
- // Add the data to the cache and return
- _cache[query] = data;
- return data;
+ // Make the request
+ http.Response resp = await _makePersonRequest(query);
+ if (resp.statusCode != 200) return null;
+
+ // Parse the json
+ var map = jsonDecode(resp.body);
+ Tiss data = Tiss.fromJson(map);
+
+ // Add the data to the cache and return
+ _cache[query] = data.results;
+ return data.results;
+ }
}
@override
ThemeData appBarTheme(BuildContext context) {
- return Theme.of(context);
+ final ThemeData theme = Theme.of(context);
+ if (theme.brightness == Brightness.dark) return theme;
+
+ return theme.copyWith(
+ primaryColor: theme.cardColor,
+ primaryIconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey),
+ primaryColorBrightness: MediaQuery.of(context).platformBrightness,
+ primaryTextTheme: theme.textTheme,
+ );
}
@override
@@ -104,14 +151,15 @@ class PersonSearch extends SearchDelegate {
@override
Widget buildResults(BuildContext context) {
- Future data = _getData(query);
+ Future?> data = _getData(query);
_suggestionManager.addSuggestion(query);
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
- return FutureBuilder(
+ return FutureBuilder?>(
future: data,
- builder: (BuildContext context, AsyncSnapshot snapshot) {
+ builder:
+ (BuildContext context, AsyncSnapshot?> snapshot) {
// Check for errors
if (snapshot.hasError) {
return SafeArea(
@@ -149,7 +197,7 @@ class PersonSearch extends SearchDelegate {
}
// Check if the server answered successfully
- Tiss? data = snapshot.data;
+ List? data = snapshot.data;
if (data == null) {
return Center(
child:
@@ -157,7 +205,7 @@ class PersonSearch extends SearchDelegate {
}
// Show a error if there are no results
- if (data.results.length == 0) {
+ if (data.length == 0) {
return SafeArea(
child: Center(
child: Column(
@@ -184,7 +232,7 @@ class PersonSearch extends SearchDelegate {
}
// Filter the results
- Iterable unfiltered = data.results;
+ Iterable unfiltered = data;
if (_studentFilter) {
unfiltered = unfiltered.where((Person p) => p.student != null);
}
diff --git a/lib/widgets/person_card.dart b/lib/widgets/person_card.dart
index a34906f..b1f298c 100644
--- a/lib/widgets/person_card.dart
+++ b/lib/widgets/person_card.dart
@@ -129,23 +129,22 @@ class PersonInfoCard extends StatelessWidget {
},
),
),
- if (person.tissUri != null)
- Expanded(
- child: TextButton(
- child: Column(
- children: [
- Icon(Icons.school),
- Text(
- "TISS",
- style: Theme.of(context).textTheme.caption,
- ),
- ],
- ),
- onPressed: () {
- launchInBrowser(person.getTissUrl());
- },
+ Expanded(
+ child: TextButton(
+ child: Column(
+ children: [
+ Icon(Icons.school),
+ Text(
+ "TISS",
+ style: Theme.of(context).textTheme.caption,
+ ),
+ ],
),
+ onPressed: () {
+ launchInBrowser(person.getTissUrl());
+ },
),
+ ),
]),
),
],