Browse Source

Merge pull request #123 from simple-login/get-alias-v2

Add GET /api/v2/aliases
Son Nguyen Kim 5 năm trước cách đây
mục cha
commit
cb6a18b1bf
5 tập tin đã thay đổi với 306 bổ sung30 xóa
  1. 62 25
      README.md
  2. 102 4
      app/api/serializer.py
  3. 54 1
      app/api/views/alias.py
  4. 11 0
      app/models.py
  5. 77 0
      tests/api/test_alias.py

+ 62 - 25
README.md

@@ -831,7 +831,8 @@ Input:
 
 Output: always return 200, even if email doesn't exist. User need to enter correctly their email.
 
-#### GET /api/aliases
+
+#### GET /api/v2/aliases
 
 Get user aliases.
 
@@ -841,34 +842,70 @@ Input:
 - (Optional) query: included in request body. Some frameworks might prevent GET request having a non-empty body, in this case this endpoint also supports POST. 
 
 Output:
-If success, 200 with the list of aliases, for example:
+If success, 200 with the list of aliases. Each alias has the following fields:
+
+- id
+- email
+- enabled
+- creation_timestamp
+- note
+- nb_block
+- nb_forward
+- nb_reply
+- (optional) latest_activity:
+    - action: forward|reply|block|bounced
+    - timestamp
+    - contact:
+        - email
+        - name
+        - reverse_alias
+
+Here's an example:
 
 ```json
 {
-    "aliases": [
-        {
-            "creation_date": "2020-02-04 16:23:02+00:00",
-            "creation_timestamp": 1580833382,
-            "email": "e3@.alo@sl.local",
-            "id": 4,
-            "nb_block": 0,
-            "nb_forward": 0,
-            "nb_reply": 0,
-            "enabled": true,
-            "note": "This is a note"
+  "aliases": [
+    {
+      "creation_date": "2020-04-06 17:57:14+00:00",
+      "creation_timestamp": 1586195834,
+      "email": "prefix1.cat@sl.local",
+      "enabled": true,
+      "id": 3,
+      "latest_activity": {
+        "action": "forward",
+        "contact": {
+          "email": "c1@example.com",
+          "name": null,
+          "reverse_alias": "\"c1 at example.com\" <re1@SL>"
         },
-        {
-            "creation_date": "2020-02-04 16:23:02+00:00",
-            "creation_timestamp": 1580833382,
-            "email": "e2@.meo@sl.local",
-            "id": 3,
-            "nb_block": 0,
-            "nb_forward": 0,
-            "nb_reply": 0,
-            "enabled": false,
-            "note": null
-        }
-    ]
+        "timestamp": 1586195834
+      },
+      "nb_block": 0,
+      "nb_forward": 1,
+      "nb_reply": 0,
+      "note": null
+    },
+    {
+      "creation_date": "2020-04-06 17:57:14+00:00",
+      "creation_timestamp": 1586195834,
+      "email": "prefix0.hey@sl.local",
+      "enabled": true,
+      "id": 2,
+      "latest_activity": {
+        "action": "forward",
+        "contact": {
+          "email": "c0@example.com",
+          "name": null,
+          "reverse_alias": "\"c0 at example.com\" <re0@SL>"
+        },
+        "timestamp": 1586195834
+      },
+      "nb_block": 0,
+      "nb_forward": 1,
+      "nb_reply": 0,
+      "note": null
+    }
+  ]
 }
 ```
 

+ 102 - 4
app/api/serializer.py

@@ -1,12 +1,11 @@
 from dataclasses import dataclass
 
-from sqlalchemy import or_
-from sqlalchemy.orm import joinedload
+from arrow import Arrow
+from sqlalchemy import or_, func, case
 
 from app.config import PAGE_LIMIT
 from app.extensions import db
-
-from app.models import Alias, Mailbox, Contact, EmailLog
+from app.models import Alias, Contact, EmailLog, Mailbox
 
 
 @dataclass
@@ -17,6 +16,9 @@ class AliasInfo:
     nb_blocked: int
     nb_reply: int
 
+    latest_email_log: EmailLog = None
+    latest_contact: Contact = None
+
 
 def serialize_alias_info(alias_info: AliasInfo) -> dict:
     return {
@@ -34,6 +36,36 @@ def serialize_alias_info(alias_info: AliasInfo) -> dict:
     }
 
 
+def serialize_alias_info_v2(alias_info: AliasInfo) -> dict:
+    res = {
+        # Alias field
+        "id": alias_info.alias.id,
+        "email": alias_info.alias.email,
+        "creation_date": alias_info.alias.created_at.format(),
+        "creation_timestamp": alias_info.alias.created_at.timestamp,
+        "enabled": alias_info.alias.enabled,
+        "note": alias_info.alias.note,
+        # activity
+        "nb_forward": alias_info.nb_forward,
+        "nb_block": alias_info.nb_blocked,
+        "nb_reply": alias_info.nb_reply,
+    }
+    if alias_info.latest_email_log:
+        email_log = alias_info.latest_email_log
+        contact = alias_info.latest_contact
+        # latest activity
+        res["latest_activity"] = {
+            "timestamp": email_log.created_at.timestamp,
+            "action": email_log.get_action(),
+            "contact": {
+                "email": contact.website_email,
+                "name": contact.name,
+                "reverse_alias": contact.website_send_to(),
+            },
+        }
+    return res
+
+
 def serialize_contact(contact: Contact) -> dict:
     res = {
         "id": contact.id,
@@ -74,6 +106,40 @@ def get_alias_infos_with_pagination(user, page_id=0, query=None) -> [AliasInfo]:
     return ret
 
 
+def get_alias_infos_with_pagination_v2(user, page_id=0, query=None) -> [AliasInfo]:
+    ret = []
+    latest_activity = func.max(
+        case(
+            [
+                (Alias.created_at > EmailLog.created_at, Alias.created_at),
+                (Alias.created_at < EmailLog.created_at, EmailLog.created_at),
+            ],
+            else_=Alias.created_at,
+        )
+    ).label("latest")
+
+    q = (
+        db.session.query(Alias, latest_activity)
+        .join(Contact, Alias.id == Contact.alias_id, isouter=True)
+        .join(EmailLog, Contact.id == EmailLog.contact_id, isouter=True)
+        .filter(Alias.user_id == user.id)
+        .group_by(Alias.id)
+        .order_by(latest_activity.desc())
+    )
+
+    if query:
+        q = q.filter(
+            or_(Alias.email.ilike(f"%{query}%"), Alias.note.ilike(f"%{query}%"))
+        )
+
+    q = q.limit(PAGE_LIMIT).offset(page_id * PAGE_LIMIT)
+
+    for alias, latest_activity in q:
+        ret.append(get_alias_info_v2(alias))
+
+    return ret
+
+
 def get_alias_info(alias: Alias) -> AliasInfo:
     q = (
         db.session.query(Contact, EmailLog)
@@ -94,6 +160,38 @@ def get_alias_info(alias: Alias) -> AliasInfo:
     return alias_info
 
 
+def get_alias_info_v2(alias: Alias) -> AliasInfo:
+    q = (
+        db.session.query(Contact, EmailLog)
+        .filter(Contact.alias_id == alias.id)
+        .filter(EmailLog.contact_id == Contact.id)
+    )
+
+    latest_activity: Arrow = alias.created_at
+    latest_email_log = None
+    latest_contact = None
+
+    alias_info = AliasInfo(alias=alias, nb_blocked=0, nb_forward=0, nb_reply=0,)
+
+    for contact, email_log in q:
+        if email_log.is_reply:
+            alias_info.nb_reply += 1
+        elif email_log.blocked:
+            alias_info.nb_blocked += 1
+        else:
+            alias_info.nb_forward += 1
+
+        if email_log.created_at > latest_activity:
+            latest_activity = email_log.created_at
+            latest_email_log = email_log
+            latest_contact = contact
+
+    alias_info.latest_contact = latest_contact
+    alias_info.latest_email_log = latest_email_log
+
+    return alias_info
+
+
 def get_alias_contacts(alias, page_id: int) -> [dict]:
     q = (
         Contact.query.filter_by(alias_id=alias.id)

+ 54 - 1
app/api/views/alias.py

@@ -11,6 +11,8 @@ from app.api.serializer import (
     get_alias_infos_with_pagination,
     get_alias_info,
     get_alias_contacts,
+    get_alias_infos_with_pagination_v2,
+    serialize_alias_info_v2,
 )
 from app.config import EMAIL_DOMAIN
 from app.dashboard.views.alias_log import get_alias_log
@@ -64,6 +66,57 @@ def get_aliases():
     )
 
 
+@api_bp.route("/v2/aliases", methods=["GET", "POST"])
+@cross_origin()
+@verify_api_key
+def get_aliases_v2():
+    """
+    Get aliases
+    Input:
+        page_id: in query
+    Output:
+        - aliases: list of alias:
+            - id
+            - email
+            - creation_date
+            - creation_timestamp
+            - nb_forward
+            - nb_block
+            - nb_reply
+            - note
+            - (optional) latest_activity:
+                - timestamp
+                - action: forward|reply|block|bounced
+                - contact:
+                    - email
+                    - name
+                    - reverse_alias
+
+
+    """
+    user = g.user
+    try:
+        page_id = int(request.args.get("page_id"))
+    except (ValueError, TypeError):
+        return jsonify(error="page_id must be provided in request query"), 400
+
+    query = None
+    data = request.get_json(silent=True)
+    if data:
+        query = data.get("query")
+
+    alias_infos: [AliasInfo] = get_alias_infos_with_pagination_v2(
+        user, page_id=page_id, query=query
+    )
+
+    return (
+        jsonify(
+            aliases=[serialize_alias_info_v2(alias_info) for alias_info in alias_infos]
+        ),
+        200,
+    )
+
+
 @api_bp.route("/aliases/<int:alias_id>", methods=["DELETE"])
 @cross_origin()
 @verify_api_key
@@ -127,7 +180,7 @@ def get_alias_activities(alias_id):
             - from
             - to
             - timestamp
-            - action: forward|reply|block
+            - action: forward|reply|block|bounced
             - reverse_alias
 
     """

+ 11 - 0
app/models.py

@@ -848,6 +848,17 @@ class EmailLog(db.Model, ModelMixin):
 
     contact = db.relationship(Contact)
 
+    def get_action(self) -> str:
+        """return the action name: forward|reply|block|bounced"""
+        if self.is_reply:
+            return "reply"
+        elif self.bounced:
+            return "bounced"
+        elif self.blocked:
+            return "blocked"
+        else:
+            return "forward"
+
 
 class Subscription(db.Model, ModelMixin):
     # Come from Paddle

+ 77 - 0
tests/api/test_alias.py

@@ -1,3 +1,5 @@
+import json
+
 from flask import url_for
 
 from flask import url_for
@@ -101,6 +103,81 @@ def test_get_aliases_with_pagination(flask_client):
     assert len(r.json["aliases"]) == 1
 
 
+def test_get_aliases_v2(flask_client):
+    user = User.create(
+        email="a@b.c", password="password", name="Test User", activated=True
+    )
+    db.session.commit()
+
+    # create api_key
+    api_key = ApiKey.create(user.id, "for test")
+    db.session.commit()
+
+    a0 = Alias.create_new(user, "prefix0")
+    a1 = Alias.create_new(user, "prefix1")
+    db.session.commit()
+
+    # add activity for a0
+    c0 = Contact.create(
+        user_id=user.id,
+        alias_id=a0.id,
+        website_email="c0@example.com",
+        reply_email="re0@SL",
+    )
+    db.session.commit()
+    EmailLog.create(contact_id=c0.id, user_id=user.id)
+    db.session.commit()
+
+    # a1 has more recent activity
+    c1 = Contact.create(
+        user_id=user.id,
+        alias_id=a1.id,
+        website_email="c1@example.com",
+        reply_email="re1@SL",
+    )
+    db.session.commit()
+    EmailLog.create(contact_id=c1.id, user_id=user.id)
+    db.session.commit()
+
+    # get aliases v2
+    r = flask_client.get(
+        url_for("api.get_aliases_v2", page_id=0),
+        headers={"Authentication": api_key.code},
+    )
+    assert r.status_code == 200
+
+    # make sure a1 is returned before a0
+    r0 = r.json["aliases"][0]
+    # r0 will have the following format
+    # {
+    #   "creation_date": "2020-04-06 17:52:47+00:00",
+    #   "creation_timestamp": 1586195567,
+    #   "email": "prefix1.hey@sl.local",
+    #   "enabled": true,
+    #   "id": 3,
+    #   "latest_activity": {
+    #     "action": "forward",
+    #     "contact": {
+    #       "email": "c1@example.com",
+    #       "name": null,
+    #       "reverse_alias": "\"c1 at example.com\" <re1@SL>"
+    #     },
+    #     "timestamp": 1586195567
+    #   },
+    #   "nb_block": 0,
+    #   "nb_forward": 1,
+    #   "nb_reply": 0,
+    #   "note": null
+    # }
+    assert r0["email"].startswith("prefix1")
+    assert r0["latest_activity"]["action"] == "forward"
+    assert "timestamp" in r0["latest_activity"]
+
+    assert r0["latest_activity"]["contact"]["email"] == "c1@example.com"
+    assert "name" in r0["latest_activity"]["contact"]
+    assert "reverse_alias" in r0["latest_activity"]["contact"]
+
+
 def test_delete_alias(flask_client):
     user = User.create(
         email="a@b.c", password="password", name="Test User", activated=True