Hide activitystream entries whose corresponding object has been deleted
diff --git a/Allura/allura/model/timeline.py b/Allura/allura/model/timeline.py
index 9c4da0e..85d28ed 100644
--- a/Allura/allura/model/timeline.py
+++ b/Allura/allura/model/timeline.py
@@ -14,9 +14,10 @@
 #       KIND, either express or implied.  See the License for the
 #       specific language governing permissions and limitations
 #       under the License.
-
+from __future__ import annotations
 import bson
 import logging
+import typing
 
 from ming.odm import Mapper
 from tg import tmpl_context as c
@@ -28,6 +29,10 @@
 from allura.lib import security
 from allura.tasks.activity_tasks import create_timelines
 
+if typing.TYPE_CHECKING:
+    import allura.model as M
+    from activitystream.storage.mingstorage import Activity
+
 log = logging.getLogger(__name__)
 
 
@@ -45,9 +50,9 @@
 
         from allura.model.project import Project
         super().create_activity(actor, verb, obj,
-                                              target=target,
-                                              related_nodes=related_nodes,
-                                              tags=tags)
+                                target=target,
+                                related_nodes=related_nodes,
+                                tags=tags)
         # aggregate actor and follower's timelines
         if actor.node_id:
             create_timelines.post(actor.node_id)
@@ -115,15 +120,22 @@
         self.activity_name = activity_name
 
 
+def get_allura_id(activity_object_dict):
+    extras_dict = activity_object_dict.activity_extras
+    if not extras_dict:
+        return None
+    allura_id = extras_dict.get('allura_id')
+    if not allura_id:
+        return None
+    return allura_id
+
+
 def get_activity_object(activity_object_dict):
     """Given a BSON-serialized activity object (e.g. activity.obj dict in a
     timeline), return the corresponding :class:`ActivityObject`.
 
     """
-    extras_dict = activity_object_dict.activity_extras
-    if not extras_dict:
-        return None
-    allura_id = extras_dict.get('allura_id')
+    allura_id = get_allura_id(activity_object_dict)
     if not allura_id:
         return None
     classname, _id = allura_id.split(':', 1)
@@ -132,15 +144,23 @@
         _id = bson.ObjectId(_id)
     except bson.errors.InvalidId:
         pass
-    return cls.query.get(_id=_id)
+    obj = cls.query.get(_id=_id)
+    return obj
 
 
-def perm_check(user):
+def perm_check(user: M.User):
     """
     Return a function that returns True if ``user`` has 'read' access to a given activity,
     otherwise returns False.
     """
-    def _perm_check(activity):
+    def _perm_check(activity: Activity):
+        if not get_allura_id(activity.obj):
+            # include activity records that do not have an allura object
+            return True
         obj = get_activity_object(activity.obj)
-        return obj is None or obj.has_activity_access('read', user, activity)
+        if obj is None:
+            # Do not include if there's supposed to be an allura object, but it's missing
+            return False
+        # Finally, if there's an allura object, perform a permission check
+        return obj.has_activity_access('read', user, activity)
     return _perm_check