Add part of the ability to accept follow requests.

This commit is contained in:
Andrew Pietila 2024-08-01 02:18:48 -05:00
parent 0a0dec7b86
commit d6d8a86704
11 changed files with 1101 additions and 0 deletions

View file

@ -0,0 +1,7 @@
CREATE TABLE `remoteUser` (
`id` integer PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`domain` text,
`cachedActivity` text,
`expiry` integer
);

View file

@ -0,0 +1,8 @@
CREATE TABLE `followers` (
`id` integer PRIMARY KEY NOT NULL,
`followingCollection` text NOT NULL,
`userid` integer,
`remoteUserid` integer,
FOREIGN KEY (`userid`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`remoteUserid`) REFERENCES `remoteUser`(`id`) ON UPDATE no action ON DELETE no action
);

View file

@ -0,0 +1,8 @@
CREATE TABLE `followAcceptActivity` (
`id` integer PRIMARY KEY NOT NULL,
`userid` integer NOT NULL,
`remoteUserid` integer NOT NULL,
`activity` text,
FOREIGN KEY (`userid`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`remoteUserid`) REFERENCES `remoteUser`(`id`) ON UPDATE no action ON DELETE no action
);

View file

@ -0,0 +1 @@
ALTER TABLE `remoteUser` ADD `actor` text;

View file

@ -0,0 +1,157 @@
{
"version": "6",
"dialect": "sqlite",
"id": "26aaeabf-e6e2-4723-80ec-33520c4d49f1",
"prevId": "2ef072ba-1781-4656-80d5-2ae9c3e29638",
"tables": {
"pubPrivKeys": {
"name": "pubPrivKeys",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"userid": {
"name": "userid",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"publicKey": {
"name": "publicKey",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"privateKey": {
"name": "privateKey",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"pubPrivKeys_userid_user_id_fk": {
"name": "pubPrivKeys_userid_user_id_fk",
"tableFrom": "pubPrivKeys",
"tableTo": "user",
"columnsFrom": [
"userid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"remoteUser": {
"name": "remoteUser",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"domain": {
"name": "domain",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cachedActivity": {
"name": "cachedActivity",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expiry": {
"name": "expiry",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"pwhash": {
"name": "pwhash",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"activity": {
"name": "activity",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_name_unique": {
"name": "user_name_unique",
"columns": [
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View file

@ -0,0 +1,221 @@
{
"version": "6",
"dialect": "sqlite",
"id": "9abdf6af-70ff-42af-83a5-cfe7f3318620",
"prevId": "26aaeabf-e6e2-4723-80ec-33520c4d49f1",
"tables": {
"followers": {
"name": "followers",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"followingCollection": {
"name": "followingCollection",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userid": {
"name": "userid",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"remoteUserid": {
"name": "remoteUserid",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"followers_userid_user_id_fk": {
"name": "followers_userid_user_id_fk",
"tableFrom": "followers",
"tableTo": "user",
"columnsFrom": [
"userid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"followers_remoteUserid_remoteUser_id_fk": {
"name": "followers_remoteUserid_remoteUser_id_fk",
"tableFrom": "followers",
"tableTo": "remoteUser",
"columnsFrom": [
"remoteUserid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"pubPrivKeys": {
"name": "pubPrivKeys",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"userid": {
"name": "userid",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"publicKey": {
"name": "publicKey",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"privateKey": {
"name": "privateKey",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"pubPrivKeys_userid_user_id_fk": {
"name": "pubPrivKeys_userid_user_id_fk",
"tableFrom": "pubPrivKeys",
"tableTo": "user",
"columnsFrom": [
"userid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"remoteUser": {
"name": "remoteUser",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"domain": {
"name": "domain",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cachedActivity": {
"name": "cachedActivity",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expiry": {
"name": "expiry",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"pwhash": {
"name": "pwhash",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"activity": {
"name": "activity",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_name_unique": {
"name": "user_name_unique",
"columns": [
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View file

@ -0,0 +1,285 @@
{
"version": "6",
"dialect": "sqlite",
"id": "df1b4c75-8b8f-4e4b-b942-f98ee63fc784",
"prevId": "9abdf6af-70ff-42af-83a5-cfe7f3318620",
"tables": {
"followAcceptActivity": {
"name": "followAcceptActivity",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"userid": {
"name": "userid",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"remoteUserid": {
"name": "remoteUserid",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"activity": {
"name": "activity",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"followAcceptActivity_userid_user_id_fk": {
"name": "followAcceptActivity_userid_user_id_fk",
"tableFrom": "followAcceptActivity",
"tableTo": "user",
"columnsFrom": [
"userid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"followAcceptActivity_remoteUserid_remoteUser_id_fk": {
"name": "followAcceptActivity_remoteUserid_remoteUser_id_fk",
"tableFrom": "followAcceptActivity",
"tableTo": "remoteUser",
"columnsFrom": [
"remoteUserid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"followers": {
"name": "followers",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"followingCollection": {
"name": "followingCollection",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userid": {
"name": "userid",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"remoteUserid": {
"name": "remoteUserid",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"followers_userid_user_id_fk": {
"name": "followers_userid_user_id_fk",
"tableFrom": "followers",
"tableTo": "user",
"columnsFrom": [
"userid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"followers_remoteUserid_remoteUser_id_fk": {
"name": "followers_remoteUserid_remoteUser_id_fk",
"tableFrom": "followers",
"tableTo": "remoteUser",
"columnsFrom": [
"remoteUserid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"pubPrivKeys": {
"name": "pubPrivKeys",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"userid": {
"name": "userid",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"publicKey": {
"name": "publicKey",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"privateKey": {
"name": "privateKey",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"pubPrivKeys_userid_user_id_fk": {
"name": "pubPrivKeys_userid_user_id_fk",
"tableFrom": "pubPrivKeys",
"tableTo": "user",
"columnsFrom": [
"userid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"remoteUser": {
"name": "remoteUser",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"domain": {
"name": "domain",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cachedActivity": {
"name": "cachedActivity",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expiry": {
"name": "expiry",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"pwhash": {
"name": "pwhash",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"activity": {
"name": "activity",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_name_unique": {
"name": "user_name_unique",
"columns": [
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View file

@ -0,0 +1,292 @@
{
"version": "6",
"dialect": "sqlite",
"id": "071e7c99-3a77-42dd-b86f-148bf1b146db",
"prevId": "df1b4c75-8b8f-4e4b-b942-f98ee63fc784",
"tables": {
"followAcceptActivity": {
"name": "followAcceptActivity",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"userid": {
"name": "userid",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"remoteUserid": {
"name": "remoteUserid",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"activity": {
"name": "activity",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"followAcceptActivity_userid_user_id_fk": {
"name": "followAcceptActivity_userid_user_id_fk",
"tableFrom": "followAcceptActivity",
"tableTo": "user",
"columnsFrom": [
"userid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"followAcceptActivity_remoteUserid_remoteUser_id_fk": {
"name": "followAcceptActivity_remoteUserid_remoteUser_id_fk",
"tableFrom": "followAcceptActivity",
"tableTo": "remoteUser",
"columnsFrom": [
"remoteUserid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"followers": {
"name": "followers",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"followingCollection": {
"name": "followingCollection",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userid": {
"name": "userid",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"remoteUserid": {
"name": "remoteUserid",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"followers_userid_user_id_fk": {
"name": "followers_userid_user_id_fk",
"tableFrom": "followers",
"tableTo": "user",
"columnsFrom": [
"userid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"followers_remoteUserid_remoteUser_id_fk": {
"name": "followers_remoteUserid_remoteUser_id_fk",
"tableFrom": "followers",
"tableTo": "remoteUser",
"columnsFrom": [
"remoteUserid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"pubPrivKeys": {
"name": "pubPrivKeys",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"userid": {
"name": "userid",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"publicKey": {
"name": "publicKey",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"privateKey": {
"name": "privateKey",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"pubPrivKeys_userid_user_id_fk": {
"name": "pubPrivKeys_userid_user_id_fk",
"tableFrom": "pubPrivKeys",
"tableTo": "user",
"columnsFrom": [
"userid"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"remoteUser": {
"name": "remoteUser",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"domain": {
"name": "domain",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"actor": {
"name": "actor",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cachedActivity": {
"name": "cachedActivity",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expiry": {
"name": "expiry",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"pwhash": {
"name": "pwhash",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"activity": {
"name": "activity",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_name_unique": {
"name": "user_name_unique",
"columns": [
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View file

@ -15,6 +15,34 @@
"when": 1722228088866,
"tag": "0001_public_private_keys_table",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1722489767897,
"tag": "0002_remote_users_table",
"breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1722493945807,
"tag": "0003_followers_table",
"breakpoints": true
},
{
"idx": 4,
"version": "6",
"when": 1722495120997,
"tag": "0004_follow_accept_activity_table",
"breakpoints": true
},
{
"idx": 5,
"version": "6",
"when": 1722495162448,
"tag": "0005_remote_user_cached_actor_url_column",
"breakpoints": true
}
]
}

View file

@ -0,0 +1,71 @@
import { NextRequest } from "next/server";
import * as jsonld from 'jsonld';
import db from "@/db/drizzle";
import { followAcceptActivity, remoteUser, user } from "@/db/schema";
import { eq } from "drizzle-orm";
export const dynamic = 'force-dynamic';
export async function POST(request: NextRequest) {
// TODO: HTTP Signature Validation (middleware?)
const originBody = await request.json();
const jsonldBody = await jsonld.compact(originBody, ["https://www.w3.org/ns/activitystreams"]);
if ( jsonldBody.type === "Follow" ) {
for ( let toActor of jsonldBody.to ) {
let toActorUrl = new URL(toActor);
let matches = toActorUrl.pathname.match(/\/api\/activitypub\/user\/(.*)/);
// TODO: Check domain as well. Hopefully brainz gets installed on multiple instances, and this'll cause problems.
if ( matches && matches[1] ) {
const userRow = await db.select({id: user.id}).from(user).where(eq(user.name, matches[1]));
if ( userRow[0] ) {
// TODO: Implement user consent, moderation.
let response = {
"@context": ["https://www.w3.org/ns/activitystreams"],
"actor": toActor,
"object": {...jsonldBody},
"to": [jsonldBody.actor],
"type": "Accept",
"id": "PLACEHOLDER"
}
delete response.object["@context"];
delete response.object.to;
// First, check the validity of the cache.
const remoteUserRow = await db.select({id: remoteUser.id, cachedActivity: remoteUser.cachedActivity, cacheTimeout: remoteUser.cacheTimeout}).from(remoteUser).where(eq(remoteUser.cachedActorUrl, response.object.actor));
let remoteUserId;
if ( remoteUserRow[0]?.cacheTimeout ) {
if (Date.now() < remoteUserRow[0].cacheTimeout ) {
remoteUserId = remoteUserRow[0].id;
}
}
// TODO: What to do if that didn't provide a remoteUserId.
if ( remoteUserId ) {
const placeholderJSON = JSON.stringify(response, null, 4);
await db.insert(followAcceptActivity).values({user: userRow[0].id, remoteUserid: remoteUserId, activity: placeholderJSON});
const assignedId = await db.select({id: followAcceptActivity.id}).from(followAcceptActivity).where(eq(followAcceptActivity.activity, placeholderJSON));
if ( assignedId[0]?.id ) {
response.id = new URL(`/api/activitypub/follow_accept/${assignedId[0].id}`, toActorUrl).toString();
await db.update(followAcceptActivity).set({activity: JSON.stringify(response, null, 4)}).where(eq(followAcceptActivity.id, assignedId[0].id));
}
}
// TODO: Emit the activity.
return new Response("", {
status: 204
})
}
}
}
return new Response("", {
status: 401 // Not authorized, seems appropriate here.
})
}
return new Response("", {
status: 501 // Not implemented.
})
}

View file

@ -13,4 +13,27 @@ export const pubPrivKeys = sqliteTable("pubPrivKeys", {
user: int("userid").references(() => user.id),
publicKey: text("publicKey"),
privateKey: text("privateKey")
})
export const remoteUser = sqliteTable("remoteUser", {
id: int("id").primaryKey(),
name: text("name").notNull(),
domain: text("domain"),
cachedActorUrl: text("actor"),
cachedActivity: text("cachedActivity"),
cacheTimeout: int("expiry")
})
export const followers = sqliteTable("followers", {
id: int("id").primaryKey(),
followingCollection: text("followingCollection").notNull(),
userid: int("userid").references(() => user.id),
remoteUserid: int("remoteUserid").references(() => remoteUser.id)
})
export const followAcceptActivity = sqliteTable("followAcceptActivity", {
id: int("id").primaryKey(),
user: int("userid").references(() => user.id).notNull(),
remoteUserid: int("remoteUserid").references(() => remoteUser.id).notNull(),
activity: text("activity")
})