diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml
index 73aa948..1a337d6 100644
--- a/.github/workflows/documentation.yaml
+++ b/.github/workflows/documentation.yaml
@@ -3,49 +3,46 @@ on:
push:
branches:
- master
- - workflows
+ - mobile_client*
+ workflow_dispatch:
jobs:
- DeployBackDocumentation:
- runs-on: ubuntu-20.04
+ DeployDocumentation:
+ name: "Generate ${{ matrix.run_name }}'s documentation"
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - project_dir: mobile
+ run_name: 'Mobile client'
+ doc_dir: 'doc/api'
+ use: 'subosito/flutter-action@v1.5.3'
+ warm_up: flutter pub global activate dartdoc; rm -rf doc ; flutter gen-l10n
+ doc_generation: dartdoc;
+ defaults:
+ run:
+ working-directory: ${{ matrix.project_dir }}
steps:
- uses: actions/checkout@v2.3.4
- - name: Install stack
- run: curl -sSL https://get.haskellstack.org/ | sh -s - -f
- - name: Generate Haddock documentation
- run: stack haddock --haddock-arguments --odir="docs"
+ - uses: subosito/flutter-action@v2.2.0
+ with:
+ channel: 'dev'
+ if: matrix.project_dir == 'mobile'
+ - name: Warmup
+ run: ${{ matrix.warm_up }} ; ls -l
+ - name: Generate documentation
+ run: ${{ matrix.doc_generation }}
- name: Deploy
run: |
- cd docs
+ cd "${{ matrix.doc_dir }}"
git config --global user.email "${GITHUB_ACTOR}@github.com";
git config --global user.name "${GITHUB_ACTOR}";
git init
git add -A;
- git commit -m "Deploying the back-end's documentation";
+ git commit -m "Deploying the ${{ matrix.run_name }}'s documentation";
git remote add origin https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@${GITHUB_REPO};
- git checkout -b documentation-back
- git push --force origin documentation-back;
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- GITHUB_REPO: "github.com/AnonymusRaccoon/Aeris"
- DeployMobileAppDocumentation:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2.3.4
- - uses: subosito/flutter-action@v1.5.3
- - name: Update Documentation
- run: |
- flutter pub global activate dartdoc
- rm -rf doc
- dartdoc
- cd doc/api
- git config --global user.email "${GITHUB_ACTOR}@github.com";
- git config --global user.name "${GITHUB_ACTOR}";
- git init
- git add -A;
- git commit -m "Deploying the mobile client documentation";
- git remote add origin https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@${GITHUB_REPO};
- git checkout -b documentation-mobile
- git push --force origin documentation-mobile;
+ git checkout -b documentation-${{ matrix.project_dir }}
+ git push --force origin documentation-${{ matrix.project_dir }};
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPO: "github.com/AnonymusRaccoon/Aeris"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f157f21..2195681 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
.env
-
+*.secret.yaml
.DS_Store
-.idea
\ No newline at end of file
+.idea
diff --git a/README.md b/README.md
index 5e8ac90..2ca2dce 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Aeris supports a variety of service, on which Pipelines are perfomed upon:
- GitHub
- Spotify
- Youtube
-- Discord
+- Reddit
- Anilist
- Twitter
diff --git a/api/aeris.cabal b/api/aeris.cabal
index 0d391c0..bf28948 100644
--- a/api/aeris.cabal
+++ b/api/aeris.cabal
@@ -18,8 +18,8 @@ build-type: Simple
extra-source-files:
README.md
services/anilist.json
- services/discord.json
services/github.json
+ services/reddit.json
services/spotify.json
services/twitter.json
services/utils.json
diff --git a/api/services/discord.json b/api/services/discord.json
deleted file mode 100644
index 58bca3f..0000000
--- a/api/services/discord.json
+++ /dev/null
@@ -1,454 +0,0 @@
-{
- "name": "Discord",
- "actions": [
- {
- "name": "Discord_OnDiscordMessage",
- "description": {
- "en": "An event triggered by the reception of a direct message",
- "fr": "Un évènement déclenché par la réception d'un message privé."
- },
- "label": {
- "en": "Reception of a direct message",
- "fr": "Réception d'un message privé"
- },
- "params": [],
- "returns": [
- {
- "name": "MESSAGE",
- "description": {
- "en": "The body of the received message",
- "fr": "Corps du message reçu"
- }
- },
- {
- "name": "AUTHOR_ID",
- "description": {
- "en": "The Discord ID of the message's author",
- "fr": "Identifiant Discord de l'auteur du message"
- }
- },
- {
- "name": "AUTHOR_NAME",
- "description": {
- "en": "The Discord username of the message's author",
- "fr": "Pseudo Discord de l'auteur du message"
- }
- }
- ]
- },
- {
- "name": "Discord_OnDiscordMessageFrom",
- "description": {
- "en": "An event triggered by the reception of a direct message from a specified user",
- "fr": "Un évènement déclenché par la réception d'un message privé d'un certain utilisateur"
- },
- "label": {
- "en": "Reception of a direct message from a specific user",
- "fr": "Réception d'un message privé d'un certain utilisateur"
- },
- "params": [
- {
- "name": "user_id",
- "type": "string",
- "description": {
- "en": "The Discord ID of the sender",
- "fr": "Identifiant Discord de l'envoyeur"
- }
- }
- ],
- "returns": [
- {
- "name": "MESSAGE",
- "description": {
- "en": "The body of the received message",
- "fr": "Corps du message reçu"
- }
- },
- {
- "name": "AUTHOR_ID",
- "description": {
- "en": "The Discord ID of the message's author",
- "fr": "Identifiant Discord de l'auteur du message"
- }
- },
- {
- "name": "AUTHOR_NAME",
- "description": {
- "en": "The Discord username of the message's author",
- "fr": "Pseudo Discord de l'auteur du message"
- }
- }
- ]
- },
- {
- "name": "Discord_OnDiscordMention",
- "description": {
- "en": "An event triggered when the authenticated user is mentioned",
- "fr": "Un évènement déclenché lorsque l'utilisateur authentifié est mentionné"
- },
- "label": {
- "en": "Authenticated user mentioned",
- "fr": "Utilisateur authentifié mentioné"
- },
- "params": [],
- "returns": [
- {
- "name": "MESSAGE",
- "description": {
- "en": "The body of the received message",
- "fr": "Corps du message reçu"
- }
- },
- {
- "name": "AUTHOR_ID",
- "description": {
- "en": "The Discord ID of the message's author",
- "fr": "Identifiant Discord de l'auteur du message"
- }
- },
- {
- "name": "AUTHOR_NAME",
- "description": {
- "en": "The Discord username of the message's author",
- "fr": "Pseudo Discord de l'auteur du message"
- }
- }
- ]
- },
- {
- "name": "Discord_OnNewDiscordGuildMember",
- "description": {
- "en": "An event triggered when a new member joins a guild",
- "fr": "Un évènement déclenché lorsqu'un nouveau membre rejoint un serveur"
- },
- "label": {
- "en": "New member joins a guild",
- "fr": "Nouveau membre rejoint un serveur"
- },
- "params": [],
- "returns": [
- {
- "name": "NEW_MEMBER_ID",
- "description": {
- "en": "The Discord ID of the new member",
- "fr": "Identifiant Discord du nouveau membre"
- }
- },
- {
- "name": "NEW_MEMBER_NAME",
- "description": {
- "en": "The Discord Username of the new member",
- "fr": "Pseudo Discord du nouveau membre"
- }
- },
- {
- "name": "SERVER_ID",
- "description": {
- "en": "The Discord ID of the guild",
- "fr": "Identifiant Discord du serveur"
- }
- },
- {
- "name": "SERVER_NAME",
- "description": {
- "en": "The name of the guild",
- "fr": "Nom du serveur"
- }
- }
- ]
- },
- {
- "name": "Discord_OnDiscordGuildLeave",
- "description": {
- "en": "An event triggered when a member leaves a guild",
- "fr": "Un évènement déclenché lorsqu'un membre quitte un serveur"
- },
- "label": {
- "en": "A member leaves a guild",
- "fr": "Un utilisateur quitte un serveur"
- },
- "params": [],
- "returns": [
- {
- "name": "NEW_MEMBER_ID",
- "description": {
- "en": "The Discord ID of the new member",
- "fr": "Identifiant Discord du nouveau membre"
- }
- },
- {
- "name": "NEW_MEMBER_NAME",
- "description": {
- "en": "The Discord Username of the new member",
- "fr": "Pseudo Discord du nouveau membre"
- }
- },
- {
- "name": "SERVER_ID",
- "description": {
- "en": "The Discord ID of the guild",
- "fr": "Identifiant Discord du serveur"
- }
- },
- {
- "name": "SERVER_NAME",
- "description": {
- "en": "The name of the guild",
- "fr": "Nom du serveur"
- }
- }
- ]
- }
- ],
- "reactions": [
- {
- "name": "Discord_PostDiscordMessage",
- "description": {
- "en": "Posts a message on a given server (in a given channel)",
- "fr": "Poste un message sur le serveur donné dans un canal donné"
- },
- "label": {
- "en": "Post a message on a server",
- "fr": "Poste un message sur un serveur"
- },
- "params": [
- {
- "name": "server_id",
- "type": "string",
- "description": {
- "en": "The Discord ID of the server",
- "fr": "Identifiant Discord du serveur"
- }
- },
- {
- "name": "channel_id",
- "type": "string",
- "description": {
- "en": "Discord ID of the channel",
- "fr": "Identifiant Discord du canal"
- }
- },
- {
- "name": "content",
- "type": "string",
- "description": {
- "en": "Body of the message to post",
- "fr": "Contenu du message à envoyer"
- }
- }
- ],
- "returns": [
- {
- "name": "USER_ID",
- "description": {
- "en": "The Discord ID of the sender",
- "fr": "Identifiant Discord de l'envoyeur"
- }
- },
- {
- "name": "USERNAME",
- "description": {
- "en": "The Discord username of the sender",
- "fr": "Pseudo Discord de l'envoyeur"
- }
- },
- {
- "name": "MESSAGE_CONTENT",
- "description": {
- "en": "The body of the posted message",
- "fr": "Corps du message posté"
- }
- },
- {
- "name": "CHANNEL_ID",
- "description": {
- "en": "The Discord ID of the channel",
- "fr": "Identifiant Discord du canal"
- }
- },
- {
- "name": "CHANNEL_NAME",
- "description": {
- "en": "The Discord name of the channel",
- "fr": "Nom du canal Discord"
- }
- },
- {
- "name": "SERVER_ID",
- "description": {
- "en": "The Discord ID of the server",
- "fr": "Identifiant Discord du serveur"
- }
- },
- {
- "name": "SERVER_NAME",
- "description": {
- "en": "The Discord name of the server",
- "fr": "Nom du serveur Discord"
- }
- }
- ]
- },
- {
- "name": "Discord_PostDiscordDM",
- "description": {
- "en": "Sends a message to a given user",
- "fr": "Envoie un message à un utilisateur donné"
- },
- "label": {
- "en": "Send a direct message",
- "fr": "Envoi un message privé"
- },
- "params": [
- {
- "name": "other_id",
- "type": "string",
- "description": {
- "en": "The Discord ID of the recipient",
- "fr": "Identifiant Discord du receveur"
- }
- },
- {
- "name": "content",
- "type": "string",
- "description": {
- "en": "Body of the message to send",
- "fr": "Corps du message à envoyer"
- }
- }
- ],
- "returns": [
- {
- "name": "USER_ID",
- "description": {
- "en": "The Discord ID of the sender",
- "fr": "Identifiant Discord de l'envoyeur"
- }
- },
- {
- "name": "USERNAME",
- "description": {
- "en": "The Discord username of the sender",
- "fr": "Pseudo Discord de l'envoyeur"
- }
- },
- {
- "name": "MESSAGE_CONTENT",
- "description": {
- "en": "The body of the posted message",
- "fr": "Corps du message posté"
- }
- },
- {
- "name": "SENDEE_ID",
- "description": {
- "en": "The Discord ID of the recipient",
- "fr": "Identifiant Discord du receveur"
- }
- },
- {
- "name": "SENDEE_USERNAME",
- "description": {
- "en": "The Discord name of the recipient",
- "fr": "Pseudo Discord du receveur"
- }
- }
- ]
- },
- {
- "name": "Discord_LeaveDiscordServer",
- "description": {
- "en": "Leave given Discord server",
- "fr": "Quitte le serveur Discord donné"
- },
- "label": {
- "en": "Leave a server",
- "fr": "Quitte un serveur"
- },
- "params": [
- {
- "name": "server_id",
- "type": "string",
- "description": {
- "en": "The Discord ID of the server",
- "fr": "Identifiant Discord du serveur"
- }
- }
- ],
- "returns": [
- {
- "name": "USER_ID",
- "description": {
- "en": "The Discord ID of the leaver",
- "fr": "Identifiant Discord de l'utilisateur qui quitte le serveur"
- }
- },
- {
- "name": "USERNAME",
- "description": {
- "en": "The Discord username of the leaver",
- "fr": "Pseudo Discord de l'utilisateur qui quitte le serveur"
- }
- },
- {
- "name": "SERVER_ID",
- "description": {
- "en": "The Discord ID of the server",
- "fr": "Identifiant Discord du serveur"
- }
- },
- {
- "name": "SERVER_NAME",
- "description": {
- "en": "The Discord name of the server",
- "fr": "Nom du serveur Discord"
- }
- }
- ]
- },
- {
- "name": "Discord_SetDiscordStatus",
- "description": {
- "en": "Set discord status",
- "fr": "Change le status Discord"
- },
- "label": {
- "en": "Set Discord status",
- "fr": "Change le status Discord"
- },
- "params": [
- {
- "name": "status",
- "type": "string",
- "description": {
- "en": "The status to set",
- "fr": "Statut à définir"
- }
- }
- ],
- "returns": [
- {
- "name": "USER_ID",
- "description": {
- "en": "The Discord ID of the user",
- "fr": "Identifiant Discord de l'utilisateur"
- }
- },
- {
- "name": "USERNAME",
- "description": {
- "en": "The Discord username of the user",
- "fr": "Pseudo Discord de l'utilisateur"
- }
- },
- {
- "name": "STATUS",
- "description": {
- "en": "The Discord status of the user",
- "fr": "Status Discord de l'utilisateur"
- }
- }
- ]
- }
- ]
-}
\ No newline at end of file
diff --git a/api/services/github.json b/api/services/github.json
index a49a3e1..e4649c5 100644
--- a/api/services/github.json
+++ b/api/services/github.json
@@ -1,723 +1,6 @@
{
"name": "Github",
- "actions": [
- {
- "name": "Github_OnOpenPR",
- "description": {
- "en": "An event triggered when a pull request is opened on the repository given as parameter",
- "fr": "Un évènement déclenché lorsqu'une pull request est ouverte sur le répertoire donné"
- },
- "label": {
- "en": "Pull request opening",
- "fr": "Ouverture d'une pull request"
- },
- "params": [
- {
- "name": "owner",
- "type": "string",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "repo",
- "type": "string",
- "description": {
- "en": "The targeted repository's name",
- "fr": "Nom du répertoire ciblé"
- }
- }
- ],
- "returns": [
- {
- "name": "PR_NAME",
- "description": {
- "en": "The opened pull request name",
- "fr": "Nom de la pull request ouverte"
- }
- },
- {
- "name": "PR_BODY",
- "description": {
- "en": "The opened pull request body",
- "fr": "Corps de la pull request ouverte"
- }
- },
- {
- "name": "PR_OPENER",
- "description": {
- "en": "The user's username that opened the pull request",
- "fr": "Pseudo de l'utilisateur qui a ouvert la pull request"
- }
- },
- {
- "name": "PR_HEAD",
- "description": {
- "en": "The name of the branch to merge into the base branch",
- "fr": "Nom de la branche à merger dans la branche de base"
- }
- },
- {
- "name": "PR_BASE",
- "description": {
- "en": "The name of the branch to merge head branch into",
- "fr": "Nom de la branche dans laquelle merger la branche 'head'"
- }
- },
- {
- "name": "REPO_NAME",
- "description": {
- "en": "The name of the repository",
- "fr": "Nom du répertoire"
- }
- },
- {
- "name": "REPO_OWNER",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- }
- ]
- },
- {
- "name": "Github_OnCommentPR",
- "description": {
- "en": "An event triggered by a new comment inside the pull request on the repository given as parameter",
- "fr": "Un évènement déclenché par un nouveau commentaire sous la pull request du répertoire donné en paramètre"
- },
- "label": {
- "en": "New comment inside a pull request",
- "fr": "Nouveau commentaire sous une pull request"
- },
- "params": [
- {
- "name": "owner",
- "type": "string",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "repo",
- "type": "string",
- "description": {
- "en": "The targeted repository's name",
- "fr": "Nom du répertoire ciblé"
- }
- }
- ],
- "returns": [
- {
- "name": "PR_NAME",
- "description": {
- "en": "The opened pull request name",
- "fr": "Nom de la pull request ouverte"
- }
- },
- {
- "name": "PR_BODY",
- "description": {
- "en": "The opened pull request body",
- "fr": "Corps de la pull request ouverte"
- }
- },
- {
- "name": "PR_OPENER",
- "description": {
- "en": "The user's username that opened the pull request",
- "fr": "Pseudo de l'utilisateur qui a ouvert la pull request"
- }
- },
- {
- "name": "PR_HEAD",
- "description": {
- "en": "The name of the branch to merge into the base branch",
- "fr": "Nom de la branche à merger dans la branche de base"
- }
- },
- {
- "name": "PR_BASE",
- "description": {
- "en": "The name of the branch to merge head branch into",
- "fr": "Nom de la branche dans laquelle merger la branche 'head'"
- }
- },
- {
- "name": "REPO_NAME",
- "description": {
- "en": "The name of the repository",
- "fr": "Nom du répertoire"
- }
- },
- {
- "name": "REPO_OWNER",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "COMMENTER",
- "description": {
- "en": "The user's username that comment the pull request",
- "fr": "Pseudo de l'utilisateur qui a commenté la pull request"
- }
- },
- {
- "name": "COMMENT_BODY",
- "description": {
- "en": "The body of the new comment on the pull request",
- "fr": "Corps du nouveau commentaire sous la pull request"
- }
- }
- ]
- },
- {
- "name": "Github_OnClosePR",
- "description": {
- "en": "An event triggered when a pull request is closed on the repository given as parameter",
- "fr": "Un évènement déclenché lorsqu'une pull request est fermée sur le répertoire donné"
- },
- "label": {
- "en": "Closed pull request",
- "fr": "Fermeture d'une pull request"
- },
- "params": [
- {
- "name": "owner",
- "type": "string",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "repo",
- "type": "string",
- "description": {
- "en": "The targeted repository's name",
- "fr": "Nom du répertoire ciblé"
- }
- }
- ],
- "returns": [
- {
- "name": "PR_NAME",
- "description": {
- "en": "The opened pull request name",
- "fr": "Nom de la pull request ouverte"
- }
- },
- {
- "name": "PR_BODY",
- "description": {
- "en": "The opened pull request body",
- "fr": "Corps de la pull request ouverte"
- }
- },
- {
- "name": "PR_OPENER",
- "description": {
- "en": "The user's username that opened the pull request",
- "fr": "Pseudo de l'utilisateur qui a ouvert la pull request"
- }
- },
- {
- "name": "PR_HEAD",
- "description": {
- "en": "The name of the branch to merge into the base branch",
- "fr": "Nom de la branche à merger dans la branche de base"
- }
- },
- {
- "name": "PR_BASE",
- "description": {
- "en": "The name of the branch to merge head branch into",
- "fr": "Nom de la branche dans laquelle merger la branche 'head'"
- }
- },
- {
- "name": "REPO_NAME",
- "description": {
- "en": "The name of the repository",
- "fr": "Nom du répertoire"
- }
- },
- {
- "name": "REPO_OWNER",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- }
- ]
- },
- {
- "name": "Github_OnMergePR",
- "description": {
- "en": "An event triggered when a pull request is merged on the repository given as parameter",
- "fr": "Un évènement déclenché lorsqu'une pull request est mergée sur le répertoire donné"
- },
- "label": {
- "en": "Merged pull request",
- "fr": "Pull request mergée"
- },
- "params": [
- {
- "name": "owner",
- "type": "string",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "repo",
- "type": "string",
- "description": {
- "en": "The targeted repository's name",
- "fr": "Nom du répertoire ciblé"
- }
- }
- ],
- "returns": [
- {
- "name": "PR_NAME",
- "description": {
- "en": "The opened pull request name",
- "fr": "Nom de la pull request ouverte"
- }
- },
- {
- "name": "PR_BODY",
- "description": {
- "en": "The opened pull request body",
- "fr": "Corps de la pull request ouverte"
- }
- },
- {
- "name": "PR_OPENER",
- "description": {
- "en": "The user's username that opened the pull request",
- "fr": "Pseudo de l'utilisateur qui a ouvert la pull request"
- }
- },
- {
- "name": "PR_HEAD",
- "description": {
- "en": "The name of the branch to merge into the base branch",
- "fr": "Nom de la branche à merger dans la branche de base"
- }
- },
- {
- "name": "PR_BASE",
- "description": {
- "en": "The name of the branch to merge head branch into",
- "fr": "Nom de la branche dans laquelle merger la branche 'head'"
- }
- },
- {
- "name": "REPO_NAME",
- "description": {
- "en": "The name of the repository",
- "fr": "Nom du répertoire"
- }
- },
- {
- "name": "REPO_OWNER",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- }
- ]
- },
- {
- "name": "Github_OnCreateIssue",
- "description": {
- "en": "An event triggered when an issue is created on the repository given as parameter",
- "fr": "Un évènement déclenché lorsqu'une nouvelle issue est créée sur le répertoire donné"
- },
- "label": {
- "en": "Created issue",
- "fr": "Issue créée"
- },
- "params": [
- {
- "name": "owner",
- "type": "string",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "repo",
- "type": "string",
- "description": {
- "en": "The targeted repository's name",
- "fr": "Nom du répertoire ciblé"
- }
- }
- ],
- "returns": [
- {
- "name": "REPO_NAME",
- "description": {
- "en": "The name of the repository",
- "fr": "Nom du répertoire"
- }
- },
- {
- "name": "REPO_OWNER",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "ISSUE_NAME",
- "description": {
- "en": "The title of the created issue",
- "fr": "Titre de l'issue créée"
- }
- },
- {
- "name": "ISSUE_CONTENT",
- "description": {
- "en": "The description of the created issue",
- "fr": "Description de l'issue créée"
- }
- },
- {
- "name": "ISSUE_AUTHOR",
- "description": {
- "en": "The issue creator username",
- "fr": "Pseudo de l'auteur de l'issue"
- }
- }
- ]
- },
- {
- "name": "Github_OnCommentIssue",
- "description": {
- "en": "An event triggered when an user comments an issue on the repository given as parameter",
- "fr": "Un évènement déclenché lorsqu'un utilisateur commente une issue sur le répertoire donné"
- },
- "label": {
- "en": "New comment on an issue",
- "fr": "Nouveau commentaire sur une issue"
- },
- "params": [
- {
- "name": "owner",
- "type": "string",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "repo",
- "type": "string",
- "description": {
- "en": "The targeted repository's name",
- "fr": "Nom du répertoire ciblé"
- }
- }
- ],
- "returns": [
- {
- "name": "REPO_NAME",
- "description": {
- "en": "The name of the repository",
- "fr": "Nom du répertoire"
- }
- },
- {
- "name": "REPO_OWNER",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "ISSUE_NAME",
- "description": {
- "en": "The title of the created issue",
- "fr": "Titre de l'issue créée"
- }
- },
- {
- "name": "ISSUE_CONTENT",
- "description": {
- "en": "The description of the created issue",
- "fr": "Description de l'issue créée"
- }
- },
- {
- "name": "ISSUE_AUTHOR",
- "description": {
- "en": "The issue creator username",
- "fr": "Pseudo de l'auteur de l'issue"
- }
- },
- {
- "name": "COMMENT",
- "description": {
- "en": "The body of the new comment on an issue of the repository given as parameter",
- "fr": "Corps du nouveau commentaire sur une issue dans le répertoire donné"
- }
- },
- {
- "name": "COMMENTER",
- "description": {
- "en": "The author of the comment on an issue",
- "fr": "Pseudo de l'auteur du commentaire de l'issue"
- }
- }
- ]
- },
- {
- "name": "Github_OnCloseIssue",
- "description": {
- "en": "An event triggered when an issue is closed in the repository given as parameter",
- "fr": "Un évènement déclenché lorsqu'une issue est fermée sur le répertoire donné"
- },
- "label": {
- "en": "Issue closing",
- "fr": "Fermeture d'une issue"
- },
- "params": [
- {
- "name": "owner",
- "type": "string",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "repo",
- "type": "string",
- "description": {
- "en": "The targeted repository's name",
- "fr": "Nom du répertoire ciblé"
- }
- }
- ],
- "returns": [
- {
- "name": "REPO_NAME",
- "description": {
- "en": "The name of the repository",
- "fr": "Nom du répertoire"
- }
- },
- {
- "name": "REPO_OWNER",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "ISSUE_NAME",
- "description": {
- "en": "The title of the created issue",
- "fr": "Titre de l'issue créée"
- }
- },
- {
- "name": "ISSUE_CONTENT",
- "description": {
- "en": "The description of the created issue",
- "fr": "Description de l'issue créée"
- }
- },
- {
- "name": "ISSUE_AUTHOR",
- "description": {
- "en": "The issue creator username",
- "fr": "Pseudo de l'auteur de l'issue"
- }
- }
- ]
- },
- {
- "name": "Github_OnForkRepo",
- "description": {
- "en": "An event triggered when the repository given as parameter is forked",
- "fr": "Un évènement déclenché lorsque le répertoire donné est fourché"
- },
- "label": {
- "en": "Forked repository",
- "fr": "Répertoire fourché"
- },
- "params": [
- {
- "name": "owner",
- "type": "string",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "repo",
- "type": "string",
- "description": {
- "en": "The targeted repository's name",
- "fr": "Nom du répertoire ciblé"
- }
- }
- ],
- "returns": [
- {
- "name": "REPO_NAME",
- "description": {
- "en": "The name of the repository",
- "fr": "Nom du répertoire"
- }
- },
- {
- "name": "REPO_OWNER",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "FORKER",
- "description": {
- "en": "The username of the user that fork the repository",
- "fr": "Pseudo de l'utilisateur qui a fork le répertoire"
- }
- }
- ]
- },
- {
- "name": "Github_OnStarRepo",
- "description": {
- "en": "An event triggered when a user star the repository given as parameter",
- "fr": "Un évènement déclenché lorsqu'un utilisateur étoile un répertoire donné"
- },
- "label": {
- "en": "Starred repository",
- "fr": "Répertoire étoilé"
- },
- "params": [
- {
- "name": "owner",
- "type": "string",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "repo",
- "type": "string",
- "description": {
- "en": "The targeted repository's name",
- "fr": "Nom du répertoire ciblé"
- }
- }
- ],
- "returns": [
- {
- "name": "REPO_NAME",
- "description": {
- "en": "The name of the repository",
- "fr": "Nom du répertoire"
- }
- },
- {
- "name": "REPO_OWNER",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "STAR_COUNT",
- "description": {
- "en": "The number of stars on the repository",
- "fr": "Nombre de star sur le répertoire"
- }
- },
- {
- "name": "STARER",
- "description": {
- "en": "The username of the user that starred the repository",
- "fr": "Pseudo de l'utilisateur qui a star le répertoire"
- }
- }
- ]
- },
- {
- "name": "Github_OnWatchRepo",
- "description": {
- "en": "An event triggered when an user watch a repository",
- "fr": "Un évènement déclenché lorsqu'un utilisateur watch le répertoire donné"
- },
- "label": {
- "en": "Watch a repository",
- "fr": "Répertoire observé"
- },
- "params": [
- {
- "name": "owner",
- "type": "string",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "repo",
- "type": "string",
- "description": {
- "en": "The targeted repository's name",
- "fr": "Nom du répertoire ciblé"
- }
- }
- ],
- "returns": [
- {
- "name": "REPO_NAME",
- "description": {
- "en": "The name of the repository",
- "fr": "Nom du répertoire"
- }
- },
- {
- "name": "REPO_OWNER",
- "description": {
- "en": "The user's username that owns the repository",
- "fr": "Pseudo de l'utilisateur qui possède le répertoire"
- }
- },
- {
- "name": "WATCH_COUNT",
- "description": {
- "en": "The number of users watching the repository",
- "fr": "Nombre d'utilisateur qui watch le répertoire"
- }
- },
- {
- "name": "WATCHER",
- "description": {
- "en": "The username of the user that watched the repository",
- "fr": "Pseudo de l'utilisateur qui vient de watch le répertoire"
- }
- }
- ]
- }
- ],
+ "actions": [],
"reactions": [
{
"name": "Github_OpenPR",
diff --git a/api/services/reddit.json b/api/services/reddit.json
new file mode 100644
index 0000000..79eb989
--- /dev/null
+++ b/api/services/reddit.json
@@ -0,0 +1,320 @@
+{
+ "name": "Reddit",
+ "actions": [],
+ "reactions": [
+ {
+ "name": "Reddit_JoinSubreddit",
+ "description": {
+ "en": "Subscribe to a Subbreddit",
+ "fr": "S'abonner à un Subreddit"
+ },
+ "label": {
+ "en": "Join a Subbreddit",
+ "fr": "Rejoindre un Subreddit"
+ },
+ "params": [
+ {
+ "name": "sub_id",
+ "type": "string",
+ "description": {
+ "en": "The name of the subreddit",
+ "fr": "Le nom du subreddit"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "SUB_NAME",
+ "type": "string",
+ "description": {
+ "en": "The name of the subreddit",
+ "fr": "Le nom du subreddit"
+ }
+ },
+ {
+ "name": "SUB_ID",
+ "type": "string",
+ "description": {
+ "en": "The ID of the subreddit",
+ "fr": "L'ID du subreddit"
+ }
+ }
+ ]
+ },
+ {
+ "name": "Reddit_LeaveSubreddit",
+ "description": {
+ "en": "Unsubscribe to a Subbreddit",
+ "fr": "Se désabonner à un Subreddit"
+ },
+ "label": {
+ "en": "Leave a Subbreddit",
+ "fr": "Quitter un Subreddit"
+ },
+ "params": [
+ {
+ "name": "sub_id",
+ "type": "string",
+ "description": {
+ "en": "The ID of the subreddit",
+ "fr": "Le ID du subreddit"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "SUB_NAME",
+ "type": "string",
+ "description": {
+ "en": "The name of the subreddit",
+ "fr": "Le nom du subreddit"
+ }
+ },
+ {
+ "name": "SUB_ID",
+ "type": "string",
+ "description": {
+ "en": "The ID of the subreddit",
+ "fr": "L'ID du subreddit"
+ }
+ }
+ ]
+ },
+ {
+ "name": "Reddit_PostInSubreddit",
+ "description": {
+ "en": "Post in a Subbreddit",
+ "fr": "Créer un post dans un Subreddit"
+ },
+ "label": {
+ "en": "Create a post",
+ "fr": "Créer un post"
+ },
+ "params": [
+ {
+ "name": "sub_id",
+ "type": "string",
+ "description": {
+ "en": "The ID of the subreddit",
+ "fr": "Le ID du subreddit"
+ }
+ },
+ {
+ "name": "title",
+ "type": "string",
+ "description": {
+ "en": "The title of the post",
+ "fr": "Le titre du post"
+ }
+ },
+ {
+ "name": "body",
+ "type": "string",
+ "description": {
+ "en": "The content of the post",
+ "fr": "Le contenu du post"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "SUB_NAME",
+ "type": "string",
+ "description": {
+ "en": "The name of the parent subreddit",
+ "fr": "Le nom du subreddit parent"
+ }
+ },
+ {
+ "name": "SUB_ID",
+ "type": "string",
+ "description": {
+ "en": "The ID of the subreddit parent",
+ "fr": "L'ID du subreddit parent"
+ }
+ },
+ {
+ "name": "POST_ID",
+ "type": "string",
+ "description": {
+ "en": "The ID of the post",
+ "fr": "L'ID du post"
+ }
+ }
+ ]
+ },
+ {
+ "name": "Reddit_ReplyToPost",
+ "description": {
+ "en": "Reply to Post in a Subbreddit",
+ "fr": "Créer un réponse à un post dans un Subreddit"
+ },
+ "label": {
+ "en": "Reply to a post",
+ "fr": "Répondre à un post"
+ },
+ "params": [
+ {
+ "name": "post_id",
+ "type": "string",
+ "description": {
+ "en": "The ID of the post",
+ "fr": "L'ID du post"
+ }
+ },
+ {
+ "name": "reply_body",
+ "type": "string",
+ "description": {
+ "en": "The content of the reply",
+ "fr": "Le contenu de la réponse"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "SUB_NAME",
+ "type": "string",
+ "description": {
+ "en": "The name of the parent subreddit",
+ "fr": "Le nom du subreddit parent"
+ }
+ },
+ {
+ "name": "SUB_ID",
+ "type": "string",
+ "description": {
+ "en": "The ID of the parent subreddit",
+ "fr": "L'ID du subreddit parent"
+ }
+ },
+ {
+ "name": "POST_ID",
+ "type": "string",
+ "description": {
+ "en": "The ID of the post",
+ "fr": "L'ID du post"
+ }
+ },
+ {
+ "name": "REPLY_ID",
+ "type": "string",
+ "description": {
+ "en": "The ID of the reply",
+ "fr": "L'ID de la réponse"
+ }
+ }
+ ]
+ },
+ {
+ "name": "Reddit_Upvote",
+ "description": {
+ "en": "Upvote a Post in a Subbreddit",
+ "fr": "Liker un un post dans un Subreddit"
+ },
+ "label": {
+ "en": "Upvote to a post",
+ "fr": "Liker un post"
+ },
+ "params": [
+ {
+ "name": "post_id",
+ "type": "string",
+ "description": {
+ "en": "The ID of the post",
+ "fr": "L'ID du post"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "SUB_NAME",
+ "type": "string",
+ "description": {
+ "en": "The name of the parent subreddit",
+ "fr": "Le nom du subreddit parent"
+ }
+ },
+ {
+ "name": "SUB_ID",
+ "type": "string",
+ "description": {
+ "en": "The ID of the parent subreddit",
+ "fr": "L'ID du subreddit parent"
+ }
+ },
+ {
+ "name": "POST_ID",
+ "type": "string",
+ "description": {
+ "en": "The ID of the upvoted post",
+ "fr": "L'ID du post liké"
+ }
+ },
+ {
+ "name": "UPVOTE_COUNT",
+ "type": "integer",
+ "description": {
+ "en": "The count of upvote of the post",
+ "fr": "Le nombre d'upvote sur le post"
+ }
+ }
+ ]
+ },
+ {
+ "name": "Reddit_Downvote",
+ "description": {
+ "en": "Downvote a Post in a Subbreddit",
+ "fr": "Disliker un un post dans un Subreddit"
+ },
+ "label": {
+ "en": "Downvote to a post",
+ "fr": "Disliker un post"
+ },
+ "params": [
+ {
+ "name": "post_id",
+ "type": "string",
+ "description": {
+ "en": "The ID of the post",
+ "fr": "L'ID du post"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "SUB_NAME",
+ "type": "string",
+ "description": {
+ "en": "The name of the parent subreddit",
+ "fr": "Le nom du subreddit parent"
+ }
+ },
+ {
+ "name": "SUB_ID",
+ "type": "string",
+ "description": {
+ "en": "The ID of the parent subreddit",
+ "fr": "L'ID du subreddit parent"
+ }
+ },
+ {
+ "name": "POST_ID",
+ "type": "string",
+ "description": {
+ "en": "The ID of the downvoted post",
+ "fr": "L'ID du post disliké"
+ }
+ },
+ {
+ "name": "DOWNVOTE_COUNT",
+ "type": "integer",
+ "description": {
+ "en": "The count of downvote of the post",
+ "fr": "Le nombre de downvote sur le post"
+ }
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/api/src/Api/About.hs b/api/src/Api/About.hs
index e24446c..4885b2c 100644
--- a/api/src/Api/About.hs
+++ b/api/src/Api/About.hs
@@ -45,7 +45,7 @@ $(deriveJSON defaultOptions ''About)
servicesDir :: [(FilePath, S.ByteString)]
servicesDir = $(embedDir "./services/")
--- servicesDir = undefined
+--servicesDir = undefined
about :: SockAddr -> AppM About
about host = do
diff --git a/api/src/Api/Auth.hs b/api/src/Api/Auth.hs
index c2d4a5c..45de609 100644
--- a/api/src/Api/Auth.hs
+++ b/api/src/Api/Auth.hs
@@ -18,14 +18,18 @@ import Servant (
Post,
ReqBody,
err401,
+ err400,
+ err403,
throwError,
type (:<|>) (..),
- type (:>),
+ type (:>), Capture, QueryParam
)
import Control.Monad.IO.Class (liftIO)
+import Control.Monad.Trans.Maybe (MaybeT(runMaybeT))
+import Core.OIDC ( getOauthTokens )
import Data.Aeson (FromJSON, ToJSON)
-import Db.User (User', UserDB (UserDB), password, toUser)
+import Db.User (User', UserDB (UserDB, userDBId), password, toUser)
import GHC.Generics (Generic)
import Servant.API.Generic (ToServantApi, (:-))
import Servant.Auth.Server (CookieSettings, JWT, JWTSettings, SetCookie, ThrowAll (throwAll), acceptLogin, AuthResult (Authenticated), makeJWT)
@@ -34,10 +38,10 @@ import Servant.Server.Generic (AsServerT)
import Data.ByteString.Lazy.Char8 ( unpack )
import Api.OIDC (OauthAPI, oauth)
import App (AppM)
-import Core.User (User, UserId (UserId))
+import Core.User (User, UserId (UserId), Service)
import Data.Text (pack)
import Password (hashPassword'', toPassword, validatePassword')
-import Repository (createUser, getUserByName')
+import Repository (createUser, getUserByName', getUserByToken, updateTokens)
import Utils (UserAuth, AuthRes)
data LoginUser = LoginUser
@@ -57,15 +61,16 @@ newtype LoginResponse = LoginResponse
}
deriving (Eq, Show, Read, Generic)
+
+instance ToJSON LoginResponse
+instance FromJSON LoginResponse
+
instance ToJSON LoginUser
instance FromJSON LoginUser
instance ToJSON SignupUser
instance FromJSON SignupUser
-instance ToJSON LoginResponse
-instance FromJSON LoginResponse
-
type Protected =
"me" :> Get '[JSON] User
@@ -80,6 +85,14 @@ type Unprotected =
:<|> "signup"
:> ReqBody '[JSON] SignupUser
:> Post '[JSON] NoContent
+ :<|> Capture "service" Service :> "signin"
+ :> QueryParam "code" String
+ :> Post '[JSON] LoginResponse
+ :<|> Capture "service" Service :> "signup"
+ :> QueryParam "code" String
+ :> ReqBody '[JSON] SignupUser
+ :> Post '[JSON] LoginResponse
+
loginHandler ::
CookieSettings ->
@@ -95,11 +108,35 @@ loginHandler cs jwts (LoginUser username p) = do
case etoken of
Left e -> throwError err401
Right v -> return $ LoginResponse $ unpack v
- {--mApplyCookies <- liftIO $ acceptLogin cs jwts (toUser usr)
- case mApplyCookies of
- Nothing -> throwError err401
- Just applyCookies -> return $ applyCookies NoContent
- --}else throwError err401
+ else throwError err401
+
+loginOauthHandler :: JWTSettings -> Service -> Maybe String -> AppM LoginResponse
+loginOauthHandler jwts _ Nothing = throwError err400
+loginOauthHandler jwts service (Just code) = do
+ tokens <- liftIO $ runMaybeT $ getOauthTokens service code
+ case tokens of
+ Nothing -> throwError err403
+ Just t -> do
+ user <- getUserByToken t
+ etoken <- liftIO $ makeJWT (toUser user) jwts Nothing
+ case etoken of
+ Left e -> throwError err401
+ Right v -> return $ LoginResponse $ unpack v
+
+signupOauthHandler :: JWTSettings -> Service -> Maybe String -> SignupUser -> AppM LoginResponse
+signupOauthHandler jwts service (Just code) (SignupUser name p) = do
+ hashed <- hashPassword'' $ toPassword $ pack p
+ user <- createUser $ UserDB (UserId 1) (pack name) hashed (pack name) []
+ tokens <- liftIO $ runMaybeT $ getOauthTokens service code
+ case tokens of
+ Nothing -> throwError err403
+ Just t -> do
+ updateTokens (userDBId user) t
+ etoken <- liftIO $ makeJWT (toUser user) jwts Nothing
+ case etoken of
+ Left e -> throwError err401
+ Right v -> return $ LoginResponse $ unpack v
+signupOauthHandler _ _ _ _ = throwError err400
signupHandler ::
SignupUser ->
@@ -111,8 +148,10 @@ signupHandler (SignupUser name p) = do
unprotected :: CookieSettings -> JWTSettings -> ServerT Unprotected AppM
unprotected cs jwts =
- loginHandler cs jwts
- :<|> signupHandler
+ loginHandler cs jwts
+ :<|> signupHandler
+ :<|> loginOauthHandler jwts
+ :<|> signupOauthHandler jwts
data AuthAPI mode = AuthAPI
{ protectedApi :: mode :- UserAuth :> Protected
diff --git a/api/src/Api/OIDC.hs b/api/src/Api/OIDC.hs
index 4eb3cdd..ff33a51 100644
--- a/api/src/Api/OIDC.hs
+++ b/api/src/Api/OIDC.hs
@@ -7,22 +7,24 @@ module Api.OIDC where
import App (AppM)
import Control.Monad.IO.Class (liftIO)
-import Core.User (ExternalToken (ExternalToken, service), Service (Github, Spotify, Twitter, Google, Anilist, Discord), UserId (UserId), User (User))
+import Core.User (ExternalToken (ExternalToken, service), Service (Github, Spotify, Twitter, Google, Anilist, Reddit), UserId (UserId), User (User))
import Data.Text (pack)
import Core.OIDC ( getOauthTokens )
import Repository.User (updateTokens, getTokensByUserId, delTokens)
-import Servant (Capture, Get, GetNoContent, JSON, NoContent (NoContent), QueryParam, ServerT, err400, throwError, type (:<|>) ((:<|>)), type (:>), err401, err403, ServerError (errHeaders), err302, Delete)
+import Servant (Capture, Get, GetNoContent, JSON, NoContent (NoContent), QueryParam, ServerT, err400, throwError, type (:<|>) ((:<|>)), type (:>), err401, err403, ServerError (errHeaders), err302, Delete, Post)
import Servant.API.Generic (type (:-))
import Servant.Server.Generic (AsServerT)
import Utils (UserAuth, AuthRes)
import qualified Data.ByteString.Char8 as B8
-import Servant.Auth.Server (AuthResult(Authenticated))
+import Servant.Auth.Server (AuthResult(Authenticated), makeJWT)
import System.Environment.MrEnv (envAsString)
+import Control.Monad.Trans.Maybe (MaybeT(runMaybeT))
+import Db.User (toUser)
oauthHandler :: AuthRes -> Service -> Maybe String -> AppM NoContent
oauthHandler _ _ Nothing = throwError err400
oauthHandler (Authenticated (User uid _ _)) service (Just code) = do
- tokens <- liftIO $ getOauthTokens service code
+ tokens <- liftIO $ runMaybeT $ getOauthTokens service code
case tokens of
Nothing -> throwError err403
Just t -> do
@@ -43,11 +45,11 @@ urlHandler Anilist (Just r) = do
backRedirect <- liftIO $ envAsString "BACK_URL" ""
throwError $ err302 { errHeaders =
[("Location", B8.pack $ "https://anilist.co/api/v2/oauth/authorize?client_id=" ++ clientId ++ "&response_type=code&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] }
-urlHandler Discord (Just r) = do
- clientId <- liftIO $ envAsString "DISCORD_CLIENT_ID" ""
+urlHandler Reddit (Just r) = do
+ clientId <- liftIO $ envAsString "REDDIT_CLIENT_ID" ""
backRedirect <- liftIO $ envAsString "BACK_URL" ""
throwError $ err302 { errHeaders =
- [("Location", B8.pack $ "https://discord.com/api/oauth2/authorize?response_type=code&scope=identify%20guilds%20messages.read%20activities.write%20webhook.incoming&client_id=" ++ clientId ++ "&response_type=code&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] }
+ [("Location", B8.pack $ "https://www.reddit.com/api/v1/authorize?client_id=" ++ clientId ++ "&response_type=code&state=" ++ r ++ "&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&duration=permanent&scope=*")] }
urlHandler Google (Just r) = do
clientId <- liftIO $ envAsString "GOOGLE_CLIENT_ID" ""
backRedirect <- liftIO $ envAsString "BACK_URL" ""
@@ -63,7 +65,6 @@ urlHandler Spotify (Just r) = do
backRedirect <- liftIO $ envAsString "BACK_URL" ""
throwError $ err302 { errHeaders =
[("Location", B8.pack $ "https://accounts.spotify.com/authorize?response_type=code&scope=user-library-read user-library-modify streaming playlist-modify-private playlist-read-collaborative playlist-read-private playlist-modify-public user-modify-playback-state user-read-private&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] }
-
urlHandler Github (Just r) = do
clientId <- liftIO $ envAsString "GITHUB_CLIENT_ID" ""
backRedirect <- liftIO $ envAsString "BACK_URL" ""
@@ -89,7 +90,7 @@ type OauthAPI = UserAuth :> Capture "service" Service :> QueryParam "code" Strin
:<|> Capture "service" Service :> "url" :> QueryParam "redirect_uri" String :> Get '[JSON] NoContent
:<|> UserAuth :> "services" :> Get '[JSON] [String]
:<|> "redirect" :> QueryParam "code" String :> QueryParam "state" String :> Get '[JSON] NoContent
-
+
oauth :: ServerT OauthAPI AppM
oauth = oauthHandler
:<|> oauthDelHandler
diff --git a/api/src/Api/Worker.hs b/api/src/Api/Worker.hs
index d701889..b3ac08a 100644
--- a/api/src/Api/Worker.hs
+++ b/api/src/Api/Worker.hs
@@ -114,7 +114,7 @@ refreshHandler :: Service -> UserId -> Maybe String -> RefreshBody -> AppM NoCon
refreshHandler service uid (Just key) (RefreshBody at rt ex) = do
k <- liftIO $ envAsString "WORKER_API_KEY" ""
if k == key then do
- updateTokens uid $ ExternalToken at rt ex service
+ updateTokens uid $ ExternalToken at rt ex service Nothing
return NoContent
else throwError err403
refreshHandler _ _ _ _ = throwError err403
diff --git a/api/src/Core/OIDC.hs b/api/src/Core/OIDC.hs
index 2a74b5d..c5cc976 100644
--- a/api/src/Core/OIDC.hs
+++ b/api/src/Core/OIDC.hs
@@ -6,13 +6,17 @@ import qualified Data.ByteString.Char8 as B8
import qualified Data.HashMap.Strict as HM
import App (AppM)
-import Core.User (ExternalToken (ExternalToken, expiresAt), Service (Github, Discord, Spotify, Google, Twitter, Anilist))
+import Core.User (ExternalToken (ExternalToken, accessToken, providerId, expiresAt), Service (Github, Reddit, Spotify, Google, Twitter, Anilist))
import Data.Aeson.Types (Object, Value (String))
import Data.Text (Text, pack, unpack)
-import Network.HTTP.Simple (JSONException, addRequestHeader, getResponseBody, httpJSONEither, parseRequest, setRequestMethod, setRequestQueryString, setRequestBodyURLEncoded)
+import Network.HTTP.Simple (JSONException, addRequestHeader, getResponseBody, httpJSONEither, parseRequest, setRequestMethod, setRequestQueryString, setRequestBodyURLEncoded, setRequestBodyJSON, setRequestBodyLBS)
import System.Environment.MrEnv (envAsBool, envAsInt, envAsInteger, envAsString)
-import Utils (lookupObjString, lookupObjInt)
-import Data.ByteString.Base64
+import Utils (lookupObjString, lookupObjObject, lookupObjInt)
+import Control.Monad.Trans.Maybe (MaybeT (MaybeT, runMaybeT))
+import Control.Monad.IO.Class (MonadIO(liftIO))
+import Control.Monad (MonadPlus (mzero))
+import Data.Aeson (decode)
+import Data.ByteString.Base64 ( encodeBase64 )
import Data.Time (getCurrentTime, addUTCTime)
data OAuth2Conf = OAuth2Conf
{ oauthClientId :: String
@@ -33,6 +37,10 @@ tokenEndpoint code oc =
, code
]
+liftMaybe :: (MonadPlus m) => Maybe a -> m a
+liftMaybe = maybe mzero return
+
+
-- GITHUB
getGithubConfig :: IO OAuth2Conf
getGithubConfig =
@@ -41,8 +49,8 @@ getGithubConfig =
<*> envAsString "GITHUB_SECRET" ""
<*> pure "https://github.com/login/oauth/access_token"
-getGithubTokens :: String -> IO (Maybe ExternalToken)
-getGithubTokens code = do
+getGithubTokens :: String -> MaybeT IO ExternalToken
+getGithubTokens code = MaybeT $ do
gh <- getGithubConfig
let endpoint = tokenEndpoint code gh
request' <- parseRequest endpoint
@@ -57,46 +65,79 @@ getGithubTokens code = do
request'
response <- httpJSONEither request
currTime <- getCurrentTime
- return $ case (getResponseBody response :: Either JSONException Object) of
- Left _ -> Nothing
- Right obj -> do
- access <- lookupObjString obj "access_token"
- Just $ ExternalToken (pack access) "" currTime Github
+ let (Right obj) = (getResponseBody response :: Either JSONException Object)
+ access <- liftMaybe $ lookupObjString obj "access_token"
+ let t = ExternalToken access "" currTime Github Nothing
+ githubId <- runMaybeT $ getGithubId t
+ return $ Just $ t { providerId = githubId }
--- DISCORD
-getDiscordConfig :: IO OAuth2Conf
-getDiscordConfig =
+rightToMaybe :: Either a b -> Maybe b
+rightToMaybe = either (const Nothing) Just
+
+getGithubId :: ExternalToken -> MaybeT IO Text
+getGithubId t = MaybeT $ do
+ let endpoint = "https://api.github.com/user"
+ request' <- parseRequest endpoint
+ let request =
+ addRequestHeader "Authorization" (B8.pack ("token " ++ unpack (accessToken t))) $
+ addRequestHeader "Accept" "application/json" $
+ addRequestHeader "User-Agent" "aeris-server"
+ request'
+ response <- httpJSONEither request
+ case (getResponseBody response :: Either JSONException Object) of
+ Left err -> return Nothing
+ Right obj -> case lookupObjInt obj "id" of
+ Just githubId -> return $ Just $ pack $ show githubId
+ _ -> return Nothing
+
+-- Reddit
+getRedditConfig :: IO OAuth2Conf
+getRedditConfig =
OAuth2Conf
- <$> envAsString "DISCORD_CLIENT_ID" ""
- <*> envAsString "DISCORD_SECRET" ""
- <*> pure "https://discord.com/api/oauth2/token"
+ <$> envAsString "REDDIT_CLIENT_ID" ""
+ <*> envAsString "REDDIT_SECRET" ""
+ <*> pure "https://www.reddit.com/api/v1/access_token"
-getDiscordTokens :: String -> IO (Maybe ExternalToken)
-getDiscordTokens code = do
- cfg <- getDiscordConfig
+getRedditTokens :: String -> MaybeT IO ExternalToken
+getRedditTokens code = MaybeT $ do
+ cfg <- getRedditConfig
+ backUrl <- envAsString "BACK_URL" ""
+ let basicAuth = encodeBase64 $ B8.pack $ oauthClientId cfg ++ ":" ++ oauthClientSecret cfg
let endpoint = tokenEndpoint code cfg
request' <- parseRequest endpoint
let request =
setRequestMethod "POST" $
addRequestHeader "Accept" "application/json" $
+ addRequestHeader "User-Agent" "Aeris" $
+ addRequestHeader "Authorization" (B8.pack $ "Basic " ++ unpack basicAuth) $
setRequestBodyURLEncoded
- [ ("client_id", B8.pack . oauthClientId $ cfg)
- , ("client_secret", B8.pack . oauthClientSecret $ cfg)
- , ("code", B8.pack code)
- , ("grant_type", "authorization_code")
- , ("redirect_uri", "http://localhost:8080/auth/redirect")
+ [ ("grant_type", "authorization_code")
+ , ("redirect_uri", B8.pack $ backUrl ++ "auth/redirect")
]
request'
response <- httpJSONEither request
+ let (Right obj) = (getResponseBody response :: Either JSONException Object)
+ access <- liftMaybe $ lookupObjString obj "access_token"
+ refresh <- liftMaybe $ lookupObjString obj "refresh_token"
currTime <- getCurrentTime
- return $ case (getResponseBody response :: Either JSONException Object) of
- Left _ -> Nothing
- Right obj -> do
- access <- lookupObjString obj "access_token"
- refresh <- lookupObjString obj "refresh_token"
- expiresIn <- lookupObjInt obj "expires_in"
- let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
- Just $ ExternalToken (pack access) (pack refresh) expiresAt Discord
+ expiresIn <- liftMaybe $ lookupObjInt obj "expires_in"
+ let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
+ let t = ExternalToken access refresh expiresAt Reddit Nothing
+ id <- runMaybeT $ getRedditId t
+ return $ Just $ t { providerId = id }
+
+getRedditId :: ExternalToken -> MaybeT IO Text
+getRedditId t = MaybeT $ do
+ let endpoint = "https://oauth.reddit.com/api/v1/me"
+ request' <- parseRequest endpoint
+ let request =
+ addRequestHeader "Accept" "application/json" $
+ addRequestHeader "User-Agent" "Aeris" $
+ addRequestHeader "Authorization" (B8.pack $ "Bearer " ++ unpack (accessToken t))
+ request'
+ response <- httpJSONEither request
+ let (Right obj) = (getResponseBody response :: Either JSONException Object)
+ return $ lookupObjString obj "id"
-- GOOGLE
getGoogleConfig :: IO OAuth2Conf
@@ -106,9 +147,10 @@ getGoogleConfig =
<*> envAsString "GOOGLE_SECRET" ""
<*> pure "https://oauth2.googleapis.com/token"
-getGoogleTokens :: String -> IO (Maybe ExternalToken)
-getGoogleTokens code = do
+getGoogleTokens :: String -> MaybeT IO ExternalToken
+getGoogleTokens code = MaybeT $ do
cfg <- getGoogleConfig
+ backUrl <- envAsString "BACK_URL" ""
let endpoint = tokenEndpoint code cfg
request' <- parseRequest endpoint
let request =
@@ -119,19 +161,33 @@ getGoogleTokens code = do
, ("client_secret", B8.pack . oauthClientSecret $ cfg)
, ("code", B8.pack code)
, ("grant_type", "authorization_code")
- , ("redirect_uri", "http://localhost:8080/auth/redirect")
+ , ("redirect_uri", B8.pack $ backUrl ++ "auth/redirect")
]
request'
response <- httpJSONEither request
currTime <- getCurrentTime
- return $ case (getResponseBody response :: Either JSONException Object) of
- Left _ -> Nothing
- Right obj -> do
- access <- lookupObjString obj "access_token"
- refresh <- lookupObjString obj "refresh_token"
- expiresIn <- lookupObjInt obj "expires_in"
- let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
- Just $ ExternalToken (pack access) (pack refresh) expiresAt Google
+ let (Right obj) = (getResponseBody response :: Either JSONException Object)
+ access <- liftMaybe $ lookupObjString obj "access_token"
+ refresh <- liftMaybe $ lookupObjString obj "refresh_token"
+ expiresIn <- liftMaybe $ lookupObjInt obj "expires_in"
+ let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
+ let t = ExternalToken access refresh expiresAt Google Nothing
+ googleId <- runMaybeT $ getGoogleId t
+ return $ Just $ t { providerId = googleId }
+
+getGoogleId :: ExternalToken -> MaybeT IO Text
+getGoogleId t = MaybeT $ do
+ let endpoint = "https://oauth2.googleapis.com/tokeninfo"
+ request' <- parseRequest endpoint
+ let request =
+ addRequestHeader "Accept" "application/json" $
+ setRequestQueryString
+ [ ("access_token", Just . B8.pack . unpack $ accessToken t)
+ ]
+ request'
+ response <- httpJSONEither request
+ let (Right obj) = (getResponseBody response :: Either JSONException Object)
+ return $ lookupObjString obj "sub"
-- SPOTIFY
getSpotifyConfig :: IO OAuth2Conf
@@ -141,10 +197,10 @@ getSpotifyConfig =
<*> envAsString "SPOTIFY_SECRET" ""
<*> pure "https://accounts.spotify.com/api/token"
-getSpotifyTokens :: String -> IO (Maybe ExternalToken)
-getSpotifyTokens code = do
+getSpotifyTokens :: String -> MaybeT IO ExternalToken
+getSpotifyTokens code = MaybeT $ do
cfg <- getSpotifyConfig
-
+ backUrl <- envAsString "BACK_URL" ""
let basicAuth = encodeBase64 $ B8.pack $ oauthClientId cfg ++ ":" ++ oauthClientSecret cfg
let endpoint = tokenEndpoint code cfg
request' <- parseRequest endpoint
@@ -155,19 +211,31 @@ getSpotifyTokens code = do
setRequestBodyURLEncoded
[ ("code", B8.pack code)
, ("grant_type", "authorization_code")
- , ("redirect_uri", "http://localhost:8080/auth/redirect")
+ , ("redirect_uri", B8.pack $ backUrl ++ "auth/redirect")
]
request'
response <- httpJSONEither request
currTime <- getCurrentTime
- return $ case (getResponseBody response :: Either JSONException Object) of
- Left _ -> Nothing
- Right obj -> do
- access <- lookupObjString obj "access_token"
- refresh <- lookupObjString obj "refresh_token"
- expiresIn <- lookupObjInt obj "expires_in"
- let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
- Just $ ExternalToken (pack access) (pack refresh) expiresAt Spotify
+ let (Right obj) = (getResponseBody response :: Either JSONException Object)
+ access <- liftMaybe $ lookupObjString obj "access_token"
+ refresh <- liftMaybe $ lookupObjString obj "refresh_token"
+ expiresIn <- liftMaybe $ lookupObjInt obj "expires_in"
+ let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
+ let t = ExternalToken access refresh expiresAt Spotify Nothing
+ spotifyId <- runMaybeT $ getSpotifyId t
+ return $ Just $ t { providerId = spotifyId }
+
+getSpotifyId :: ExternalToken -> MaybeT IO Text
+getSpotifyId t = MaybeT $ do
+ let endpoint = "https://api.spotify.com/v1/me"
+ request' <- parseRequest endpoint
+ let request =
+ addRequestHeader "Content-Type" "application/json" $
+ addRequestHeader "Authorization" (B8.pack $ "Bearer " ++ unpack (accessToken t))
+ request'
+ response <- httpJSONEither request
+ let (Right obj) = (getResponseBody response :: Either JSONException Object)
+ return $ lookupObjString obj "id"
-- TWITTER
getTwitterConfig :: IO OAuth2Conf
@@ -177,9 +245,10 @@ getTwitterConfig =
<*> envAsString "TWITTER_SECRET" ""
<*> pure "https://api.twitter.com/2/oauth2/token"
-getTwitterTokens :: String -> IO (Maybe ExternalToken)
-getTwitterTokens code = do
+getTwitterTokens :: String -> MaybeT IO ExternalToken
+getTwitterTokens code = MaybeT $ do
cfg <- getTwitterConfig
+ backUrl <- envAsString "BACK_URL" ""
let basicAuth = encodeBase64 $ B8.pack $ "Basic " ++ oauthClientId cfg ++ ":" ++ oauthClientSecret cfg
let endpoint = tokenEndpoint code cfg
request' <- parseRequest endpoint
@@ -190,20 +259,34 @@ getTwitterTokens code = do
setRequestBodyURLEncoded
[ ("code", B8.pack code)
, ("grant_type", "authorization_code")
- , ("redirect_uri", "http://localhost:8080/auth/redirect")
+ , ("redirect_uri", B8.pack $ backUrl ++ "auth/redirect")
, ("code_verifier", "challenge")
]
request'
response <- httpJSONEither request
currTime <- getCurrentTime
- return $ case (getResponseBody response :: Either JSONException Object) of
- Left _ -> Nothing
- Right obj -> do
- access <- lookupObjString obj "access_token"
- refresh <- lookupObjString obj "refresh_token"
- expiresIn <- lookupObjInt obj "expires_in"
- let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
- Just $ ExternalToken (pack access) (pack refresh) expiresAt Twitter
+ let (Right obj) = (getResponseBody response :: Either JSONException Object)
+ access <- liftMaybe $ lookupObjString obj "access_token"
+ refresh <- liftMaybe $ lookupObjString obj "refresh_token"
+ expiresIn <- liftMaybe $ lookupObjInt obj "expires_in"
+ let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
+ let t = ExternalToken access refresh expiresAt Twitter Nothing
+ twitterId <- runMaybeT $ getTwitterId t
+ return $ Just $ t { providerId = twitterId }
+
+getTwitterId :: ExternalToken -> MaybeT IO Text
+getTwitterId t = MaybeT $ do
+ let endpoint = "https://api.twitter.com/2/users/me"
+ request' <- parseRequest endpoint
+ let request =
+ addRequestHeader "Content-Type" "application/json" $
+ addRequestHeader "Authorization" (B8.pack $ "Bearer " ++ unpack (accessToken t))
+ request'
+ response <- httpJSONEither request
+ let (Right obj) = (getResponseBody response :: Either JSONException Object)
+ case lookupObjObject obj "data" of
+ Just dataBody -> return $ lookupObjString dataBody "id"
+ _ -> return Nothing
-- ANILIST
getAnilistConfig :: IO OAuth2Conf
@@ -213,9 +296,10 @@ getAnilistConfig =
<*> envAsString "ANILIST_SECRET" ""
<*> pure "https://anilist.co/api/v2/oauth/token"
-getAnilistTokens :: String -> IO (Maybe ExternalToken)
-getAnilistTokens code = do
+getAnilistTokens :: String -> MaybeT IO ExternalToken
+getAnilistTokens code = MaybeT $ do
cfg <- getAnilistConfig
+ backUrl <- envAsString "BACK_URL" ""
let endpoint = tokenEndpoint code cfg
request' <- parseRequest endpoint
let request =
@@ -226,27 +310,47 @@ getAnilistTokens code = do
, ("client_secret", B8.pack . oauthClientSecret $ cfg)
, ("code", B8.pack code)
, ("grant_type", "authorization_code")
- , ("redirect_uri", "http://localhost:8080/auth/redirect")
+ , ("redirect_uri", B8.pack $ backUrl ++ "auth/redirect")
]
request'
response <- httpJSONEither request
currTime <- getCurrentTime
- return $ case (getResponseBody response :: Either JSONException Object) of
- Left _ -> Nothing
- Right obj -> do
- access <- lookupObjString obj "access_token"
- refresh <- lookupObjString obj "refresh_token"
- expiresIn <- lookupObjInt obj "expires_in"
- let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
- Just $ ExternalToken (pack access) (pack refresh) expiresAt Anilist
-
-
+ let (Right obj) = (getResponseBody response :: Either JSONException Object)
+ access <- liftMaybe $ lookupObjString obj "access_token"
+ refresh <- liftMaybe $ lookupObjString obj "refresh_token"
+ expiresIn <- liftMaybe $ lookupObjInt obj "expires_in"
+ let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
+ let t = ExternalToken access refresh expiresAt Anilist Nothing
+ anilistId <- runMaybeT $ getAnilistId t
+ return $ Just $ t { providerId = anilistId }
+getAnilistId :: ExternalToken -> MaybeT IO Text
+getAnilistId t = MaybeT $ do
+ let endpoint = "https://graphql.anilist.co"
+ request' <- parseRequest endpoint
+ let query = (decode "{Viewer {id,}}" :: Maybe Object)
+ let request =
+ addRequestHeader "Content-Type" "application/json" $
+ addRequestHeader "Authorization" (B8.pack $ "Bearer " ++ unpack (accessToken t)) $
+ addRequestHeader "User-Agent" "aeris-server" $
+ setRequestMethod "POST" $
+ setRequestBodyLBS "{\"query\": \"{Viewer {id}}\"}"
+ request'
+ response <- httpJSONEither request
+ let (Right obj) = (getResponseBody response :: Either JSONException Object)
+ case lookupObjObject obj "data" of
+ Just dataBody -> case liftMaybe $ lookupObjObject dataBody "Viewer" of
+ Just viewer -> case lookupObjInt viewer "id" of
+ Just anilistId -> return . Just . pack . show $ anilistId
+ _ -> return Nothing
+ _ -> return Nothing
+ _ -> return Nothing
+
-- General
-getOauthTokens :: Service -> String -> IO (Maybe ExternalToken)
+getOauthTokens :: Service -> String -> MaybeT IO ExternalToken
getOauthTokens Github = getGithubTokens
-getOauthTokens Discord = getDiscordTokens
+getOauthTokens Reddit = getRedditTokens
getOauthTokens Spotify = getSpotifyTokens
getOauthTokens Google = getGoogleTokens
getOauthTokens Twitter = getTwitterTokens
diff --git a/api/src/Core/User.hs b/api/src/Core/User.hs
index 3a7f7b2..9136718 100644
--- a/api/src/Core/User.hs
+++ b/api/src/Core/User.hs
@@ -26,7 +26,7 @@ newtype UserId = UserId {toInt64 :: Int64}
deriving newtype (DBEq, DBType, Eq, Show, Num, FromJSON, ToJSON, FromHttpApiData)
deriving stock (Generic)
-data Service = Github | Google | Spotify | Twitter | Discord | Anilist
+data Service = Github | Google | Spotify | Twitter | Reddit | Anilist
deriving (Eq, Show, Generic, ToJSON, FromJSON)
instance FromHttpApiData Service where
@@ -35,7 +35,7 @@ instance FromHttpApiData Service where
parseUrlPiece "google" = Right Google
parseUrlPiece "spotify" = Right Spotify
parseUrlPiece "twitter" = Right Twitter
- parseUrlPiece "discord" = Right Discord
+ parseUrlPiece "reddit" = Right Reddit
parseUrlPiece "anilist" = Right Anilist
parseUrlPiece _ = Left "not a service"
@@ -44,6 +44,7 @@ data ExternalToken = ExternalToken
, refreshToken :: Text
, expiresAt :: UTCTime
, service :: Service
+ , providerId :: Maybe Text
}
deriving (Eq, Show, Generic)
deriving anyclass (ToJSON, FromJSON)
diff --git a/api/src/Repository/User.hs b/api/src/Repository/User.hs
index ce3ef9b..a1659ae 100644
--- a/api/src/Repository/User.hs
+++ b/api/src/Repository/User.hs
@@ -1,12 +1,14 @@
module Repository.User where
import App (AppM)
-import Core.User (ExternalToken, UserId, Service)
+import Core.User (ExternalToken (service, providerId), UserId, Service)
import Data.Text (Text)
-import Db.User (User', getUserByName, getUserTokensById, insertUser, selectAllUser, updateUserTokens, getUserById, updateDelTokens)
+import Db.User (User', getUserByName, getUserTokensById, insertUser, selectAllUser, updateUserTokens, getUserById, updateDelTokens, UserDB (externalTokens))
import Rel8 (insert, select, update, limit, lit)
import Repository.Utils (runQuery)
import Data.Int (Int64)
+import Data.List (find)
+import Servant (err401, throwError)
users :: AppM [User']
users = runQuery (select selectAllUser)
@@ -19,8 +21,25 @@ getUserById' uid = do
res <- runQuery (select $ limit 1 $ getUserById (lit uid))
return $ head res
-createUser :: User' -> AppM [UserId]
-createUser user = runQuery (insert $ insertUser user)
+getUserByToken :: ExternalToken -> AppM User'
+getUserByToken t = do
+ users' <- users
+ case find findByToken users' of
+ Nothing -> throwError err401
+ Just x -> return x
+ where
+ findByToken :: User' -> Bool
+ findByToken usr = do
+ let userTokens = externalTokens usr
+ case find (\tok -> service tok == service t) userTokens of
+ Nothing -> False
+ Just tok -> providerId tok == providerId t
+
+
+createUser :: User' -> AppM User'
+createUser user = do
+ ids <- runQuery (insert $ insertUser user)
+ getUserById' $ head ids
getTokensByUserId :: UserId -> AppM [ExternalToken]
getTokensByUserId uid = do
diff --git a/api/src/Utils.hs b/api/src/Utils.hs
index 7a35ef2..bb6ab89 100644
--- a/api/src/Utils.hs
+++ b/api/src/Utils.hs
@@ -3,6 +3,7 @@
{-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-}
{-# LANGUAGE FlexibleInstances #-}
+
module Utils where
import Data.Aeson.Types (Value (String), Object)
@@ -18,16 +19,22 @@ import Db.Pipeline (Pipeline (Pipeline), PipelineId (PipelineId), pipelineLastTr
import Core.Pipeline (PipelineParams (PipelineParams))
import Data.Time (UTCTime (UTCTime), fromGregorian, secondsToDiffTime)
import Data.Default (Default, def)
-import Data.Aeson (Value(Number, Object), decode)
+import Data.Aeson (Value(Number, Object), decode, ToJSON, FromJSON)
import Data.Int (Int64)
import Data.Scientific ( toBoundedInteger )
+import GHC.Generics (Generic)
mapInd :: (a -> Int -> b) -> [a] -> [b]
mapInd f l = zipWith f l [0 ..]
-lookupObjString :: Object -> Text -> Maybe String
+lookupObjString :: Object -> Text -> Maybe Text
lookupObjString obj key = case Data.HashMap.Strict.lookup key obj of
- Just (String x) -> Just . unpack $ x
+ Just (String x) -> Just x
+ _ -> Nothing
+
+lookupObjObject :: Object -> Text -> Maybe Object
+lookupObjObject obj key = case Data.HashMap.Strict.lookup key obj of
+ Just (Object x) -> Just x
_ -> Nothing
@@ -55,4 +62,5 @@ instance Default (Pipeline Identity) where
}
type UserAuth = Servant.Auth.Server.Auth '[JWT] User
-type AuthRes = Servant.Auth.Server.AuthResult User
\ No newline at end of file
+type AuthRes = Servant.Auth.Server.AuthResult User
+
diff --git a/db/Dockerfile b/db/Dockerfile
new file mode 100644
index 0000000..2307466
--- /dev/null
+++ b/db/Dockerfile
@@ -0,0 +1,3 @@
+FROM postgres:14
+
+COPY db.sql /docker-entrypoint-initdb.d/
diff --git a/db/db.sql b/db/db.sql
new file mode 100644
index 0000000..3469156
--- /dev/null
+++ b/db/db.sql
@@ -0,0 +1,35 @@
+CREATE TABLE IF NOT EXISTS users (
+ id SERIAL PRIMARY KEY,
+ username VARCHAR(255) UNIQUE NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ slug VARCHAR(255) UNIQUE NOT NULL,
+ external_tokens JSONB[] NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS pipelines (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ type VARCHAR(255) NOT NULL,
+ params JSONB NOT NULL,
+ user_id INTEGER NOT NULL,
+ enabled BOOLEAN NOT NULL,
+ error TEXT,
+ trigger_count INTEGER NOT NULL,
+ last_trigger TIMESTAMP,
+ CONSTRAINT fk_user
+ FOREIGN KEY (user_id)
+ REFERENCES users(id)
+ ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS reactions (
+ id SERIAL PRIMARY KEY,
+ type VARCHAR(255) NOT NULL,
+ params JSONB NOT NULL,
+ pipeline_id INTEGER NOT NULL,
+ react_order INTEGER NOT NULL,
+ CONSTRAINT fk_pipeline
+ FOREIGN KEY (pipeline_id)
+ REFERENCES pipelines(id)
+ ON DELETE CASCADE
+);
diff --git a/docker-compose.yml b/docker-compose.yml
index feedd51..4734e8c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -20,6 +20,7 @@ services:
dockerfile: Dockerfile
args:
- API_ROUTE=/api
+ - MOBILE_APK_LOCATION=/client.apk
ports:
- "8081:80"
depends_on:
@@ -43,8 +44,8 @@ services:
- WORKER_API_KEY=${WORKER_API_KEY}
- WORKER_URL=${WORKER_URL}
- BACK_URL=${BACK_URL}
- - DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
- - DISCORD_SECRET=${DISCORD_SECRET}
+ - REDDIT_CLIENT_ID=${REDDIT_CLIENT_ID}
+ - REDDIT_SECRET=${REDDIT_SECRET}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
- GITHUB_SECRET=${GITHUB_SECRET}
- TWITTER_CLIENT_ID=${TWITTER_CLIENT_ID}
@@ -62,8 +63,8 @@ services:
depends_on:
- "api"
environment:
- - DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
- - DISCORD_SECRET=${DISCORD_SECRET}
+ - REDDIT_CLIENT_ID=${REDDIT_CLIENT_ID}
+ - REDDIT_SECRET=${REDDIT_SECRET}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
- GITHUB_SECRET=${GITHUB_SECRET}
- TWITTER_CLIENT_ID=${TWITTER_CLIENT_ID}
diff --git a/k8s/aeris/.helmignore b/k8s/aeris/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/k8s/aeris/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/k8s/aeris/Chart.yaml b/k8s/aeris/Chart.yaml
new file mode 100644
index 0000000..3624076
--- /dev/null
+++ b/k8s/aeris/Chart.yaml
@@ -0,0 +1,24 @@
+apiVersion: v2
+name: aeris
+description: A Helm chart for Kubernetes
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+# Versions are expected to follow Semantic Versioning (https://semver.org/)
+version: 0.1.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application. Versions are not expected to
+# follow Semantic Versioning. They should reflect the version the application is using.
+# It is recommended to use it with quotes.
+appVersion: "1.16.0"
diff --git a/k8s/aeris/templates/back/back.configmap.yaml b/k8s/aeris/templates/back/back.configmap.yaml
new file mode 100644
index 0000000..18adef2
--- /dev/null
+++ b/k8s/aeris/templates/back/back.configmap.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: configmap-back
+data:
+ WORKER_API_URL: http://api:8080/worker
+ WORKER_URL: http://worker:5000
+ BACK_URL: https://aeris.westeurope.cloudapp.azure.com/api/
diff --git a/k8s/aeris/templates/back/back.deployment.yaml b/k8s/aeris/templates/back/back.deployment.yaml
new file mode 100644
index 0000000..fbf980a
--- /dev/null
+++ b/k8s/aeris/templates/back/back.deployment.yaml
@@ -0,0 +1,36 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: aeris-back
+spec:
+ replicas: 3
+ selector:
+ matchLabels:
+ app: back
+ template:
+ metadata:
+ labels:
+ app: back
+ spec:
+ containers:
+ - name: back
+ imagePullPolicy: Always
+ image: {{ .Values.BACK_IMAGE }}
+ ports:
+ - containerPort: 8080
+ volumeMounts:
+ - mountPath: "/cache"
+ name: postgres-volume-claim
+ envFrom:
+ - configMapRef:
+ name: configmap-postgres
+ - configMapRef:
+ name: configmap-back
+ - secretRef:
+ name: secret-postgres
+ - secretRef:
+ name: secret-back
+ volumes:
+ - name: postgres-volume-claim
+ persistentVolumeClaim:
+ claimName: volume-claim-back
\ No newline at end of file
diff --git a/k8s/aeris/templates/back/back.service.yaml b/k8s/aeris/templates/back/back.service.yaml
new file mode 100644
index 0000000..bed46d6
--- /dev/null
+++ b/k8s/aeris/templates/back/back.service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: api-service
+spec:
+ type: LoadBalancer
+ # loadBalancerIP: "20.105.195.175"
+ selector:
+ app: back
+ ports:
+ - port: 8080
+ targetPort: 8080
\ No newline at end of file
diff --git a/k8s/aeris/templates/back/back.volume.yaml b/k8s/aeris/templates/back/back.volume.yaml
new file mode 100644
index 0000000..9950b0e
--- /dev/null
+++ b/k8s/aeris/templates/back/back.volume.yaml
@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: volume-back
+ labels:
+ type: local
+ name: volume-back
+spec:
+ storageClassName: manual
+ capacity:
+ storage: 50M
+ accessModes:
+ - ReadWriteOnce
+ hostPath:
+ path: "/mnt/data"
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: volume-claim-back
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 50M
\ No newline at end of file
diff --git a/k8s/aeris/templates/back/back_cluster.service.yaml b/k8s/aeris/templates/back/back_cluster.service.yaml
new file mode 100644
index 0000000..8b5e7e0
--- /dev/null
+++ b/k8s/aeris/templates/back/back_cluster.service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: api
+spec:
+ type: ClusterIP
+ # loadBalancerIP: "20.105.195.175"
+ selector:
+ app: back
+ ports:
+ - port: 8080
+ targetPort: 8080
\ No newline at end of file
diff --git a/k8s/aeris/templates/front/cluster-issuer.yaml b/k8s/aeris/templates/front/cluster-issuer.yaml
new file mode 100644
index 0000000..c5019d6
--- /dev/null
+++ b/k8s/aeris/templates/front/cluster-issuer.yaml
@@ -0,0 +1,18 @@
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+ name: letsencrypt
+spec:
+ acme:
+ server: https://acme-v02.api.letsencrypt.org/directory
+ email: louis.auzuret@gmail.com
+ privateKeySecretRef:
+ name: letsencrypt
+ solvers:
+ - http01:
+ ingress:
+ class: nginx
+ podTemplate:
+ spec:
+ nodeSelector:
+ "kubernetes.io/os": linux
diff --git a/k8s/aeris/templates/front/front.deployment.yaml b/k8s/aeris/templates/front/front.deployment.yaml
new file mode 100644
index 0000000..2f7088d
--- /dev/null
+++ b/k8s/aeris/templates/front/front.deployment.yaml
@@ -0,0 +1,20 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: aeris-front
+spec:
+ replicas: 3
+ selector:
+ matchLabels:
+ app: front
+ template:
+ metadata:
+ labels:
+ app: front
+ spec:
+ containers:
+ - name: front
+ imagePullPolicy: Always
+ image: {{ .Values.FRONT_IMAGE }}
+ ports:
+ - containerPort: 80
\ No newline at end of file
diff --git a/k8s/aeris/templates/front/front.ingress.yaml b/k8s/aeris/templates/front/front.ingress.yaml
new file mode 100644
index 0000000..c11504a
--- /dev/null
+++ b/k8s/aeris/templates/front/front.ingress.yaml
@@ -0,0 +1,52 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: aeris-ingress
+ annotations:
+ kubernetes.io/ingress.class: nginx
+ nginx.ingress.kubernetes.io/rewrite-target: /$1
+ nginx.ingress.kubernetes.io/use-regex: "true"
+ cert-manager.io/cluster-issuer: letsencrypt
+spec:
+ tls:
+ - hosts:
+ - aeris.westeurope.cloudapp.azure.com
+ secretName: tls-secret
+ rules:
+ - host: aeris.westeurope.cloudapp.azure.com
+ http:
+ paths:
+ - path: /(.*)
+ pathType: Prefix
+ backend:
+ service:
+ name: front-cluster
+ port:
+ number: 80
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: aeris-ingress-static
+ annotations:
+ kubernetes.io/ingress.class: nginx
+ nginx.ingress.kubernetes.io/rewrite-target: /static/$2
+ nginx.ingress.kubernetes.io/use-regex: "true"
+ cert-manager.io/cluster-issuer: letsencrypt
+spec:
+ tls:
+ - hosts:
+ - aeris.westeurope.cloudapp.azure.com
+ secretName: tls-secret
+ rules:
+ - host: aeris.westeurope.cloudapp.azure.com
+ http:
+ paths:
+ - path:
+ pathType: Prefix
+ backend:
+ service:
+ name: front-cluster
+ port:
+ number: 80
+ path: /static(/|$)(.*)
diff --git a/k8s/aeris/templates/front/front.service.yaml b/k8s/aeris/templates/front/front.service.yaml
new file mode 100644
index 0000000..24d1f26
--- /dev/null
+++ b/k8s/aeris/templates/front/front.service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: front-service
+spec:
+ type: LoadBalancer
+ #loadBalancerIP: "13.81.34.213"
+ selector:
+ app: front
+ ports:
+ - port: 80
+ targetPort: 80
\ No newline at end of file
diff --git a/k8s/aeris/templates/front/front_cluster.service.yaml b/k8s/aeris/templates/front/front_cluster.service.yaml
new file mode 100644
index 0000000..2a893eb
--- /dev/null
+++ b/k8s/aeris/templates/front/front_cluster.service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: front-cluster
+spec:
+ type: ClusterIP
+ #loadBalancerIP: "13.81.34.213"
+ selector:
+ app: front
+ ports:
+ - port: 80
+ targetPort: 80
\ No newline at end of file
diff --git a/k8s/aeris/templates/postgres/postgres.configmap.yaml b/k8s/aeris/templates/postgres/postgres.configmap.yaml
new file mode 100644
index 0000000..8efd3a7
--- /dev/null
+++ b/k8s/aeris/templates/postgres/postgres.configmap.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: configmap-postgres
+data:
+ POSTGRES_HOST: "postgres-service"
+ POSTGRES_PORT: "5432"
+ POSTGRES_DB: "aeris"
+ PGDATA: /var/lib/postgresql/data/d
\ No newline at end of file
diff --git a/k8s/aeris/templates/postgres/postgres.deployment.yaml b/k8s/aeris/templates/postgres/postgres.deployment.yaml
new file mode 100644
index 0000000..21c00ba
--- /dev/null
+++ b/k8s/aeris/templates/postgres/postgres.deployment.yaml
@@ -0,0 +1,34 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: postgres-deployment
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: postgres
+ template:
+ metadata:
+ name: postgres
+ labels:
+ app: postgres
+ spec:
+ containers:
+ - name: postgres
+ image: {{ .Values.DB_IMAGE }}
+ ports:
+ - containerPort: 5432
+ hostPort: 5432
+ volumeMounts:
+ - mountPath: "/var/lib/postgresql/data"
+ name: postgres-volume-claim
+ envFrom:
+ - configMapRef:
+ name: configmap-postgres
+ - secretRef:
+ name: secret-postgres
+ volumes:
+ - name: postgres-volume-claim
+ persistentVolumeClaim:
+ claimName: volume-claim-postgres
+ restartPolicy: Always
\ No newline at end of file
diff --git a/k8s/aeris/templates/postgres/postgres.service.yaml b/k8s/aeris/templates/postgres/postgres.service.yaml
new file mode 100644
index 0000000..6dbca56
--- /dev/null
+++ b/k8s/aeris/templates/postgres/postgres.service.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: postgres-service
+spec:
+ type: ClusterIP
+ selector:
+ app: postgres
+ ports:
+ - port: 5432
+ targetPort: 5432
\ No newline at end of file
diff --git a/k8s/aeris/templates/postgres/postgres.volume.yaml b/k8s/aeris/templates/postgres/postgres.volume.yaml
new file mode 100644
index 0000000..eec6598
--- /dev/null
+++ b/k8s/aeris/templates/postgres/postgres.volume.yaml
@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: volume-postgres
+ labels:
+ type: local
+ name: volume-postgres
+spec:
+ storageClassName: manual
+ capacity:
+ storage: 200M
+ accessModes:
+ - ReadWriteOnce
+ hostPath:
+ path: "/mnt/data"
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: volume-claim-postgres
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 200M
\ No newline at end of file
diff --git a/k8s/aeris/templates/worker/worker.deployment.yaml b/k8s/aeris/templates/worker/worker.deployment.yaml
new file mode 100644
index 0000000..43c020f
--- /dev/null
+++ b/k8s/aeris/templates/worker/worker.deployment.yaml
@@ -0,0 +1,26 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: aeris-worker
+spec:
+ replicas: 3
+ selector:
+ matchLabels:
+ app: worker
+ template:
+ metadata:
+ labels:
+ app: worker
+ spec:
+ restartPolicy: Always
+ containers:
+ - name: worker
+ imagePullPolicy: Always
+ image: {{ .Values.WORKER_IMAGE }}
+ ports:
+ - containerPort: 5000
+ envFrom:
+ - secretRef:
+ name: secret-back
+ - configMapRef:
+ name: configmap-back
\ No newline at end of file
diff --git a/k8s/aeris/templates/worker/worker.service.yaml b/k8s/aeris/templates/worker/worker.service.yaml
new file mode 100644
index 0000000..6a31656
--- /dev/null
+++ b/k8s/aeris/templates/worker/worker.service.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: worker
+spec:
+ type: ClusterIP
+ selector:
+ app: worker
+ ports:
+ - port: 5000
+ targetPort: 5000
\ No newline at end of file
diff --git a/k8s/aeris/values.yaml b/k8s/aeris/values.yaml
new file mode 100644
index 0000000..f488ced
--- /dev/null
+++ b/k8s/aeris/values.yaml
@@ -0,0 +1,4 @@
+BACK_IMAGE: aeris.azurecr.io/back
+FRONT_IMAGE: aeris.azurecr.io/front
+WORKER_IMAGE: aeris.azurecr.io/worker
+DB_IMAGE: aeris.azurecr.io/postgres
\ No newline at end of file
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index 704f300..ca84d3e 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -31,9 +31,7 @@
-
-
-
+