From 30c1be654525979b20518c8e84a12422d6bd37da Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:47:09 +0100 Subject: [PATCH 01/40] feat: add protocol support for register, transfer and accept --- packages/frontend-main/ARCHITECTURE.md | 6 ++ packages/lib-api-types/src/posts/index.ts | 25 +++++++ packages/reader-main/src/messages/accept.ts | 70 ++++++++++++++++++ packages/reader-main/src/messages/index.ts | 6 ++ packages/reader-main/src/messages/register.ts | 70 ++++++++++++++++++ packages/reader-main/src/messages/transfer.ts | 71 +++++++++++++++++++ packages/tool-network-spammer/src/logic.ts | 1 + 7 files changed, 249 insertions(+) create mode 100644 packages/reader-main/src/messages/accept.ts create mode 100644 packages/reader-main/src/messages/register.ts create mode 100644 packages/reader-main/src/messages/transfer.ts diff --git a/packages/frontend-main/ARCHITECTURE.md b/packages/frontend-main/ARCHITECTURE.md index 7508d88d..a87cfee1 100644 --- a/packages/frontend-main/ARCHITECTURE.md +++ b/packages/frontend-main/ARCHITECTURE.md @@ -193,6 +193,11 @@ dither.Flag('0xghi789...', 'spam'); // User management dither.Unfollow('cosmos1user...'); dither.Dislike('0xjkl012...'); + +// Username registration and transfer to a new address +dither.Register('handle'); +dither.Transfer('handle', '0xabc123...'); +dither.Accept('handle'); ``` ### Supported Wallets @@ -212,3 +217,4 @@ dither.Dislike('0xjkl012...'); - **Reply threading** with nested conversations - **User tipping** and social interactions - **Content moderation** through flagging system +- **Usernames** by registering transferable handles diff --git a/packages/lib-api-types/src/posts/index.ts b/packages/lib-api-types/src/posts/index.ts index 446dfca2..af17c9fa 100644 --- a/packages/lib-api-types/src/posts/index.ts +++ b/packages/lib-api-types/src/posts/index.ts @@ -100,6 +100,31 @@ export const UnfollowBodySchema = t.Object({ }); export type UnfollowBody = Static; +export const RegisterBodySchema = t.Object({ + hash: t.String(), + from: t.String(), + handle: t.String(), + timestamp: t.String(), +}); +export type RegisterBody = Static; + +export const TransferBodySchema = t.Object({ + hash: t.String(), + from: t.String(), + handle: t.String(), + to_address: t.String(), + timestamp: t.String(), +}); +export type TransferBody = Static; + +export const AcceptBodySchema = t.Object({ + hash: t.String(), + from: t.String(), + handle: t.String(), + timestamp: t.String(), +}); +export type AcceptBody = Static; + export const AddPublicKeySchema = t.Object({ key: t.String(), }); diff --git a/packages/reader-main/src/messages/accept.ts b/packages/reader-main/src/messages/accept.ts new file mode 100644 index 00000000..8cf66b4e --- /dev/null +++ b/packages/reader-main/src/messages/accept.ts @@ -0,0 +1,70 @@ +/* eslint-disable ts/no-namespace */ +import type { Posts } from '@atomone/dither-api-types'; + +import type { ActionWithData, ResponseStatus } from '../types/index'; + +import process from 'node:process'; + +import { extractMemoContent } from '@atomone/chronostate'; + +import { useConfig } from '../config/index'; + +declare module '@atomone/chronostate' { + export namespace MemoExtractor { + export interface TypeMap { + 'dither.Accept': [string]; + } + } +} + +const { AUTH } = useConfig(); +const apiRoot = process.env.API_ROOT ?? 'http://localhost:3000/v1'; + +export async function Accept(action: ActionWithData): Promise { + try { + const [handle] = extractMemoContent(action.memo, 'dither.Accept'); + const postBody: Posts.AcceptBody = { + hash: action.hash, + from: action.sender, + handle, + timestamp: action.timestamp, + }; + + const rawResponse = await fetch(`${apiRoot}/accept`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': AUTH, + }, + body: JSON.stringify(postBody), + }); + + if (rawResponse.status !== 200) { + console.error('Error posting to API:', rawResponse); + return 'RETRY'; + } + + const response = await rawResponse.json() as { status: number; error?: string }; + if (response.status === 200) { + console.log(`dither.Accept message processed successfully: ${action.hash}`); + return 'SUCCESS'; + } + + if (response.status === 500) { + console.log(`dither.Accept could not reach database: ${action.hash}`); + return 'RETRY'; + } + + if (response.status === 401) { + console.log(`dither.Accept message skipped, invalid address provided: ${action.hash}`); + return 'SKIP'; + } + + console.warn(`dither.Accept failed: ${action.hash} (${response.error})`); + return 'RETRY'; + } catch (error) { + console.error('Error processing message:', error); + return 'RETRY'; + }; +} diff --git a/packages/reader-main/src/messages/index.ts b/packages/reader-main/src/messages/index.ts index 74d530d4..160032db 100644 --- a/packages/reader-main/src/messages/index.ts +++ b/packages/reader-main/src/messages/index.ts @@ -1,19 +1,25 @@ +import { Accept } from './accept'; import { Dislike } from './dislike'; import { Flag } from './flag'; import { Follow } from './follow'; import { Like } from './like'; import { Post } from './post'; +import { Register } from './register'; import { Remove } from './remove'; import { Reply } from './reply'; +import { Transfer } from './transfer'; import { Unfollow } from './unfollow'; export const MessageHandlers = { + Accept, Dislike, Flag, Follow, Like, Post, + Register, Remove, Reply, + Transfer, Unfollow, }; diff --git a/packages/reader-main/src/messages/register.ts b/packages/reader-main/src/messages/register.ts new file mode 100644 index 00000000..9a704ffc --- /dev/null +++ b/packages/reader-main/src/messages/register.ts @@ -0,0 +1,70 @@ +/* eslint-disable ts/no-namespace */ +import type { Posts } from '@atomone/dither-api-types'; + +import type { ActionWithData, ResponseStatus } from '../types/index'; + +import process from 'node:process'; + +import { extractMemoContent } from '@atomone/chronostate'; + +import { useConfig } from '../config/index'; + +declare module '@atomone/chronostate' { + export namespace MemoExtractor { + export interface TypeMap { + 'dither.Register': [string]; + } + } +} + +const { AUTH } = useConfig(); +const apiRoot = process.env.API_ROOT ?? 'http://localhost:3000/v1'; + +export async function Register(action: ActionWithData): Promise { + try { + const [handle] = extractMemoContent(action.memo, 'dither.Register'); + const postBody: Posts.RegisterBody = { + hash: action.hash, + from: action.sender, + handle, + timestamp: action.timestamp, + }; + + const rawResponse = await fetch(`${apiRoot}/register`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': AUTH, + }, + body: JSON.stringify(postBody), + }); + + if (rawResponse.status !== 200) { + console.error('Error posting to API:', rawResponse); + return 'RETRY'; + } + + const response = await rawResponse.json() as { status: number; error?: string }; + if (response.status === 200) { + console.log(`dither.Register message processed successfully: ${action.hash}`); + return 'SUCCESS'; + } + + if (response.status === 500) { + console.log(`dither.Register could not reach database: ${action.hash}`); + return 'RETRY'; + } + + if (response.status === 401) { + console.log(`dither.Register message skipped, invalid address provided: ${action.hash}`); + return 'SKIP'; + } + + console.warn(`dither.Register failed: ${action.hash} (${response.error})`); + return 'RETRY'; + } catch (error) { + console.error('Error processing message:', error); + return 'RETRY'; + }; +} diff --git a/packages/reader-main/src/messages/transfer.ts b/packages/reader-main/src/messages/transfer.ts new file mode 100644 index 00000000..238f7838 --- /dev/null +++ b/packages/reader-main/src/messages/transfer.ts @@ -0,0 +1,71 @@ +/* eslint-disable ts/no-namespace */ +import type { Posts } from '@atomone/dither-api-types'; + +import type { ActionWithData, ResponseStatus } from '../types/index'; + +import process from 'node:process'; + +import { extractMemoContent } from '@atomone/chronostate'; + +import { useConfig } from '../config/index'; + +declare module '@atomone/chronostate' { + export namespace MemoExtractor { + export interface TypeMap { + 'dither.Transfer': [string, string]; + } + } +} + +const { AUTH } = useConfig(); +const apiRoot = process.env.API_ROOT ?? 'http://localhost:3000/v1'; + +export async function Transfer(action: ActionWithData): Promise { + try { + const [handle, to_address] = extractMemoContent(action.memo, 'dither.Transfer'); + const postBody: Posts.TransferBody = { + hash: action.hash, + from: action.sender, + handle, + to_address, + timestamp: action.timestamp, + }; + + const rawResponse = await fetch(`${apiRoot}/transfer`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': AUTH, + }, + body: JSON.stringify(postBody), + }); + + if (rawResponse.status !== 200) { + console.error('Error posting to API:', rawResponse); + return 'RETRY'; + } + + const response = await rawResponse.json() as { status: number; error?: string }; + if (response.status === 200) { + console.log(`dither.Transfer message processed successfully: ${action.hash}`); + return 'SUCCESS'; + } + + if (response.status === 500) { + console.log(`dither.Transfer could not reach database: ${action.hash}`); + return 'RETRY'; + } + + if (response.status === 401) { + console.log(`dither.Transfer message skipped, invalid address provided: ${action.hash}`); + return 'SKIP'; + } + + console.warn(`dither.Transfer failed: ${action.hash} (${response.error})`); + return 'RETRY'; + } catch (error) { + console.error('Error processing message:', error); + return 'RETRY'; + }; +} diff --git a/packages/tool-network-spammer/src/logic.ts b/packages/tool-network-spammer/src/logic.ts index 030afb95..f54577b5 100644 --- a/packages/tool-network-spammer/src/logic.ts +++ b/packages/tool-network-spammer/src/logic.ts @@ -39,6 +39,7 @@ function getRandomHash(): hash { return hashes[Math.floor(Math.random() * hashes.length)]; } +// TODO: Add support for usernames (register, transfer, accept) const actions = ['post', 'reply', 'like', 'dislike', 'delete', 'flag'] as const; type ActionType = (typeof actions)[number]; From ad49c3b0569794ebc926712372c95560ee1f72a7 Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:59:49 +0100 Subject: [PATCH 02/40] feat: add API endpoints for register, transfer and accept --- packages/api-main/drizzle/schema.ts | 29 ++++++++++ packages/api-main/src/posts/accept.ts | 33 +++++++++++ packages/api-main/src/posts/index.ts | 3 + packages/api-main/src/posts/register.ts | 58 +++++++++++++++++++ packages/api-main/src/posts/transfer.ts | 48 +++++++++++++++ packages/api-main/src/routes/reader.ts | 3 + packages/api-main/src/utility/index.ts | 7 +++ packages/lib-api-types/src/posts/index.ts | 2 +- packages/reader-main/src/messages/transfer.ts | 4 +- 9 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 packages/api-main/src/posts/accept.ts create mode 100644 packages/api-main/src/posts/register.ts create mode 100644 packages/api-main/src/posts/transfer.ts diff --git a/packages/api-main/drizzle/schema.ts b/packages/api-main/drizzle/schema.ts index 80cde5ba..b369a330 100644 --- a/packages/api-main/drizzle/schema.ts +++ b/packages/api-main/drizzle/schema.ts @@ -3,6 +3,33 @@ import { bigint, boolean, index, integer, pgEnum, pgTable, primaryKey, serial, t const MEMO_LENGTH = 512; +export const HandleTable = pgTable( + 'handle', + { + name: varchar({ length: 32 }).primaryKey(), + address: varchar({ length: 44 }).notNull(), + hash: varchar({ length: 64 }).notNull(), + timestamp: timestamp({ withTimezone: true }).notNull(), + }, + t => [ + index('handle_address_idx').on(t.address), + ], +); + +export const HandleTransferTable = pgTable( + 'handle_transfer', + { + hash: varchar({ length: 64 }).notNull(), + name: varchar({ length: 32 }).notNull(), + from_address: varchar({ length: 44 }).notNull(), + to_address: varchar({ length: 44 }).notNull(), + timestamp: timestamp({ withTimezone: true }).notNull(), + }, + t => [ + index('handle_transfer_to_idx').on(t.name, t.to_address), + ], +); + export const FeedTable = pgTable( 'feed', { @@ -166,4 +193,6 @@ export const tables = [ 'state', 'authrequests', 'ratelimits', + 'handle', + 'handle_transfer', ]; diff --git a/packages/api-main/src/posts/accept.ts b/packages/api-main/src/posts/accept.ts new file mode 100644 index 00000000..f65b1b4d --- /dev/null +++ b/packages/api-main/src/posts/accept.ts @@ -0,0 +1,33 @@ +import type { Posts } from '@atomone/dither-api-types'; + +import { and, eq } from 'drizzle-orm'; + +import { getDatabase } from '../../drizzle/db'; +import { HandleTable, HandleTransferTable } from '../../drizzle/schema'; +import { lower } from '../utility'; + +export async function Accept(body: Posts.AcceptBody) { + try { + if (!await doesTransferToAddressExists(body.handle, body.to_address)) { + return { status: 400, error: 'handle not found or not transferred to address' }; + } + + await getDatabase() + .update(HandleTable) + .set({ address: body.to_address.toLowerCase() }) + .where(eq(lower(HandleTable.name), body.handle.toLowerCase())); + + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to transfer handle' }; + } +} + +async function doesTransferToAddressExists(handle: string, address: string): Promise { + const count = await getDatabase().$count(HandleTransferTable, and( + eq(lower(HandleTransferTable.name), handle.toLowerCase()), + eq(HandleTransferTable.to_address, address.toLowerCase()), + )); + return count !== 0; +} diff --git a/packages/api-main/src/posts/index.ts b/packages/api-main/src/posts/index.ts index d7030b77..8105ebea 100644 --- a/packages/api-main/src/posts/index.ts +++ b/packages/api-main/src/posts/index.ts @@ -1,3 +1,4 @@ +export * from './accept'; export * from './auth'; export * from './authCreate'; export * from './dislike'; @@ -8,6 +9,8 @@ export * from './logout'; export * from './mod'; export * from './post'; export * from './postRemove'; +export * from './register'; export * from './reply'; +export * from './transfer'; export * from './unfollow'; export * from './updateState'; diff --git a/packages/api-main/src/posts/register.ts b/packages/api-main/src/posts/register.ts new file mode 100644 index 00000000..0c3df497 --- /dev/null +++ b/packages/api-main/src/posts/register.ts @@ -0,0 +1,58 @@ +import type { Posts } from '@atomone/dither-api-types'; + +import { eq, sql } from 'drizzle-orm'; + +import { getDatabase } from '../../drizzle/db'; +import { HandleTable } from '../../drizzle/schema'; +import { lower } from '../utility'; + +const statement = getDatabase() + .insert(HandleTable) + .values({ + name: sql.placeholder('name'), + address: sql.placeholder('address'), + hash: sql.placeholder('hash'), + timestamp: sql.placeholder('timestamp'), + }) + .onConflictDoNothing() + .prepare('stmnt_handle_insert'); + +export const tgHandleRegex = /^[a-z]{3}\w*$/i; + +export async function Register(body: Posts.RegisterBody) { + // TODO: Define how many names a single account can register + + if (!tgHandleRegex.test(body.handle)) { + return { + status: 400, + error: 'handle must start with three letters and can only include letters, numbers and underscores', + }; + } + + if (body.handle.length < 5 || body.handle.length > 32) { + return { status: 400, error: 'handle must have between 5 and 32 characters long' }; + } + + try { + if (await doesHandleExists(body.handle)) { + return { status: 400, error: 'handle is already registered' }; + } + + await statement.execute({ + hash: body.hash.toLowerCase(), + address: body.from.toLowerCase(), + name: body.handle, + timestamp: new Date(body.timestamp), + }); + + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to register handle' }; + } +} + +async function doesHandleExists(name: string): Promise { + const count = await getDatabase().$count(HandleTable, eq(lower(HandleTable.name), name.toLowerCase())); + return count !== 0; +} diff --git a/packages/api-main/src/posts/transfer.ts b/packages/api-main/src/posts/transfer.ts new file mode 100644 index 00000000..87072f86 --- /dev/null +++ b/packages/api-main/src/posts/transfer.ts @@ -0,0 +1,48 @@ +import type { Posts } from '@atomone/dither-api-types'; + +import { and, eq, sql } from 'drizzle-orm'; + +import { getDatabase } from '../../drizzle/db'; +import { HandleTable, HandleTransferTable } from '../../drizzle/schema'; +import { lower } from '../utility'; + +const statement = getDatabase() + .insert(HandleTransferTable) + .values({ + hash: sql.placeholder('hash'), + name: sql.placeholder('name'), + from_address: sql.placeholder('from_address'), + to_address: sql.placeholder('to_address'), + timestamp: sql.placeholder('timestamp'), + }) + .onConflictDoNothing() + .prepare('stmnt_handle_transfer_insert'); + +export async function Transfer(body: Posts.TransferBody) { + try { + if (!await isHandleOwner(body.handle, body.from_address)) { + return { status: 400, error: 'handle not found or not registered for address' }; + } + + await statement.execute({ + hash: body.hash.toLowerCase(), + from_address: body.from_address.toLowerCase(), + to_address: body.to_address.toLowerCase(), + name: body.handle, + timestamp: new Date(body.timestamp), + }); + + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to transfer handle' }; + } +} + +async function isHandleOwner(handle: string, address: string): Promise { + const count = await getDatabase().$count(HandleTable, and( + eq(lower(HandleTable.name), handle.toLowerCase()), + eq(HandleTable.address, address.toLowerCase()), + )); + return count !== 0; +} diff --git a/packages/api-main/src/routes/reader.ts b/packages/api-main/src/routes/reader.ts index f8e1d973..f1cc7e8d 100644 --- a/packages/api-main/src/routes/reader.ts +++ b/packages/api-main/src/routes/reader.ts @@ -18,6 +18,9 @@ export const readerRoutes = new Elysia() .post('/dislike', ({ body }) => PostRequests.Dislike(body), { body: Posts.DislikeBodySchema }) .post('/flag', ({ body }) => PostRequests.Flag(body), { body: Posts.FlagBodySchema }) .post('/post-remove', ({ body }) => PostRequests.PostRemove(body), { body: Posts.PostRemoveBodySchema }) + .post('/register', ({ body }) => PostRequests.Register(body), { body: Posts.RegisterBodySchema }) + .post('/transfer', ({ body }) => PostRequests.Transfer(body), { body: Posts.TransferBodySchema }) + .post('/accept', ({ body }) => PostRequests.Accept(body), { body: Posts.AcceptBodySchema }) .post('/update-state', ({ body }) => PostRequests.UpdateState(body), { body: t.Object({ last_block: t.String() }), }); diff --git a/packages/api-main/src/utility/index.ts b/packages/api-main/src/utility/index.ts index b9f87ba2..c881ab81 100644 --- a/packages/api-main/src/utility/index.ts +++ b/packages/api-main/src/utility/index.ts @@ -1,3 +1,6 @@ +import type { SQL } from 'drizzle-orm'; +import type { AnyPgColumn } from 'drizzle-orm/pg-core'; + import type * as T from '../types/index'; import { Buffer } from 'node:buffer'; @@ -10,6 +13,10 @@ import { useConfig } from '../config'; const { AUTH, DISCORD_WEBHOOK_URL } = useConfig(); +export function lower(email: AnyPgColumn): SQL { + return sql`lower(${email})`; +} + export function getTransferMessage(messages: Array) { const msgTransfer = messages.find(msg => msg['@type'] === '/cosmos.bank.v1beta1.MsgSend'); if (!msgTransfer) { diff --git a/packages/lib-api-types/src/posts/index.ts b/packages/lib-api-types/src/posts/index.ts index af17c9fa..8a1988ae 100644 --- a/packages/lib-api-types/src/posts/index.ts +++ b/packages/lib-api-types/src/posts/index.ts @@ -110,8 +110,8 @@ export type RegisterBody = Static; export const TransferBodySchema = t.Object({ hash: t.String(), - from: t.String(), handle: t.String(), + from_address: t.String(), to_address: t.String(), timestamp: t.String(), }); diff --git a/packages/reader-main/src/messages/transfer.ts b/packages/reader-main/src/messages/transfer.ts index 238f7838..978044e1 100644 --- a/packages/reader-main/src/messages/transfer.ts +++ b/packages/reader-main/src/messages/transfer.ts @@ -25,9 +25,9 @@ export async function Transfer(action: ActionWithData): Promise const [handle, to_address] = extractMemoContent(action.memo, 'dither.Transfer'); const postBody: Posts.TransferBody = { hash: action.hash, - from: action.sender, - handle, + from_address: action.sender, to_address, + handle, timestamp: action.timestamp, }; From cd6162dff1edf910e25a6813db878a311832220c Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:56:03 +0100 Subject: [PATCH 03/40] feat: change handle register to remove previous handle This forces each address to have a single asociated handle. It also keeps queries simple because there is no need to keep track of the current handle if an address would be allowed to have more than one. --- packages/api-main/drizzle/db.ts | 6 ++-- packages/api-main/src/posts/register.ts | 43 ++++++++++++------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/packages/api-main/drizzle/db.ts b/packages/api-main/drizzle/db.ts index b9684287..471e30bf 100644 --- a/packages/api-main/drizzle/db.ts +++ b/packages/api-main/drizzle/db.ts @@ -6,11 +6,13 @@ import pg from 'pg'; dotenv.config(); +export type DbClient = ReturnType; + const { Pool } = pg; -let db: ReturnType; +let db: DbClient; -export function getDatabase() { +export function getDatabase(): DbClient { if (!db) { const client = new Pool({ connectionString: process.env.PG_URI!, diff --git a/packages/api-main/src/posts/register.ts b/packages/api-main/src/posts/register.ts index 0c3df497..66c569a3 100644 --- a/packages/api-main/src/posts/register.ts +++ b/packages/api-main/src/posts/register.ts @@ -1,27 +1,16 @@ import type { Posts } from '@atomone/dither-api-types'; -import { eq, sql } from 'drizzle-orm'; +import type { DbClient } from '../../drizzle/db'; + +import { eq } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { HandleTable } from '../../drizzle/schema'; import { lower } from '../utility'; -const statement = getDatabase() - .insert(HandleTable) - .values({ - name: sql.placeholder('name'), - address: sql.placeholder('address'), - hash: sql.placeholder('hash'), - timestamp: sql.placeholder('timestamp'), - }) - .onConflictDoNothing() - .prepare('stmnt_handle_insert'); - export const tgHandleRegex = /^[a-z]{3}\w*$/i; export async function Register(body: Posts.RegisterBody) { - // TODO: Define how many names a single account can register - if (!tgHandleRegex.test(body.handle)) { return { status: 400, @@ -33,16 +22,26 @@ export async function Register(body: Posts.RegisterBody) { return { status: 400, error: 'handle must have between 5 and 32 characters long' }; } + const db = getDatabase(); + try { - if (await doesHandleExists(body.handle)) { + if (await doesHandleExists(db, body.handle)) { return { status: 400, error: 'handle is already registered' }; } - await statement.execute({ - hash: body.hash.toLowerCase(), - address: body.from.toLowerCase(), - name: body.handle, - timestamp: new Date(body.timestamp), + const address = body.from.toLowerCase(); + + await db.transaction(async (tx) => { + // Remove current handle if one is registered to address + await tx.delete(HandleTable).where(eq(HandleTable.address, address)); + + // Register a new handle for the address + await tx.insert(HandleTable).values({ + name: body.handle, + hash: body.hash.toLowerCase(), + address: body.from.toLowerCase(), + timestamp: new Date(body.timestamp), + }); }); return { status: 200 }; @@ -52,7 +51,7 @@ export async function Register(body: Posts.RegisterBody) { } } -async function doesHandleExists(name: string): Promise { - const count = await getDatabase().$count(HandleTable, eq(lower(HandleTable.name), name.toLowerCase())); +async function doesHandleExists(db: DbClient, name: string): Promise { + const count = await db.$count(HandleTable, eq(lower(HandleTable.name), name.toLowerCase())); return count !== 0; } From 32bada1b74a4953699bbe2f0a4ea15e749aa223d Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:58:37 +0100 Subject: [PATCH 04/40] fix: correct issue in reader that led to always start from 1st block Explicit start block value was ignored before. --- packages/reader-main/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/reader-main/src/index.ts b/packages/reader-main/src/index.ts index c160c124..cfe0e9a3 100644 --- a/packages/reader-main/src/index.ts +++ b/packages/reader-main/src/index.ts @@ -216,7 +216,7 @@ export async function start() { if (Number.parseInt(config.START_BLOCK) > lastBlockStored) { console.info(`START_BLOCK is higher than last block stored, starting from START_BLOCK=${config.START_BLOCK}`); - config.START_BLOCK = lastBlockStored.toString(); + startBlock = Number.parseInt(config.START_BLOCK); } else { startBlock = lastBlockStored; } From 58aeae0f7f8066692985653e8255233cd8a1040c Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:59:55 +0100 Subject: [PATCH 05/40] chore: remove one API URL from reader Docker compose file Removed because a single address can be enough, the one left keeps a bigger history. All in Bits node was configured not track history, so it makes no sense to use it to get previous TXs. --- packages/reader-main/docker-compose.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/reader-main/docker-compose.yml b/packages/reader-main/docker-compose.yml index 21a9e23a..5f640761 100644 --- a/packages/reader-main/docker-compose.yml +++ b/packages/reader-main/docker-compose.yml @@ -6,7 +6,11 @@ services: restart: always container_name: chronosync environment: - API_URLS: 'https://atomone-api.allinbits.com,https://atomone-rest.publicnode.com' + # Note: It seems all in bits node is not keeping a big history, so REST requests + # fail when trying to get previous TXs, poluting logs. + # API_URLS: 'https://atomone-api.allinbits.com,https://atomone-rest.publicnode.com' + + API_URLS: 'https://atomone-rest.publicnode.com' START_BLOCK: '2605764' BATCH_SIZE: 50 MEMO_PREFIX: dither. From d964956e06601e11b69f65120e476887500e5df5 Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:08:28 +0100 Subject: [PATCH 06/40] feat: remove prepared statements and change code for consistency Prepared statements are not useful here because they are used only once. They would make sense where the same query would be executed multiple times. --- packages/api-main/src/posts/accept.ts | 11 +++++++---- packages/api-main/src/posts/register.ts | 19 +++++++++++-------- packages/api-main/src/posts/transfer.ts | 25 ++++++++----------------- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/packages/api-main/src/posts/accept.ts b/packages/api-main/src/posts/accept.ts index f65b1b4d..22ae1ea3 100644 --- a/packages/api-main/src/posts/accept.ts +++ b/packages/api-main/src/posts/accept.ts @@ -1,5 +1,7 @@ import type { Posts } from '@atomone/dither-api-types'; +import type { DbClient } from '../../drizzle/db'; + import { and, eq } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; @@ -7,12 +9,13 @@ import { HandleTable, HandleTransferTable } from '../../drizzle/schema'; import { lower } from '../utility'; export async function Accept(body: Posts.AcceptBody) { + const db = getDatabase(); try { - if (!await doesTransferToAddressExists(body.handle, body.to_address)) { + if (!await doesTransferToAddressExists(db, body.handle, body.to_address)) { return { status: 400, error: 'handle not found or not transferred to address' }; } - await getDatabase() + await db .update(HandleTable) .set({ address: body.to_address.toLowerCase() }) .where(eq(lower(HandleTable.name), body.handle.toLowerCase())); @@ -24,8 +27,8 @@ export async function Accept(body: Posts.AcceptBody) { } } -async function doesTransferToAddressExists(handle: string, address: string): Promise { - const count = await getDatabase().$count(HandleTransferTable, and( +async function doesTransferToAddressExists(db: DbClient, handle: string, address: string): Promise { + const count = await db.$count(HandleTransferTable, and( eq(lower(HandleTransferTable.name), handle.toLowerCase()), eq(HandleTransferTable.to_address, address.toLowerCase()), )); diff --git a/packages/api-main/src/posts/register.ts b/packages/api-main/src/posts/register.ts index 66c569a3..6894977f 100644 --- a/packages/api-main/src/posts/register.ts +++ b/packages/api-main/src/posts/register.ts @@ -23,7 +23,6 @@ export async function Register(body: Posts.RegisterBody) { } const db = getDatabase(); - try { if (await doesHandleExists(db, body.handle)) { return { status: 400, error: 'handle is already registered' }; @@ -33,15 +32,19 @@ export async function Register(body: Posts.RegisterBody) { await db.transaction(async (tx) => { // Remove current handle if one is registered to address - await tx.delete(HandleTable).where(eq(HandleTable.address, address)); + await tx + .delete(HandleTable) + .where(eq(HandleTable.address, address)); // Register a new handle for the address - await tx.insert(HandleTable).values({ - name: body.handle, - hash: body.hash.toLowerCase(), - address: body.from.toLowerCase(), - timestamp: new Date(body.timestamp), - }); + await tx + .insert(HandleTable) + .values({ + name: body.handle, + hash: body.hash.toLowerCase(), + address: body.from.toLowerCase(), + timestamp: new Date(body.timestamp), + }); }); return { status: 200 }; diff --git a/packages/api-main/src/posts/transfer.ts b/packages/api-main/src/posts/transfer.ts index 87072f86..53196062 100644 --- a/packages/api-main/src/posts/transfer.ts +++ b/packages/api-main/src/posts/transfer.ts @@ -1,30 +1,21 @@ import type { Posts } from '@atomone/dither-api-types'; -import { and, eq, sql } from 'drizzle-orm'; +import type { DbClient } from '../../drizzle/db'; + +import { and, eq } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { HandleTable, HandleTransferTable } from '../../drizzle/schema'; import { lower } from '../utility'; -const statement = getDatabase() - .insert(HandleTransferTable) - .values({ - hash: sql.placeholder('hash'), - name: sql.placeholder('name'), - from_address: sql.placeholder('from_address'), - to_address: sql.placeholder('to_address'), - timestamp: sql.placeholder('timestamp'), - }) - .onConflictDoNothing() - .prepare('stmnt_handle_transfer_insert'); - export async function Transfer(body: Posts.TransferBody) { + const db = getDatabase(); try { - if (!await isHandleOwner(body.handle, body.from_address)) { + if (!await isHandleOwner(db, body.handle, body.from_address)) { return { status: 400, error: 'handle not found or not registered for address' }; } - await statement.execute({ + await db.insert(HandleTransferTable).values({ hash: body.hash.toLowerCase(), from_address: body.from_address.toLowerCase(), to_address: body.to_address.toLowerCase(), @@ -39,8 +30,8 @@ export async function Transfer(body: Posts.TransferBody) { } } -async function isHandleOwner(handle: string, address: string): Promise { - const count = await getDatabase().$count(HandleTable, and( +async function isHandleOwner(db: DbClient, handle: string, address: string): Promise { + const count = await db.$count(HandleTable, and( eq(lower(HandleTable.name), handle.toLowerCase()), eq(HandleTable.address, address.toLowerCase()), )); From 754e0f551d9e5273d2201a99b30a6273aeff7eea Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:49:20 +0100 Subject: [PATCH 07/40] fix: remove previous handle transfer if it exists This makes sure that handle can be transfered to a single address and avoids possible transfer mistakes. --- packages/api-main/src/posts/transfer.ts | 29 ++++++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/api-main/src/posts/transfer.ts b/packages/api-main/src/posts/transfer.ts index 53196062..5ef7eef2 100644 --- a/packages/api-main/src/posts/transfer.ts +++ b/packages/api-main/src/posts/transfer.ts @@ -15,12 +15,29 @@ export async function Transfer(body: Posts.TransferBody) { return { status: 400, error: 'handle not found or not registered for address' }; } - await db.insert(HandleTransferTable).values({ - hash: body.hash.toLowerCase(), - from_address: body.from_address.toLowerCase(), - to_address: body.to_address.toLowerCase(), - name: body.handle, - timestamp: new Date(body.timestamp), + const fromAddress = body.from_address.toLowerCase(); + + await db.transaction(async (tx) => { + // If it exists delete previous transfer for the same handle + await tx + .delete(HandleTransferTable) + .where( + and( + eq(lower(HandleTransferTable.name), body.handle.toLowerCase()), + eq(HandleTransferTable.from_address, fromAddress), + ), + ); + + // Add a handle transfer to a new address + await tx + .insert(HandleTransferTable) + .values({ + hash: body.hash.toLowerCase(), + name: body.handle, + from_address: fromAddress, + to_address: body.to_address.toLowerCase(), + timestamp: new Date(body.timestamp), + }); }); return { status: 200 }; From 9cdf1884bc057c73c0bd8941b08d6917a0f98ee3 Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:56:50 +0100 Subject: [PATCH 08/40] feat: support search posts by user handle --- packages/api-main/src/gets/search.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/api-main/src/gets/search.ts b/packages/api-main/src/gets/search.ts index 09a32281..1d71b4d8 100644 --- a/packages/api-main/src/gets/search.ts +++ b/packages/api-main/src/gets/search.ts @@ -1,9 +1,9 @@ import type { Gets } from '@atomone/dither-api-types'; -import { and, desc, gte, ilike, inArray, isNull, or, sql } from 'drizzle-orm'; +import { and, desc, eq, getTableColumns, gte, ilike, inArray, isNull, or, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { FeedTable } from '../../drizzle/schema'; +import { FeedTable, HandleTable } from '../../drizzle/schema'; export async function Search(query: Gets.SearchQuery) { try { @@ -22,12 +22,25 @@ export async function Search(query: Gets.SearchQuery) { const matchedAuthors = await getDatabase() .selectDistinct({ author: FeedTable.author }) .from(FeedTable) - .where(and(ilike(FeedTable.author, `%${query.text}%`), isNull(FeedTable.removed_at))); + .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) + .where( + and( + or( + eq(FeedTable.author, query.text.toLowerCase()), // Exact address + ilike(HandleTable.name, `%${query.text}%`), // Registered handle (partial match) + ), + isNull(FeedTable.removed_at), + ), + ); const matchedAuthorAddresses = matchedAuthors.map(a => a.author); const matchedPosts = await getDatabase() - .select() + .select({ + ...getTableColumns(FeedTable), + handle: HandleTable.name, + }) .from(FeedTable) + .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) .where( and( or( @@ -40,8 +53,8 @@ export async function Search(query: Gets.SearchQuery) { ) .limit(100) .offset(0) - .orderBy(desc(FeedTable.timestamp)) - .execute(); + .orderBy(desc(FeedTable.timestamp)); + return { status: 200, rows: [...matchedPosts], users: matchedAuthorAddresses }; } catch (error) { console.error(error); From 9a468eb70e69b7128f0516bd2db743f7b6dc349f Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:57:14 +0100 Subject: [PATCH 09/40] feat: add user handle to feed, follow and post endpoints --- packages/api-main/src/gets/feed.ts | 10 +++++++--- packages/api-main/src/gets/followers.ts | 9 +++++++-- packages/api-main/src/gets/following.ts | 9 +++++++-- packages/api-main/src/gets/post.ts | 16 ++++++++++++---- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/packages/api-main/src/gets/feed.ts b/packages/api-main/src/gets/feed.ts index 81f59779..0d5a1317 100644 --- a/packages/api-main/src/gets/feed.ts +++ b/packages/api-main/src/gets/feed.ts @@ -1,13 +1,17 @@ import type { Gets } from '@atomone/dither-api-types'; -import { and, count, desc, gte, isNull, sql } from 'drizzle-orm'; +import { and, count, desc, eq, getTableColumns, gte, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { FeedTable } from '../../drizzle/schema'; +import { FeedTable, HandleTable } from '../../drizzle/schema'; const statement = getDatabase() - .select() + .select({ + ...getTableColumns(FeedTable), + handle: HandleTable.name, + }) .from(FeedTable) + .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) .limit(sql.placeholder('limit')) .offset(sql.placeholder('offset')) .where( diff --git a/packages/api-main/src/gets/followers.ts b/packages/api-main/src/gets/followers.ts index bf85b9ac..27224e4d 100644 --- a/packages/api-main/src/gets/followers.ts +++ b/packages/api-main/src/gets/followers.ts @@ -3,11 +3,16 @@ import type { Gets } from '@atomone/dither-api-types'; import { and, desc, eq, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { FollowsTable } from '../../drizzle/schema'; +import { FollowsTable, HandleTable } from '../../drizzle/schema'; const statementGetFollowers = getDatabase() - .select({ address: FollowsTable.follower, hash: FollowsTable.hash }) + .select({ + address: FollowsTable.follower, + handle: HandleTable.name, + hash: FollowsTable.hash, + }) .from(FollowsTable) + .leftJoin(HandleTable, eq(HandleTable.address, FollowsTable.follower)) .where(and(eq(FollowsTable.following, sql.placeholder('following')), isNull(FollowsTable.removed_at))) .limit(sql.placeholder('limit')) .offset(sql.placeholder('offset')) diff --git a/packages/api-main/src/gets/following.ts b/packages/api-main/src/gets/following.ts index 22d7da52..d887cf0d 100644 --- a/packages/api-main/src/gets/following.ts +++ b/packages/api-main/src/gets/following.ts @@ -3,11 +3,16 @@ import type { Gets } from '@atomone/dither-api-types'; import { and, desc, eq, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { FollowsTable } from '../../drizzle/schema'; +import { FollowsTable, HandleTable } from '../../drizzle/schema'; const statementGetFollowing = getDatabase() - .select({ address: FollowsTable.following, hash: FollowsTable.hash }) + .select({ + address: FollowsTable.following, + handle: HandleTable.name, + hash: FollowsTable.hash, + }) .from(FollowsTable) + .leftJoin(HandleTable, eq(HandleTable.address, FollowsTable.follower)) .where(and(eq(FollowsTable.follower, sql.placeholder('follower')), isNull(FollowsTable.removed_at))) .limit(sql.placeholder('limit')) .offset(sql.placeholder('offset')) diff --git a/packages/api-main/src/gets/post.ts b/packages/api-main/src/gets/post.ts index 08a2fad7..597436cb 100644 --- a/packages/api-main/src/gets/post.ts +++ b/packages/api-main/src/gets/post.ts @@ -1,19 +1,27 @@ import type { Gets } from '@atomone/dither-api-types'; -import { and, eq, isNull, sql } from 'drizzle-orm'; +import { and, eq, getTableColumns, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { FeedTable } from '../../drizzle/schema'; +import { FeedTable, HandleTable } from '../../drizzle/schema'; const statementGetPost = getDatabase() - .select() + .select({ + ...getTableColumns(FeedTable), + handle: HandleTable.name, + }) .from(FeedTable) + .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) .where(and(isNull(FeedTable.removed_at), eq(FeedTable.hash, sql.placeholder('hash')))) .prepare('stmnt_get_post'); const statementGetReply = getDatabase() - .select() + .select({ + ...getTableColumns(FeedTable), + handle: HandleTable.name, + }) .from(FeedTable) + .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) .where(and(isNull(FeedTable.removed_at), eq(FeedTable.hash, sql.placeholder('hash')), eq(FeedTable.post_hash, sql.placeholder('post_hash')))) .prepare('stmnt_get_reply'); From 6b50ff7e47ff6f1183bf28175d5ed27cd37f0c42 Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:09:33 +0100 Subject: [PATCH 10/40] feat: add user handle to posts endpoint This makes handle available to the profile frontend view. --- packages/api-main/src/gets/posts.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/api-main/src/gets/posts.ts b/packages/api-main/src/gets/posts.ts index 9bccbc5e..988c8e8a 100644 --- a/packages/api-main/src/gets/posts.ts +++ b/packages/api-main/src/gets/posts.ts @@ -1,13 +1,17 @@ import type { Gets } from '@atomone/dither-api-types'; -import { and, desc, eq, gte, inArray, isNull, sql } from 'drizzle-orm'; +import { and, desc, eq, getTableColumns, gte, inArray, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { FeedTable, FollowsTable } from '../../drizzle/schema'; +import { FeedTable, FollowsTable, HandleTable } from '../../drizzle/schema'; const statement = getDatabase() - .select() + .select({ + ...getTableColumns(FeedTable), + handle: HandleTable.name, + }) .from(FeedTable) + .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) .where( and( eq(FeedTable.author, sql.placeholder('author')), From 62bcb9b6fed67365d2e4f0f1c0d8fd52a71b97a6 Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:42:13 +0100 Subject: [PATCH 11/40] feat: add user handle display support protocol Display requires a handle to be registered. Protocol update: - dither.Register('handle', 'display text') - dither.Display('display text') --- packages/api-main/drizzle/schema.ts | 2 + packages/api-main/src/posts/display.ts | 44 ++++++++++++ packages/api-main/src/posts/index.ts | 1 + packages/api-main/src/posts/register.ts | 14 +++- packages/frontend-main/ARCHITECTURE.md | 7 +- packages/lib-api-types/src/posts/index.ts | 9 +++ packages/reader-main/src/messages/display.ts | 70 +++++++++++++++++++ packages/reader-main/src/messages/index.ts | 2 + packages/reader-main/src/messages/register.ts | 5 +- packages/tool-network-spammer/src/logic.ts | 2 +- 10 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 packages/api-main/src/posts/display.ts create mode 100644 packages/reader-main/src/messages/display.ts diff --git a/packages/api-main/drizzle/schema.ts b/packages/api-main/drizzle/schema.ts index b369a330..138b747e 100644 --- a/packages/api-main/drizzle/schema.ts +++ b/packages/api-main/drizzle/schema.ts @@ -8,11 +8,13 @@ export const HandleTable = pgTable( { name: varchar({ length: 32 }).primaryKey(), address: varchar({ length: 44 }).notNull(), + display: varchar({ length: 128 }), hash: varchar({ length: 64 }).notNull(), timestamp: timestamp({ withTimezone: true }).notNull(), }, t => [ index('handle_address_idx').on(t.address), + index('handle_display_idx').on(t.display), ], ); diff --git a/packages/api-main/src/posts/display.ts b/packages/api-main/src/posts/display.ts new file mode 100644 index 00000000..ea73a8ac --- /dev/null +++ b/packages/api-main/src/posts/display.ts @@ -0,0 +1,44 @@ +import type { Posts } from '@atomone/dither-api-types'; + +import { count, eq, sql } from 'drizzle-orm'; + +import { getDatabase } from '../../drizzle/db'; +import { HandleTable } from '../../drizzle/schema'; +import { lower } from '../utility'; +import { maxDisplayLength } from './register'; + +const handleAddressExistsStmt = getDatabase() + .select({ count: count() }) + .from(HandleTable) + .where(eq(lower(HandleTable.address), sql.placeholder('address'))) + .prepare('stmt_handle_address_exists'); + +export async function Display(body: Posts.DisplayBody) { + const display = (body.display || '').trim(); + if (display.length > maxDisplayLength) { + return { status: 400, error: `maximum display length is ${maxDisplayLength} characters long` }; + } + + const db = getDatabase(); + try { + const address = body.from.toLowerCase(); + if (!await hasHandle(address)) { + return { status: 400, error: 'account requires a handle to set a display text' }; + } + + await db + .update(HandleTable) + .set({ display }) + .where(eq(HandleTable.address, address)); + + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to register handle' }; + } +} + +async function hasHandle(address: string): Promise { + const [result] = await handleAddressExistsStmt.execute({ address }); + return (result?.count ?? 0) !== 0; +} diff --git a/packages/api-main/src/posts/index.ts b/packages/api-main/src/posts/index.ts index 8105ebea..5eda6060 100644 --- a/packages/api-main/src/posts/index.ts +++ b/packages/api-main/src/posts/index.ts @@ -2,6 +2,7 @@ export * from './accept'; export * from './auth'; export * from './authCreate'; export * from './dislike'; +export * from './display'; export * from './flag'; export * from './follow'; export * from './like'; diff --git a/packages/api-main/src/posts/register.ts b/packages/api-main/src/posts/register.ts index 6894977f..24466ea3 100644 --- a/packages/api-main/src/posts/register.ts +++ b/packages/api-main/src/posts/register.ts @@ -8,6 +8,10 @@ import { getDatabase } from '../../drizzle/db'; import { HandleTable } from '../../drizzle/schema'; import { lower } from '../utility'; +const minHandleLength = 5; +const maxHandleLength = 32; + +export const maxDisplayLength = 128; export const tgHandleRegex = /^[a-z]{3}\w*$/i; export async function Register(body: Posts.RegisterBody) { @@ -18,8 +22,13 @@ export async function Register(body: Posts.RegisterBody) { }; } - if (body.handle.length < 5 || body.handle.length > 32) { - return { status: 400, error: 'handle must have between 5 and 32 characters long' }; + if (body.handle.length < minHandleLength || body.handle.length > maxHandleLength) { + return { status: 400, error: `handle must have between ${minHandleLength} and ${maxHandleLength} characters long` }; + } + + const display = (body.display || '').trim(); + if (display.length > maxDisplayLength) { + return { status: 400, error: `maximum display length is ${maxDisplayLength} characters long` }; } const db = getDatabase(); @@ -43,6 +52,7 @@ export async function Register(body: Posts.RegisterBody) { name: body.handle, hash: body.hash.toLowerCase(), address: body.from.toLowerCase(), + display: display || null, timestamp: new Date(body.timestamp), }); }); diff --git a/packages/frontend-main/ARCHITECTURE.md b/packages/frontend-main/ARCHITECTURE.md index a87cfee1..2d1a60b4 100644 --- a/packages/frontend-main/ARCHITECTURE.md +++ b/packages/frontend-main/ARCHITECTURE.md @@ -195,9 +195,12 @@ dither.Unfollow('cosmos1user...'); dither.Dislike('0xjkl012...'); // Username registration and transfer to a new address -dither.Register('handle'); +dither.Register('handle', 'display text'); dither.Transfer('handle', '0xabc123...'); dither.Accept('handle'); + +// Display text change for registered user handle +dither.Display('display text'); ``` ### Supported Wallets @@ -217,4 +220,4 @@ dither.Accept('handle'); - **Reply threading** with nested conversations - **User tipping** and social interactions - **Content moderation** through flagging system -- **Usernames** by registering transferable handles +- **Usernames and display text** by registering transferable handles diff --git a/packages/lib-api-types/src/posts/index.ts b/packages/lib-api-types/src/posts/index.ts index 8a1988ae..0d189a9d 100644 --- a/packages/lib-api-types/src/posts/index.ts +++ b/packages/lib-api-types/src/posts/index.ts @@ -104,10 +104,19 @@ export const RegisterBodySchema = t.Object({ hash: t.String(), from: t.String(), handle: t.String(), + display: t.String(), timestamp: t.String(), }); export type RegisterBody = Static; +export const DisplayBodySchema = t.Object({ + hash: t.String(), + from: t.String(), + display: t.String(), + timestamp: t.String(), +}); +export type DisplayBody = Static; + export const TransferBodySchema = t.Object({ hash: t.String(), handle: t.String(), diff --git a/packages/reader-main/src/messages/display.ts b/packages/reader-main/src/messages/display.ts new file mode 100644 index 00000000..2c2315b7 --- /dev/null +++ b/packages/reader-main/src/messages/display.ts @@ -0,0 +1,70 @@ +/* eslint-disable ts/no-namespace */ +import type { Posts } from '@atomone/dither-api-types'; + +import type { ActionWithData, ResponseStatus } from '../types/index'; + +import process from 'node:process'; + +import { extractMemoContent } from '@atomone/chronostate'; + +import { useConfig } from '../config/index'; + +declare module '@atomone/chronostate' { + export namespace MemoExtractor { + export interface TypeMap { + 'dither.Display': [string]; + } + } +} + +const { AUTH } = useConfig(); +const apiRoot = process.env.API_ROOT ?? 'http://localhost:3000/v1'; + +export async function Display(action: ActionWithData): Promise { + try { + const [display] = extractMemoContent(action.memo, 'dither.Display'); + const postBody: Posts.DisplayBody = { + hash: action.hash, + from: action.sender, + display, + timestamp: action.timestamp, + }; + + const rawResponse = await fetch(`${apiRoot}/display`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': AUTH, + }, + body: JSON.stringify(postBody), + }); + + if (rawResponse.status !== 200) { + console.error('Error posting to API:', rawResponse); + return 'RETRY'; + } + + const response = await rawResponse.json() as { status: number; error?: string }; + if (response.status === 200) { + console.log(`dither.Display message processed successfully: ${action.hash}`); + return 'SUCCESS'; + } + + if (response.status === 500) { + console.log(`dither.Display could not reach database: ${action.hash}`); + return 'RETRY'; + } + + if (response.status === 401) { + console.log(`dither.Display message skipped, invalid address provided: ${action.hash}`); + return 'SKIP'; + } + + console.warn(`dither.Display failed: ${action.hash} (${response.error})`); + return 'RETRY'; + } catch (error) { + console.error('Error processing message:', error); + return 'RETRY'; + }; +} diff --git a/packages/reader-main/src/messages/index.ts b/packages/reader-main/src/messages/index.ts index 160032db..b6b4c5c6 100644 --- a/packages/reader-main/src/messages/index.ts +++ b/packages/reader-main/src/messages/index.ts @@ -1,5 +1,6 @@ import { Accept } from './accept'; import { Dislike } from './dislike'; +import { Display } from './display'; import { Flag } from './flag'; import { Follow } from './follow'; import { Like } from './like'; @@ -13,6 +14,7 @@ import { Unfollow } from './unfollow'; export const MessageHandlers = { Accept, Dislike, + Display, Flag, Follow, Like, diff --git a/packages/reader-main/src/messages/register.ts b/packages/reader-main/src/messages/register.ts index 9a704ffc..527941e2 100644 --- a/packages/reader-main/src/messages/register.ts +++ b/packages/reader-main/src/messages/register.ts @@ -12,7 +12,7 @@ import { useConfig } from '../config/index'; declare module '@atomone/chronostate' { export namespace MemoExtractor { export interface TypeMap { - 'dither.Register': [string]; + 'dither.Register': [string, string]; } } } @@ -22,11 +22,12 @@ const apiRoot = process.env.API_ROOT ?? 'http://localhost:3000/v1'; export async function Register(action: ActionWithData): Promise { try { - const [handle] = extractMemoContent(action.memo, 'dither.Register'); + const [handle, display] = extractMemoContent(action.memo, 'dither.Register'); const postBody: Posts.RegisterBody = { hash: action.hash, from: action.sender, handle, + display, timestamp: action.timestamp, }; diff --git a/packages/tool-network-spammer/src/logic.ts b/packages/tool-network-spammer/src/logic.ts index f54577b5..986efafc 100644 --- a/packages/tool-network-spammer/src/logic.ts +++ b/packages/tool-network-spammer/src/logic.ts @@ -39,7 +39,7 @@ function getRandomHash(): hash { return hashes[Math.floor(Math.random() * hashes.length)]; } -// TODO: Add support for usernames (register, transfer, accept) +// TODO: Add support for usernames (register, transfer, accept, display) const actions = ['post', 'reply', 'like', 'dislike', 'delete', 'flag'] as const; type ActionType = (typeof actions)[number]; From 50c3dd4bdbe0d270687dc795688e10eae617635e Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:58:34 +0100 Subject: [PATCH 12/40] feat: add display field to API endpoints --- packages/api-main/src/gets/feed.ts | 1 + packages/api-main/src/gets/followers.ts | 1 + packages/api-main/src/gets/following.ts | 1 + packages/api-main/src/gets/post.ts | 1 + packages/api-main/src/gets/posts.ts | 1 + packages/api-main/src/gets/search.ts | 1 + 6 files changed, 6 insertions(+) diff --git a/packages/api-main/src/gets/feed.ts b/packages/api-main/src/gets/feed.ts index 0d5a1317..58395bd0 100644 --- a/packages/api-main/src/gets/feed.ts +++ b/packages/api-main/src/gets/feed.ts @@ -9,6 +9,7 @@ const statement = getDatabase() .select({ ...getTableColumns(FeedTable), handle: HandleTable.name, + display: HandleTable.display, }) .from(FeedTable) .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) diff --git a/packages/api-main/src/gets/followers.ts b/packages/api-main/src/gets/followers.ts index 27224e4d..ca835615 100644 --- a/packages/api-main/src/gets/followers.ts +++ b/packages/api-main/src/gets/followers.ts @@ -9,6 +9,7 @@ const statementGetFollowers = getDatabase() .select({ address: FollowsTable.follower, handle: HandleTable.name, + display: HandleTable.display, hash: FollowsTable.hash, }) .from(FollowsTable) diff --git a/packages/api-main/src/gets/following.ts b/packages/api-main/src/gets/following.ts index d887cf0d..948d6160 100644 --- a/packages/api-main/src/gets/following.ts +++ b/packages/api-main/src/gets/following.ts @@ -9,6 +9,7 @@ const statementGetFollowing = getDatabase() .select({ address: FollowsTable.following, handle: HandleTable.name, + display: HandleTable.display, hash: FollowsTable.hash, }) .from(FollowsTable) diff --git a/packages/api-main/src/gets/post.ts b/packages/api-main/src/gets/post.ts index 597436cb..63f3a387 100644 --- a/packages/api-main/src/gets/post.ts +++ b/packages/api-main/src/gets/post.ts @@ -9,6 +9,7 @@ const statementGetPost = getDatabase() .select({ ...getTableColumns(FeedTable), handle: HandleTable.name, + display: HandleTable.display, }) .from(FeedTable) .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) diff --git a/packages/api-main/src/gets/posts.ts b/packages/api-main/src/gets/posts.ts index 988c8e8a..4ffd3787 100644 --- a/packages/api-main/src/gets/posts.ts +++ b/packages/api-main/src/gets/posts.ts @@ -9,6 +9,7 @@ const statement = getDatabase() .select({ ...getTableColumns(FeedTable), handle: HandleTable.name, + display: HandleTable.display, }) .from(FeedTable) .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) diff --git a/packages/api-main/src/gets/search.ts b/packages/api-main/src/gets/search.ts index 1d71b4d8..3dcc666e 100644 --- a/packages/api-main/src/gets/search.ts +++ b/packages/api-main/src/gets/search.ts @@ -38,6 +38,7 @@ export async function Search(query: Gets.SearchQuery) { .select({ ...getTableColumns(FeedTable), handle: HandleTable.name, + display: HandleTable.display, }) .from(FeedTable) .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) From 39c26320f06c122714707b57900082c2c5c7862b Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:56:09 +0100 Subject: [PATCH 13/40] feat: add notification for registration of a user handle already taken --- packages/api-main/drizzle/schema.ts | 9 ++++++++- packages/api-main/src/posts/register.ts | 16 ++++++++++++++-- packages/api-main/src/routes/reader.ts | 1 + packages/api-main/src/shared/notify.ts | 8 ++++---- .../notifications/NotificationType.vue | 5 +++-- .../notifications/NotificationWrapper.vue | 2 +- .../notifications/RegisterNotification.vue | 14 ++++++++++++++ packages/frontend-main/src/localization/index.ts | 1 + .../src/views/NotificationsView.vue | 2 ++ 9 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 packages/frontend-main/src/components/notifications/RegisterNotification.vue diff --git a/packages/api-main/drizzle/schema.ts b/packages/api-main/drizzle/schema.ts index 138b747e..347bf793 100644 --- a/packages/api-main/drizzle/schema.ts +++ b/packages/api-main/drizzle/schema.ts @@ -158,7 +158,14 @@ export const ModeratorTable = pgTable('moderators', { deleted_at: timestamp({ withTimezone: true }), }); -export const notificationTypeEnum = pgEnum('notification_type', ['like', 'dislike', 'flag', 'follow', 'reply']); +export const notificationTypeEnum = pgEnum('notification_type', [ + 'like', + 'dislike', + 'flag', + 'follow', + 'reply', + 'register', +]); export const NotificationTable = pgTable( 'notifications', diff --git a/packages/api-main/src/posts/register.ts b/packages/api-main/src/posts/register.ts index 24466ea3..2e5bc96f 100644 --- a/packages/api-main/src/posts/register.ts +++ b/packages/api-main/src/posts/register.ts @@ -6,6 +6,7 @@ import { eq } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { HandleTable } from '../../drizzle/schema'; +import { notify } from '../shared/notify'; import { lower } from '../utility'; const minHandleLength = 5; @@ -33,8 +34,19 @@ export async function Register(body: Posts.RegisterBody) { const db = getDatabase(); try { + const timestamp = new Date(body.timestamp); if (await doesHandleExists(db, body.handle)) { - return { status: 400, error: 'handle is already registered' }; + await notify({ + hash: body.hash, + type: 'register', + actor: body.from, + owner: body.from, + timestamp, + subcontext: 'Handle is already taken', + }); + + // Succeed to stop reader from keep trying to register + return { status: 200, error: 'handle is already registered' }; } const address = body.from.toLowerCase(); @@ -53,7 +65,7 @@ export async function Register(body: Posts.RegisterBody) { hash: body.hash.toLowerCase(), address: body.from.toLowerCase(), display: display || null, - timestamp: new Date(body.timestamp), + timestamp, }); }); diff --git a/packages/api-main/src/routes/reader.ts b/packages/api-main/src/routes/reader.ts index f1cc7e8d..c4e3c972 100644 --- a/packages/api-main/src/routes/reader.ts +++ b/packages/api-main/src/routes/reader.ts @@ -21,6 +21,7 @@ export const readerRoutes = new Elysia() .post('/register', ({ body }) => PostRequests.Register(body), { body: Posts.RegisterBodySchema }) .post('/transfer', ({ body }) => PostRequests.Transfer(body), { body: Posts.TransferBodySchema }) .post('/accept', ({ body }) => PostRequests.Accept(body), { body: Posts.AcceptBodySchema }) + .post('/display', ({ body }) => PostRequests.Display(body), { body: Posts.DisplayBodySchema }) .post('/update-state', ({ body }) => PostRequests.UpdateState(body), { body: t.Object({ last_block: t.String() }), }); diff --git a/packages/api-main/src/shared/notify.ts b/packages/api-main/src/shared/notify.ts index 1899c5d0..f0ae45d4 100644 --- a/packages/api-main/src/shared/notify.ts +++ b/packages/api-main/src/shared/notify.ts @@ -37,6 +37,10 @@ export async function notify(data: { } owner ??= post.author; + if (owner === data.actor) { + return; + } + if (!data.subcontext) { const subcontext = post.message.length >= 64 ? `${post.message.slice(0, 61)}...` : post.message; data.subcontext = subcontext; @@ -47,10 +51,6 @@ export async function notify(data: { throw new Error('failed to add owner'); } - if (owner === data.actor) { - return; - } - await statementInsertNotification.execute({ owner: owner.toLowerCase(), post_hash: data.post_hash?.toLowerCase(), diff --git a/packages/frontend-main/src/components/notifications/NotificationType.vue b/packages/frontend-main/src/components/notifications/NotificationType.vue index 04b9de3e..19aeb64e 100644 --- a/packages/frontend-main/src/components/notifications/NotificationType.vue +++ b/packages/frontend-main/src/components/notifications/NotificationType.vue @@ -1,7 +1,7 @@ + + diff --git a/packages/frontend-main/src/localization/index.ts b/packages/frontend-main/src/localization/index.ts index c2c9346c..52b7e715 100644 --- a/packages/frontend-main/src/localization/index.ts +++ b/packages/frontend-main/src/localization/index.ts @@ -117,6 +117,7 @@ export const messages = { flag: 'Redflagged your post', follow: 'Followed you', reply: 'Replied to your post', + register: 'User handle registration', empty: 'Nothing to show', }, FollowingList: { diff --git a/packages/frontend-main/src/views/NotificationsView.vue b/packages/frontend-main/src/views/NotificationsView.vue index af4315d9..1d9a15c0 100644 --- a/packages/frontend-main/src/views/NotificationsView.vue +++ b/packages/frontend-main/src/views/NotificationsView.vue @@ -6,6 +6,7 @@ import DislikeNotification from '@/components/notifications/DislikeNotification. import FlagNotification from '@/components/notifications/FlagNotification.vue'; import FollowNotification from '@/components/notifications/FollowNotification.vue'; import LikeNotification from '@/components/notifications/LikeNotification.vue'; +import RegisterNotification from '@/components/notifications/RegisterNotification.vue'; import ReplyNotification from '@/components/notifications/ReplyNotification.vue'; import Button from '@/components/ui/button/Button.vue'; import { useNotifications } from '@/composables/useNotifications'; @@ -38,6 +39,7 @@ const flatNotifications = computed(() => data.value?.pages.flat() ?? []); +
From e356f0a72f1a3d3adfa3676d120387dfc3b2f1f9 Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:35:40 +0100 Subject: [PATCH 14/40] feat: add API endpoint to get user handle by name or address --- packages/api-main/src/gets/handle.ts | 42 ++++++++++++++++++++++++ packages/api-main/src/gets/index.ts | 1 + packages/api-main/src/routes/public.ts | 1 + packages/lib-api-types/src/gets/index.ts | 6 ++++ 4 files changed, 50 insertions(+) create mode 100644 packages/api-main/src/gets/handle.ts diff --git a/packages/api-main/src/gets/handle.ts b/packages/api-main/src/gets/handle.ts new file mode 100644 index 00000000..a6e43644 --- /dev/null +++ b/packages/api-main/src/gets/handle.ts @@ -0,0 +1,42 @@ +import type { Gets } from '@atomone/dither-api-types'; + +import { eq, or, sql } from 'drizzle-orm'; + +import { getDatabase } from '../../drizzle/db'; +import { HandleTable } from '../../drizzle/schema'; + +const statement = getDatabase() + .select({ + name: HandleTable.name, + address: HandleTable.address, + display: HandleTable.display, + }) + .from(HandleTable) + .where( + or( + eq(HandleTable.name, sql.placeholder('name')), + eq(HandleTable.address, sql.placeholder('address')), + ), + ) + .orderBy(HandleTable.name) + .limit(1) + .prepare('stmnt_get_handle'); + +export async function Handle(query: Gets.HandleQuery) { + const { address, name } = query; + if (!address && !name) { + return { status: 400, error: 'handle name or address is required' }; + } + + try { + const [handle] = await statement.execute({ address, name }); + if (!handle) { + return { status: 404, rows: [] }; + } + + return { status: 200, rows: [handle] }; + } catch (error) { + console.error(error); + return { error: 'failed to read data from database' }; + } +} diff --git a/packages/api-main/src/gets/index.ts b/packages/api-main/src/gets/index.ts index 5425689a..9f15490c 100644 --- a/packages/api-main/src/gets/index.ts +++ b/packages/api-main/src/gets/index.ts @@ -4,6 +4,7 @@ export * from './feed'; export * from './flags'; export * from './followers'; export * from './following'; +export * from './handle'; export * from './health'; export * from './isFollowing'; export * from './lastBlock'; diff --git a/packages/api-main/src/routes/public.ts b/packages/api-main/src/routes/public.ts index 40317c7e..305a350b 100644 --- a/packages/api-main/src/routes/public.ts +++ b/packages/api-main/src/routes/public.ts @@ -21,4 +21,5 @@ export const publicRoutes = new Elysia() .get('/search', ({ query }) => GetRequests.Search(query), { query: Gets.SearchQuerySchema }) .get('/user-replies', ({ query }) => GetRequests.UserReplies(query), { query: Gets.UserRepliesQuerySchema }) .get('/following-posts', ({ query }) => GetRequests.FollowingPosts(query), { query: Gets.PostsQuerySchema }) + .get('/handle', ({ query }) => GetRequests.Handle(query), { query: Gets.HandleQuerySchema }) .get('/last-block', GetRequests.LastBlock); diff --git a/packages/lib-api-types/src/gets/index.ts b/packages/lib-api-types/src/gets/index.ts index ac7df26d..98ecf0c3 100644 --- a/packages/lib-api-types/src/gets/index.ts +++ b/packages/lib-api-types/src/gets/index.ts @@ -107,3 +107,9 @@ export const NotificationsCountQuerySchema = t.Object({ address: t.String(), }); export type NotificationsCountQuery = Static; + +export const HandleQuerySchema = t.Object({ + address: t.Optional(t.String()), + name: t.Optional(t.String()), +}); +export type HandleQuery = Static; From 8464e85b743669a4a48c9ea9f60a824c685e9019 Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:29:26 +0100 Subject: [PATCH 15/40] refactor: change protocol names to be more explicit This addresses review recommendations regarding having a less general protocol. It changes the following function names: - Register() -> RegisterHandle() - Transfer() -> TransferHandle() - Accept() -> AcceptHandle() - Display() -> SetDisplayHandle() --- packages/api-main/drizzle/schema.ts | 2 +- .../src/posts/{accept.ts => acceptHandle.ts} | 2 +- .../src/posts/{display.ts => displayHandle.ts} | 6 +++--- packages/api-main/src/posts/index.ts | 8 ++++---- .../posts/{register.ts => registerHandle.ts} | 2 +- .../posts/{transfer.ts => transferHandle.ts} | 2 +- packages/api-main/src/routes/reader.ts | 8 ++++---- packages/frontend-main/ARCHITECTURE.md | 8 ++++---- .../notifications/NotificationType.vue | 2 +- .../notifications/NotificationWrapper.vue | 2 +- ...tion.vue => RegisterHandleNotification.vue} | 0 .../frontend-main/src/localization/index.ts | 2 +- .../src/views/NotificationsView.vue | 4 ++-- packages/lib-api-types/src/posts/index.ts | 16 ++++++++-------- .../messages/{accept.ts => acceptHandle.ts} | 18 +++++++++--------- packages/reader-main/src/messages/index.ts | 16 ++++++++-------- .../{register.ts => registerHandle.ts} | 18 +++++++++--------- .../{display.ts => setDisplayHandle.ts} | 18 +++++++++--------- .../{transfer.ts => transferHandle.ts} | 18 +++++++++--------- packages/tool-network-spammer/src/logic.ts | 2 +- 20 files changed, 77 insertions(+), 77 deletions(-) rename packages/api-main/src/posts/{accept.ts => acceptHandle.ts} (94%) rename packages/api-main/src/posts/{display.ts => displayHandle.ts} (86%) rename packages/api-main/src/posts/{register.ts => registerHandle.ts} (97%) rename packages/api-main/src/posts/{transfer.ts => transferHandle.ts} (95%) rename packages/frontend-main/src/components/notifications/{RegisterNotification.vue => RegisterHandleNotification.vue} (100%) rename packages/reader-main/src/messages/{accept.ts => acceptHandle.ts} (71%) rename packages/reader-main/src/messages/{register.ts => registerHandle.ts} (69%) rename packages/reader-main/src/messages/{display.ts => setDisplayHandle.ts} (66%) rename packages/reader-main/src/messages/{transfer.ts => transferHandle.ts} (69%) diff --git a/packages/api-main/drizzle/schema.ts b/packages/api-main/drizzle/schema.ts index 347bf793..f12c0d47 100644 --- a/packages/api-main/drizzle/schema.ts +++ b/packages/api-main/drizzle/schema.ts @@ -164,7 +164,7 @@ export const notificationTypeEnum = pgEnum('notification_type', [ 'flag', 'follow', 'reply', - 'register', + 'registerHandle', ]); export const NotificationTable = pgTable( diff --git a/packages/api-main/src/posts/accept.ts b/packages/api-main/src/posts/acceptHandle.ts similarity index 94% rename from packages/api-main/src/posts/accept.ts rename to packages/api-main/src/posts/acceptHandle.ts index 22ae1ea3..cb9fef30 100644 --- a/packages/api-main/src/posts/accept.ts +++ b/packages/api-main/src/posts/acceptHandle.ts @@ -8,7 +8,7 @@ import { getDatabase } from '../../drizzle/db'; import { HandleTable, HandleTransferTable } from '../../drizzle/schema'; import { lower } from '../utility'; -export async function Accept(body: Posts.AcceptBody) { +export async function AcceptHandle(body: Posts.AcceptHandleBody) { const db = getDatabase(); try { if (!await doesTransferToAddressExists(db, body.handle, body.to_address)) { diff --git a/packages/api-main/src/posts/display.ts b/packages/api-main/src/posts/displayHandle.ts similarity index 86% rename from packages/api-main/src/posts/display.ts rename to packages/api-main/src/posts/displayHandle.ts index ea73a8ac..3c0f4e60 100644 --- a/packages/api-main/src/posts/display.ts +++ b/packages/api-main/src/posts/displayHandle.ts @@ -5,7 +5,7 @@ import { count, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { HandleTable } from '../../drizzle/schema'; import { lower } from '../utility'; -import { maxDisplayLength } from './register'; +import { maxDisplayLength } from './registerHandle'; const handleAddressExistsStmt = getDatabase() .select({ count: count() }) @@ -13,7 +13,7 @@ const handleAddressExistsStmt = getDatabase() .where(eq(lower(HandleTable.address), sql.placeholder('address'))) .prepare('stmt_handle_address_exists'); -export async function Display(body: Posts.DisplayBody) { +export async function DisplayHandle(body: Posts.DisplayHandleBody) { const display = (body.display || '').trim(); if (display.length > maxDisplayLength) { return { status: 400, error: `maximum display length is ${maxDisplayLength} characters long` }; @@ -34,7 +34,7 @@ export async function Display(body: Posts.DisplayBody) { return { status: 200 }; } catch (err) { console.error(err); - return { status: 400, error: 'failed to register handle' }; + return { status: 400, error: 'failed to update display handle' }; } } diff --git a/packages/api-main/src/posts/index.ts b/packages/api-main/src/posts/index.ts index 5eda6060..8a6bc37b 100644 --- a/packages/api-main/src/posts/index.ts +++ b/packages/api-main/src/posts/index.ts @@ -1,8 +1,8 @@ -export * from './accept'; +export * from './acceptHandle'; export * from './auth'; export * from './authCreate'; export * from './dislike'; -export * from './display'; +export * from './displayHandle'; export * from './flag'; export * from './follow'; export * from './like'; @@ -10,8 +10,8 @@ export * from './logout'; export * from './mod'; export * from './post'; export * from './postRemove'; -export * from './register'; +export * from './registerHandle'; export * from './reply'; -export * from './transfer'; +export * from './transferHandle'; export * from './unfollow'; export * from './updateState'; diff --git a/packages/api-main/src/posts/register.ts b/packages/api-main/src/posts/registerHandle.ts similarity index 97% rename from packages/api-main/src/posts/register.ts rename to packages/api-main/src/posts/registerHandle.ts index 2e5bc96f..5064bb8d 100644 --- a/packages/api-main/src/posts/register.ts +++ b/packages/api-main/src/posts/registerHandle.ts @@ -15,7 +15,7 @@ const maxHandleLength = 32; export const maxDisplayLength = 128; export const tgHandleRegex = /^[a-z]{3}\w*$/i; -export async function Register(body: Posts.RegisterBody) { +export async function RegisterHandle(body: Posts.RegisterHandleBody) { if (!tgHandleRegex.test(body.handle)) { return { status: 400, diff --git a/packages/api-main/src/posts/transfer.ts b/packages/api-main/src/posts/transferHandle.ts similarity index 95% rename from packages/api-main/src/posts/transfer.ts rename to packages/api-main/src/posts/transferHandle.ts index 5ef7eef2..7a8d7918 100644 --- a/packages/api-main/src/posts/transfer.ts +++ b/packages/api-main/src/posts/transferHandle.ts @@ -8,7 +8,7 @@ import { getDatabase } from '../../drizzle/db'; import { HandleTable, HandleTransferTable } from '../../drizzle/schema'; import { lower } from '../utility'; -export async function Transfer(body: Posts.TransferBody) { +export async function TransferHandle(body: Posts.TransferHandleBody) { const db = getDatabase(); try { if (!await isHandleOwner(db, body.handle, body.from_address)) { diff --git a/packages/api-main/src/routes/reader.ts b/packages/api-main/src/routes/reader.ts index c4e3c972..e9fcdb4b 100644 --- a/packages/api-main/src/routes/reader.ts +++ b/packages/api-main/src/routes/reader.ts @@ -18,10 +18,10 @@ export const readerRoutes = new Elysia() .post('/dislike', ({ body }) => PostRequests.Dislike(body), { body: Posts.DislikeBodySchema }) .post('/flag', ({ body }) => PostRequests.Flag(body), { body: Posts.FlagBodySchema }) .post('/post-remove', ({ body }) => PostRequests.PostRemove(body), { body: Posts.PostRemoveBodySchema }) - .post('/register', ({ body }) => PostRequests.Register(body), { body: Posts.RegisterBodySchema }) - .post('/transfer', ({ body }) => PostRequests.Transfer(body), { body: Posts.TransferBodySchema }) - .post('/accept', ({ body }) => PostRequests.Accept(body), { body: Posts.AcceptBodySchema }) - .post('/display', ({ body }) => PostRequests.Display(body), { body: Posts.DisplayBodySchema }) + .post('/register-handle', ({ body }) => PostRequests.RegisterHandle(body), { body: Posts.RegisterHandleBodySchema }) + .post('/transfer-handle', ({ body }) => PostRequests.TransferHandle(body), { body: Posts.TransferHandleBodySchema }) + .post('/accept-handle', ({ body }) => PostRequests.AcceptHandle(body), { body: Posts.AcceptHandleBodySchema }) + .post('/display-handle', ({ body }) => PostRequests.DisplayHandle(body), { body: Posts.DisplayHandleBodySchema }) .post('/update-state', ({ body }) => PostRequests.UpdateState(body), { body: t.Object({ last_block: t.String() }), }); diff --git a/packages/frontend-main/ARCHITECTURE.md b/packages/frontend-main/ARCHITECTURE.md index 2d1a60b4..447e3b23 100644 --- a/packages/frontend-main/ARCHITECTURE.md +++ b/packages/frontend-main/ARCHITECTURE.md @@ -195,12 +195,12 @@ dither.Unfollow('cosmos1user...'); dither.Dislike('0xjkl012...'); // Username registration and transfer to a new address -dither.Register('handle', 'display text'); -dither.Transfer('handle', '0xabc123...'); -dither.Accept('handle'); +dither.RegisterHandle('handle', 'display text'); +dither.TransferHandle('handle', '0xabc123...'); +dither.AcceptHandle('handle'); // Display text change for registered user handle -dither.Display('display text'); +dither.SetDisplayHandle('display text'); ``` ### Supported Wallets diff --git a/packages/frontend-main/src/components/notifications/NotificationType.vue b/packages/frontend-main/src/components/notifications/NotificationType.vue index 19aeb64e..ccfeab37 100644 --- a/packages/frontend-main/src/components/notifications/NotificationType.vue +++ b/packages/frontend-main/src/components/notifications/NotificationType.vue @@ -17,7 +17,7 @@ defineProps<{ notification: Notification }>(); : notification.type === 'follow' ? UserPlus : notification.type === 'flag' ? Flag : notification.type === 'reply' ? MessageCircle - : notification.type === 'register' ? UserLock : null" + : notification.type === 'registerHandle' ? UserLock : null" :class="cn('size-5', notification.type === 'dislike' && 'scale-x-[-1]')" />
diff --git a/packages/frontend-main/src/components/notifications/NotificationWrapper.vue b/packages/frontend-main/src/components/notifications/NotificationWrapper.vue index e5a3b0fd..12c8009f 100644 --- a/packages/frontend-main/src/components/notifications/NotificationWrapper.vue +++ b/packages/frontend-main/src/components/notifications/NotificationWrapper.vue @@ -25,7 +25,7 @@ const navigationPath = computed(() => { } // actions on user - if (['follow', 'accept'].includes(notification.type)) { + if (['follow'].includes(notification.type)) { return `/profile/${notification.actor}`; } diff --git a/packages/frontend-main/src/components/notifications/RegisterNotification.vue b/packages/frontend-main/src/components/notifications/RegisterHandleNotification.vue similarity index 100% rename from packages/frontend-main/src/components/notifications/RegisterNotification.vue rename to packages/frontend-main/src/components/notifications/RegisterHandleNotification.vue diff --git a/packages/frontend-main/src/localization/index.ts b/packages/frontend-main/src/localization/index.ts index 52b7e715..8c931283 100644 --- a/packages/frontend-main/src/localization/index.ts +++ b/packages/frontend-main/src/localization/index.ts @@ -117,7 +117,7 @@ export const messages = { flag: 'Redflagged your post', follow: 'Followed you', reply: 'Replied to your post', - register: 'User handle registration', + registerHandle: 'User handle registration', empty: 'Nothing to show', }, FollowingList: { diff --git a/packages/frontend-main/src/views/NotificationsView.vue b/packages/frontend-main/src/views/NotificationsView.vue index 1d9a15c0..128d1cbf 100644 --- a/packages/frontend-main/src/views/NotificationsView.vue +++ b/packages/frontend-main/src/views/NotificationsView.vue @@ -6,7 +6,7 @@ import DislikeNotification from '@/components/notifications/DislikeNotification. import FlagNotification from '@/components/notifications/FlagNotification.vue'; import FollowNotification from '@/components/notifications/FollowNotification.vue'; import LikeNotification from '@/components/notifications/LikeNotification.vue'; -import RegisterNotification from '@/components/notifications/RegisterNotification.vue'; +import RegisterHandleNotification from '@/components/notifications/RegisterHandleNotification.vue'; import ReplyNotification from '@/components/notifications/ReplyNotification.vue'; import Button from '@/components/ui/button/Button.vue'; import { useNotifications } from '@/composables/useNotifications'; @@ -39,7 +39,7 @@ const flatNotifications = computed(() => data.value?.pages.flat() ?? []); - +
diff --git a/packages/lib-api-types/src/posts/index.ts b/packages/lib-api-types/src/posts/index.ts index 0d189a9d..b081fb84 100644 --- a/packages/lib-api-types/src/posts/index.ts +++ b/packages/lib-api-types/src/posts/index.ts @@ -100,39 +100,39 @@ export const UnfollowBodySchema = t.Object({ }); export type UnfollowBody = Static; -export const RegisterBodySchema = t.Object({ +export const RegisterHandleBodySchema = t.Object({ hash: t.String(), from: t.String(), handle: t.String(), display: t.String(), timestamp: t.String(), }); -export type RegisterBody = Static; +export type RegisterHandleBody = Static; -export const DisplayBodySchema = t.Object({ +export const DisplayHandleBodySchema = t.Object({ hash: t.String(), from: t.String(), display: t.String(), timestamp: t.String(), }); -export type DisplayBody = Static; +export type DisplayHandleBody = Static; -export const TransferBodySchema = t.Object({ +export const TransferHandleBodySchema = t.Object({ hash: t.String(), handle: t.String(), from_address: t.String(), to_address: t.String(), timestamp: t.String(), }); -export type TransferBody = Static; +export type TransferHandleBody = Static; -export const AcceptBodySchema = t.Object({ +export const AcceptHandleBodySchema = t.Object({ hash: t.String(), from: t.String(), handle: t.String(), timestamp: t.String(), }); -export type AcceptBody = Static; +export type AcceptHandleBody = Static; export const AddPublicKeySchema = t.Object({ key: t.String(), diff --git a/packages/reader-main/src/messages/accept.ts b/packages/reader-main/src/messages/acceptHandle.ts similarity index 71% rename from packages/reader-main/src/messages/accept.ts rename to packages/reader-main/src/messages/acceptHandle.ts index 8cf66b4e..8909b021 100644 --- a/packages/reader-main/src/messages/accept.ts +++ b/packages/reader-main/src/messages/acceptHandle.ts @@ -12,7 +12,7 @@ import { useConfig } from '../config/index'; declare module '@atomone/chronostate' { export namespace MemoExtractor { export interface TypeMap { - 'dither.Accept': [string]; + 'dither.AcceptHandle': [string]; } } } @@ -20,17 +20,17 @@ declare module '@atomone/chronostate' { const { AUTH } = useConfig(); const apiRoot = process.env.API_ROOT ?? 'http://localhost:3000/v1'; -export async function Accept(action: ActionWithData): Promise { +export async function AcceptHandle(action: ActionWithData): Promise { try { - const [handle] = extractMemoContent(action.memo, 'dither.Accept'); - const postBody: Posts.AcceptBody = { + const [handle] = extractMemoContent(action.memo, 'dither.AcceptHandle'); + const postBody: Posts.AcceptHandleBody = { hash: action.hash, from: action.sender, handle, timestamp: action.timestamp, }; - const rawResponse = await fetch(`${apiRoot}/accept`, { + const rawResponse = await fetch(`${apiRoot}/accept-handle`, { method: 'POST', headers: { 'Accept': 'application/json', @@ -47,21 +47,21 @@ export async function Accept(action: ActionWithData): Promise { const response = await rawResponse.json() as { status: number; error?: string }; if (response.status === 200) { - console.log(`dither.Accept message processed successfully: ${action.hash}`); + console.log(`dither.AcceptHandle message processed successfully: ${action.hash}`); return 'SUCCESS'; } if (response.status === 500) { - console.log(`dither.Accept could not reach database: ${action.hash}`); + console.log(`dither.AcceptHandle could not reach database: ${action.hash}`); return 'RETRY'; } if (response.status === 401) { - console.log(`dither.Accept message skipped, invalid address provided: ${action.hash}`); + console.log(`dither.AcceptHandle message skipped, invalid address provided: ${action.hash}`); return 'SKIP'; } - console.warn(`dither.Accept failed: ${action.hash} (${response.error})`); + console.warn(`dither.AcceptHandle failed: ${action.hash} (${response.error})`); return 'RETRY'; } catch (error) { console.error('Error processing message:', error); diff --git a/packages/reader-main/src/messages/index.ts b/packages/reader-main/src/messages/index.ts index b6b4c5c6..7daccb20 100644 --- a/packages/reader-main/src/messages/index.ts +++ b/packages/reader-main/src/messages/index.ts @@ -1,27 +1,27 @@ -import { Accept } from './accept'; +import { AcceptHandle } from './acceptHandle'; import { Dislike } from './dislike'; -import { Display } from './display'; import { Flag } from './flag'; import { Follow } from './follow'; import { Like } from './like'; import { Post } from './post'; -import { Register } from './register'; +import { RegisterHandle } from './registerHandle'; import { Remove } from './remove'; import { Reply } from './reply'; -import { Transfer } from './transfer'; +import { SetDisplayHandle } from './setDisplayHandle'; +import { TransferHandle } from './transferHandle'; import { Unfollow } from './unfollow'; export const MessageHandlers = { - Accept, + AcceptHandle, Dislike, - Display, + SetDisplayHandle, Flag, Follow, Like, Post, - Register, + RegisterHandle, Remove, Reply, - Transfer, + TransferHandle, Unfollow, }; diff --git a/packages/reader-main/src/messages/register.ts b/packages/reader-main/src/messages/registerHandle.ts similarity index 69% rename from packages/reader-main/src/messages/register.ts rename to packages/reader-main/src/messages/registerHandle.ts index 527941e2..4b70fabc 100644 --- a/packages/reader-main/src/messages/register.ts +++ b/packages/reader-main/src/messages/registerHandle.ts @@ -12,7 +12,7 @@ import { useConfig } from '../config/index'; declare module '@atomone/chronostate' { export namespace MemoExtractor { export interface TypeMap { - 'dither.Register': [string, string]; + 'dither.RegisterHandle': [string, string]; } } } @@ -20,10 +20,10 @@ declare module '@atomone/chronostate' { const { AUTH } = useConfig(); const apiRoot = process.env.API_ROOT ?? 'http://localhost:3000/v1'; -export async function Register(action: ActionWithData): Promise { +export async function RegisterHandle(action: ActionWithData): Promise { try { - const [handle, display] = extractMemoContent(action.memo, 'dither.Register'); - const postBody: Posts.RegisterBody = { + const [handle, display] = extractMemoContent(action.memo, 'dither.RegisterHandle'); + const postBody: Posts.RegisterHandleBody = { hash: action.hash, from: action.sender, handle, @@ -31,7 +31,7 @@ export async function Register(action: ActionWithData): Promise timestamp: action.timestamp, }; - const rawResponse = await fetch(`${apiRoot}/register`, { + const rawResponse = await fetch(`${apiRoot}/register-handle`, { method: 'POST', headers: { 'Accept': 'application/json', @@ -48,21 +48,21 @@ export async function Register(action: ActionWithData): Promise const response = await rawResponse.json() as { status: number; error?: string }; if (response.status === 200) { - console.log(`dither.Register message processed successfully: ${action.hash}`); + console.log(`dither.RegisterHandle message processed successfully: ${action.hash}`); return 'SUCCESS'; } if (response.status === 500) { - console.log(`dither.Register could not reach database: ${action.hash}`); + console.log(`dither.RegisterHandle could not reach database: ${action.hash}`); return 'RETRY'; } if (response.status === 401) { - console.log(`dither.Register message skipped, invalid address provided: ${action.hash}`); + console.log(`dither.RegisterHandle message skipped, invalid address provided: ${action.hash}`); return 'SKIP'; } - console.warn(`dither.Register failed: ${action.hash} (${response.error})`); + console.warn(`dither.RegisterHandle failed: ${action.hash} (${response.error})`); return 'RETRY'; } catch (error) { console.error('Error processing message:', error); diff --git a/packages/reader-main/src/messages/display.ts b/packages/reader-main/src/messages/setDisplayHandle.ts similarity index 66% rename from packages/reader-main/src/messages/display.ts rename to packages/reader-main/src/messages/setDisplayHandle.ts index 2c2315b7..b55c64c5 100644 --- a/packages/reader-main/src/messages/display.ts +++ b/packages/reader-main/src/messages/setDisplayHandle.ts @@ -12,7 +12,7 @@ import { useConfig } from '../config/index'; declare module '@atomone/chronostate' { export namespace MemoExtractor { export interface TypeMap { - 'dither.Display': [string]; + 'dither.SetDisplayHandle': [string]; } } } @@ -20,17 +20,17 @@ declare module '@atomone/chronostate' { const { AUTH } = useConfig(); const apiRoot = process.env.API_ROOT ?? 'http://localhost:3000/v1'; -export async function Display(action: ActionWithData): Promise { +export async function SetDisplayHandle(action: ActionWithData): Promise { try { - const [display] = extractMemoContent(action.memo, 'dither.Display'); - const postBody: Posts.DisplayBody = { + const [display] = extractMemoContent(action.memo, 'dither.SetDisplayHandle'); + const postBody: Posts.DisplayHandleBody = { hash: action.hash, from: action.sender, display, timestamp: action.timestamp, }; - const rawResponse = await fetch(`${apiRoot}/display`, { + const rawResponse = await fetch(`${apiRoot}/display-handle`, { method: 'POST', headers: { 'Accept': 'application/json', @@ -47,21 +47,21 @@ export async function Display(action: ActionWithData): Promise { const response = await rawResponse.json() as { status: number; error?: string }; if (response.status === 200) { - console.log(`dither.Display message processed successfully: ${action.hash}`); + console.log(`dither.SetDisplayHandle message processed successfully: ${action.hash}`); return 'SUCCESS'; } if (response.status === 500) { - console.log(`dither.Display could not reach database: ${action.hash}`); + console.log(`dither.SetDisplayHandle could not reach database: ${action.hash}`); return 'RETRY'; } if (response.status === 401) { - console.log(`dither.Display message skipped, invalid address provided: ${action.hash}`); + console.log(`dither.SetDisplayHandle message skipped, invalid address provided: ${action.hash}`); return 'SKIP'; } - console.warn(`dither.Display failed: ${action.hash} (${response.error})`); + console.warn(`dither.SetDisplayHandle failed: ${action.hash} (${response.error})`); return 'RETRY'; } catch (error) { console.error('Error processing message:', error); diff --git a/packages/reader-main/src/messages/transfer.ts b/packages/reader-main/src/messages/transferHandle.ts similarity index 69% rename from packages/reader-main/src/messages/transfer.ts rename to packages/reader-main/src/messages/transferHandle.ts index 978044e1..6ec8b544 100644 --- a/packages/reader-main/src/messages/transfer.ts +++ b/packages/reader-main/src/messages/transferHandle.ts @@ -12,7 +12,7 @@ import { useConfig } from '../config/index'; declare module '@atomone/chronostate' { export namespace MemoExtractor { export interface TypeMap { - 'dither.Transfer': [string, string]; + 'dither.TransferHandle': [string, string]; } } } @@ -20,10 +20,10 @@ declare module '@atomone/chronostate' { const { AUTH } = useConfig(); const apiRoot = process.env.API_ROOT ?? 'http://localhost:3000/v1'; -export async function Transfer(action: ActionWithData): Promise { +export async function TransferHandle(action: ActionWithData): Promise { try { - const [handle, to_address] = extractMemoContent(action.memo, 'dither.Transfer'); - const postBody: Posts.TransferBody = { + const [handle, to_address] = extractMemoContent(action.memo, 'dither.TransferHandle'); + const postBody: Posts.TransferHandleBody = { hash: action.hash, from_address: action.sender, to_address, @@ -31,7 +31,7 @@ export async function Transfer(action: ActionWithData): Promise timestamp: action.timestamp, }; - const rawResponse = await fetch(`${apiRoot}/transfer`, { + const rawResponse = await fetch(`${apiRoot}/transfer-handle`, { method: 'POST', headers: { 'Accept': 'application/json', @@ -48,21 +48,21 @@ export async function Transfer(action: ActionWithData): Promise const response = await rawResponse.json() as { status: number; error?: string }; if (response.status === 200) { - console.log(`dither.Transfer message processed successfully: ${action.hash}`); + console.log(`dither.TransferHandle message processed successfully: ${action.hash}`); return 'SUCCESS'; } if (response.status === 500) { - console.log(`dither.Transfer could not reach database: ${action.hash}`); + console.log(`dither.TransferHandle could not reach database: ${action.hash}`); return 'RETRY'; } if (response.status === 401) { - console.log(`dither.Transfer message skipped, invalid address provided: ${action.hash}`); + console.log(`dither.TransferHandle message skipped, invalid address provided: ${action.hash}`); return 'SKIP'; } - console.warn(`dither.Transfer failed: ${action.hash} (${response.error})`); + console.warn(`dither.TransferHandle failed: ${action.hash} (${response.error})`); return 'RETRY'; } catch (error) { console.error('Error processing message:', error); diff --git a/packages/tool-network-spammer/src/logic.ts b/packages/tool-network-spammer/src/logic.ts index 986efafc..d51ff087 100644 --- a/packages/tool-network-spammer/src/logic.ts +++ b/packages/tool-network-spammer/src/logic.ts @@ -39,7 +39,7 @@ function getRandomHash(): hash { return hashes[Math.floor(Math.random() * hashes.length)]; } -// TODO: Add support for usernames (register, transfer, accept, display) +// TODO: Add support for user handle (registerHandle, transferHandle, acceptHandle, displayHandle) const actions = ['post', 'reply', 'like', 'dislike', 'delete', 'flag'] as const; type ActionType = (typeof actions)[number]; From f9cf2f7b3e61c49d9eb89605d72a47037140d425 Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:23:50 +0100 Subject: [PATCH 16/40] chore: use uppercase for global consts --- packages/api-main/src/posts/displayHandle.ts | 6 +++--- packages/api-main/src/posts/registerHandle.ts | 19 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/api-main/src/posts/displayHandle.ts b/packages/api-main/src/posts/displayHandle.ts index 3c0f4e60..09f7a5d7 100644 --- a/packages/api-main/src/posts/displayHandle.ts +++ b/packages/api-main/src/posts/displayHandle.ts @@ -5,7 +5,7 @@ import { count, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { HandleTable } from '../../drizzle/schema'; import { lower } from '../utility'; -import { maxDisplayLength } from './registerHandle'; +import { MAX_DISPLAY_LENGTH } from './registerHandle'; const handleAddressExistsStmt = getDatabase() .select({ count: count() }) @@ -15,8 +15,8 @@ const handleAddressExistsStmt = getDatabase() export async function DisplayHandle(body: Posts.DisplayHandleBody) { const display = (body.display || '').trim(); - if (display.length > maxDisplayLength) { - return { status: 400, error: `maximum display length is ${maxDisplayLength} characters long` }; + if (display.length > MAX_DISPLAY_LENGTH) { + return { status: 400, error: `maximum display length is ${MAX_DISPLAY_LENGTH} characters long` }; } const db = getDatabase(); diff --git a/packages/api-main/src/posts/registerHandle.ts b/packages/api-main/src/posts/registerHandle.ts index 5064bb8d..d88227b5 100644 --- a/packages/api-main/src/posts/registerHandle.ts +++ b/packages/api-main/src/posts/registerHandle.ts @@ -9,27 +9,26 @@ import { HandleTable } from '../../drizzle/schema'; import { notify } from '../shared/notify'; import { lower } from '../utility'; -const minHandleLength = 5; -const maxHandleLength = 32; - -export const maxDisplayLength = 128; -export const tgHandleRegex = /^[a-z]{3}\w*$/i; +export const MIN_HANDLE_LENGTH = 5; +export const MAX_HANDLE_LENGTH = 32; +export const MAX_DISPLAY_LENGTH = 128; +export const HANDLE_REGEX = /^[a-z]{3}\w*$/i; export async function RegisterHandle(body: Posts.RegisterHandleBody) { - if (!tgHandleRegex.test(body.handle)) { + if (!HANDLE_REGEX.test(body.handle)) { return { status: 400, error: 'handle must start with three letters and can only include letters, numbers and underscores', }; } - if (body.handle.length < minHandleLength || body.handle.length > maxHandleLength) { - return { status: 400, error: `handle must have between ${minHandleLength} and ${maxHandleLength} characters long` }; + if (body.handle.length < MIN_HANDLE_LENGTH || body.handle.length > MAX_HANDLE_LENGTH) { + return { status: 400, error: `handle must have between ${MIN_HANDLE_LENGTH} and ${MAX_HANDLE_LENGTH} characters long` }; } const display = (body.display || '').trim(); - if (display.length > maxDisplayLength) { - return { status: 400, error: `maximum display length is ${maxDisplayLength} characters long` }; + if (display.length > MAX_DISPLAY_LENGTH) { + return { status: 400, error: `maximum display length is ${MAX_DISPLAY_LENGTH} characters long` }; } const db = getDatabase(); From 94477520c27a6ebd0cceee3edfe493bb7839849f Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:21:39 +0100 Subject: [PATCH 17/40] refactor: change HandleTable name to AccountTable Done for future compatibility with user profiles. --- packages/api-main/drizzle/schema.ts | 16 +++++++--------- packages/api-main/src/gets/feed.ts | 8 ++++---- packages/api-main/src/gets/followers.ts | 8 ++++---- packages/api-main/src/gets/following.ts | 8 ++++---- packages/api-main/src/gets/handle.ts | 18 +++++++++--------- packages/api-main/src/gets/post.ts | 12 ++++++------ packages/api-main/src/gets/posts.ts | 8 ++++---- packages/api-main/src/gets/search.ts | 12 ++++++------ packages/api-main/src/posts/acceptHandle.ts | 6 +++--- packages/api-main/src/posts/displayHandle.ts | 10 +++++----- packages/api-main/src/posts/registerHandle.ts | 17 +++++++---------- packages/api-main/src/posts/transferHandle.ts | 8 ++++---- 12 files changed, 63 insertions(+), 68 deletions(-) diff --git a/packages/api-main/drizzle/schema.ts b/packages/api-main/drizzle/schema.ts index f12c0d47..179ba560 100644 --- a/packages/api-main/drizzle/schema.ts +++ b/packages/api-main/drizzle/schema.ts @@ -1,20 +1,18 @@ import { sql } from 'drizzle-orm'; -import { bigint, boolean, index, integer, pgEnum, pgTable, primaryKey, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core'; +import { bigint, boolean, index, integer, pgEnum, pgTable, primaryKey, serial, text, timestamp, unique, varchar } from 'drizzle-orm/pg-core'; const MEMO_LENGTH = 512; -export const HandleTable = pgTable( - 'handle', +export const AccountTable = pgTable( + 'account', { - name: varchar({ length: 32 }).primaryKey(), - address: varchar({ length: 44 }).notNull(), + address: varchar({ length: 44 }).primaryKey(), + handle: varchar({ length: 32 }), display: varchar({ length: 128 }), - hash: varchar({ length: 64 }).notNull(), - timestamp: timestamp({ withTimezone: true }).notNull(), }, t => [ - index('handle_address_idx').on(t.address), - index('handle_display_idx').on(t.display), + unique('account_handle_idx').on(t.handle), + index('account_display_idx').on(t.display), ], ); diff --git a/packages/api-main/src/gets/feed.ts b/packages/api-main/src/gets/feed.ts index 58395bd0..adc77e56 100644 --- a/packages/api-main/src/gets/feed.ts +++ b/packages/api-main/src/gets/feed.ts @@ -3,16 +3,16 @@ import type { Gets } from '@atomone/dither-api-types'; import { and, count, desc, eq, getTableColumns, gte, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { FeedTable, HandleTable } from '../../drizzle/schema'; +import { AccountTable, FeedTable } from '../../drizzle/schema'; const statement = getDatabase() .select({ ...getTableColumns(FeedTable), - handle: HandleTable.name, - display: HandleTable.display, + handle: AccountTable.handle, + display: AccountTable.display, }) .from(FeedTable) - .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) + .leftJoin(AccountTable, eq(FeedTable.author, AccountTable.address)) .limit(sql.placeholder('limit')) .offset(sql.placeholder('offset')) .where( diff --git a/packages/api-main/src/gets/followers.ts b/packages/api-main/src/gets/followers.ts index ca835615..15fe6dce 100644 --- a/packages/api-main/src/gets/followers.ts +++ b/packages/api-main/src/gets/followers.ts @@ -3,17 +3,17 @@ import type { Gets } from '@atomone/dither-api-types'; import { and, desc, eq, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { FollowsTable, HandleTable } from '../../drizzle/schema'; +import { AccountTable, FollowsTable } from '../../drizzle/schema'; const statementGetFollowers = getDatabase() .select({ address: FollowsTable.follower, - handle: HandleTable.name, - display: HandleTable.display, + handle: AccountTable.handle, + display: AccountTable.display, hash: FollowsTable.hash, }) .from(FollowsTable) - .leftJoin(HandleTable, eq(HandleTable.address, FollowsTable.follower)) + .leftJoin(AccountTable, eq(AccountTable.address, FollowsTable.follower)) .where(and(eq(FollowsTable.following, sql.placeholder('following')), isNull(FollowsTable.removed_at))) .limit(sql.placeholder('limit')) .offset(sql.placeholder('offset')) diff --git a/packages/api-main/src/gets/following.ts b/packages/api-main/src/gets/following.ts index 948d6160..46e5b35c 100644 --- a/packages/api-main/src/gets/following.ts +++ b/packages/api-main/src/gets/following.ts @@ -3,17 +3,17 @@ import type { Gets } from '@atomone/dither-api-types'; import { and, desc, eq, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { FollowsTable, HandleTable } from '../../drizzle/schema'; +import { AccountTable, FollowsTable } from '../../drizzle/schema'; const statementGetFollowing = getDatabase() .select({ address: FollowsTable.following, - handle: HandleTable.name, - display: HandleTable.display, + handle: AccountTable.handle, + display: AccountTable.display, hash: FollowsTable.hash, }) .from(FollowsTable) - .leftJoin(HandleTable, eq(HandleTable.address, FollowsTable.follower)) + .leftJoin(AccountTable, eq(AccountTable.address, FollowsTable.follower)) .where(and(eq(FollowsTable.follower, sql.placeholder('follower')), isNull(FollowsTable.removed_at))) .limit(sql.placeholder('limit')) .offset(sql.placeholder('offset')) diff --git a/packages/api-main/src/gets/handle.ts b/packages/api-main/src/gets/handle.ts index a6e43644..8aeb5a2f 100644 --- a/packages/api-main/src/gets/handle.ts +++ b/packages/api-main/src/gets/handle.ts @@ -3,22 +3,22 @@ import type { Gets } from '@atomone/dither-api-types'; import { eq, or, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { HandleTable } from '../../drizzle/schema'; +import { AccountTable } from '../../drizzle/schema'; const statement = getDatabase() .select({ - name: HandleTable.name, - address: HandleTable.address, - display: HandleTable.display, + handle: AccountTable.handle, + address: AccountTable.address, + display: AccountTable.display, }) - .from(HandleTable) + .from(AccountTable) .where( or( - eq(HandleTable.name, sql.placeholder('name')), - eq(HandleTable.address, sql.placeholder('address')), + eq(AccountTable.handle, sql.placeholder('handle')), + eq(AccountTable.address, sql.placeholder('address')), ), ) - .orderBy(HandleTable.name) + .orderBy(AccountTable.handle) .limit(1) .prepare('stmnt_get_handle'); @@ -29,7 +29,7 @@ export async function Handle(query: Gets.HandleQuery) { } try { - const [handle] = await statement.execute({ address, name }); + const [handle] = await statement.execute({ address, handle: name }); if (!handle) { return { status: 404, rows: [] }; } diff --git a/packages/api-main/src/gets/post.ts b/packages/api-main/src/gets/post.ts index 63f3a387..0b09059f 100644 --- a/packages/api-main/src/gets/post.ts +++ b/packages/api-main/src/gets/post.ts @@ -3,26 +3,26 @@ import type { Gets } from '@atomone/dither-api-types'; import { and, eq, getTableColumns, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { FeedTable, HandleTable } from '../../drizzle/schema'; +import { AccountTable, FeedTable } from '../../drizzle/schema'; const statementGetPost = getDatabase() .select({ ...getTableColumns(FeedTable), - handle: HandleTable.name, - display: HandleTable.display, + handle: AccountTable.handle, + display: AccountTable.display, }) .from(FeedTable) - .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) + .leftJoin(AccountTable, eq(FeedTable.author, AccountTable.address)) .where(and(isNull(FeedTable.removed_at), eq(FeedTable.hash, sql.placeholder('hash')))) .prepare('stmnt_get_post'); const statementGetReply = getDatabase() .select({ ...getTableColumns(FeedTable), - handle: HandleTable.name, + handle: AccountTable.handle, }) .from(FeedTable) - .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) + .leftJoin(AccountTable, eq(FeedTable.author, AccountTable.address)) .where(and(isNull(FeedTable.removed_at), eq(FeedTable.hash, sql.placeholder('hash')), eq(FeedTable.post_hash, sql.placeholder('post_hash')))) .prepare('stmnt_get_reply'); diff --git a/packages/api-main/src/gets/posts.ts b/packages/api-main/src/gets/posts.ts index 4ffd3787..400ee438 100644 --- a/packages/api-main/src/gets/posts.ts +++ b/packages/api-main/src/gets/posts.ts @@ -3,16 +3,16 @@ import type { Gets } from '@atomone/dither-api-types'; import { and, desc, eq, getTableColumns, gte, inArray, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { FeedTable, FollowsTable, HandleTable } from '../../drizzle/schema'; +import { AccountTable, FeedTable, FollowsTable } from '../../drizzle/schema'; const statement = getDatabase() .select({ ...getTableColumns(FeedTable), - handle: HandleTable.name, - display: HandleTable.display, + handle: AccountTable.handle, + display: AccountTable.display, }) .from(FeedTable) - .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) + .leftJoin(AccountTable, eq(FeedTable.author, AccountTable.address)) .where( and( eq(FeedTable.author, sql.placeholder('author')), diff --git a/packages/api-main/src/gets/search.ts b/packages/api-main/src/gets/search.ts index 3dcc666e..f5ffefa4 100644 --- a/packages/api-main/src/gets/search.ts +++ b/packages/api-main/src/gets/search.ts @@ -3,7 +3,7 @@ import type { Gets } from '@atomone/dither-api-types'; import { and, desc, eq, getTableColumns, gte, ilike, inArray, isNull, or, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { FeedTable, HandleTable } from '../../drizzle/schema'; +import { AccountTable, FeedTable } from '../../drizzle/schema'; export async function Search(query: Gets.SearchQuery) { try { @@ -22,12 +22,12 @@ export async function Search(query: Gets.SearchQuery) { const matchedAuthors = await getDatabase() .selectDistinct({ author: FeedTable.author }) .from(FeedTable) - .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) + .leftJoin(AccountTable, eq(FeedTable.author, AccountTable.address)) .where( and( or( eq(FeedTable.author, query.text.toLowerCase()), // Exact address - ilike(HandleTable.name, `%${query.text}%`), // Registered handle (partial match) + ilike(AccountTable.handle, `%${query.text}%`), // Registered handle (partial match) ), isNull(FeedTable.removed_at), ), @@ -37,11 +37,11 @@ export async function Search(query: Gets.SearchQuery) { const matchedPosts = await getDatabase() .select({ ...getTableColumns(FeedTable), - handle: HandleTable.name, - display: HandleTable.display, + handle: AccountTable.handle, + display: AccountTable.display, }) .from(FeedTable) - .leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address)) + .leftJoin(AccountTable, eq(FeedTable.author, AccountTable.address)) .where( and( or( diff --git a/packages/api-main/src/posts/acceptHandle.ts b/packages/api-main/src/posts/acceptHandle.ts index cb9fef30..b258c744 100644 --- a/packages/api-main/src/posts/acceptHandle.ts +++ b/packages/api-main/src/posts/acceptHandle.ts @@ -5,7 +5,7 @@ import type { DbClient } from '../../drizzle/db'; import { and, eq } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { HandleTable, HandleTransferTable } from '../../drizzle/schema'; +import { AccountTable, HandleTransferTable } from '../../drizzle/schema'; import { lower } from '../utility'; export async function AcceptHandle(body: Posts.AcceptHandleBody) { @@ -16,9 +16,9 @@ export async function AcceptHandle(body: Posts.AcceptHandleBody) { } await db - .update(HandleTable) + .update(AccountTable) .set({ address: body.to_address.toLowerCase() }) - .where(eq(lower(HandleTable.name), body.handle.toLowerCase())); + .where(eq(lower(AccountTable.handle), body.handle.toLowerCase())); return { status: 200 }; } catch (err) { diff --git a/packages/api-main/src/posts/displayHandle.ts b/packages/api-main/src/posts/displayHandle.ts index 09f7a5d7..cc67bc2c 100644 --- a/packages/api-main/src/posts/displayHandle.ts +++ b/packages/api-main/src/posts/displayHandle.ts @@ -3,14 +3,14 @@ import type { Posts } from '@atomone/dither-api-types'; import { count, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { HandleTable } from '../../drizzle/schema'; +import { AccountTable } from '../../drizzle/schema'; import { lower } from '../utility'; import { MAX_DISPLAY_LENGTH } from './registerHandle'; const handleAddressExistsStmt = getDatabase() .select({ count: count() }) - .from(HandleTable) - .where(eq(lower(HandleTable.address), sql.placeholder('address'))) + .from(AccountTable) + .where(eq(lower(AccountTable.address), sql.placeholder('address'))) .prepare('stmt_handle_address_exists'); export async function DisplayHandle(body: Posts.DisplayHandleBody) { @@ -27,9 +27,9 @@ export async function DisplayHandle(body: Posts.DisplayHandleBody) { } await db - .update(HandleTable) + .update(AccountTable) .set({ display }) - .where(eq(HandleTable.address, address)); + .where(eq(AccountTable.address, address)); return { status: 200 }; } catch (err) { diff --git a/packages/api-main/src/posts/registerHandle.ts b/packages/api-main/src/posts/registerHandle.ts index d88227b5..aab4628c 100644 --- a/packages/api-main/src/posts/registerHandle.ts +++ b/packages/api-main/src/posts/registerHandle.ts @@ -5,7 +5,7 @@ import type { DbClient } from '../../drizzle/db'; import { eq } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { HandleTable } from '../../drizzle/schema'; +import { AccountTable } from '../../drizzle/schema'; import { notify } from '../shared/notify'; import { lower } from '../utility'; @@ -33,14 +33,13 @@ export async function RegisterHandle(body: Posts.RegisterHandleBody) { const db = getDatabase(); try { - const timestamp = new Date(body.timestamp); if (await doesHandleExists(db, body.handle)) { await notify({ hash: body.hash, type: 'register', actor: body.from, owner: body.from, - timestamp, + timestamp: new Date(body.timestamp), subcontext: 'Handle is already taken', }); @@ -53,18 +52,16 @@ export async function RegisterHandle(body: Posts.RegisterHandleBody) { await db.transaction(async (tx) => { // Remove current handle if one is registered to address await tx - .delete(HandleTable) - .where(eq(HandleTable.address, address)); + .delete(AccountTable) + .where(eq(AccountTable.address, address)); // Register a new handle for the address await tx - .insert(HandleTable) + .insert(AccountTable) .values({ - name: body.handle, - hash: body.hash.toLowerCase(), + handle: body.handle, address: body.from.toLowerCase(), display: display || null, - timestamp, }); }); @@ -76,6 +73,6 @@ export async function RegisterHandle(body: Posts.RegisterHandleBody) { } async function doesHandleExists(db: DbClient, name: string): Promise { - const count = await db.$count(HandleTable, eq(lower(HandleTable.name), name.toLowerCase())); + const count = await db.$count(AccountTable, eq(lower(AccountTable.handle), name.toLowerCase())); return count !== 0; } diff --git a/packages/api-main/src/posts/transferHandle.ts b/packages/api-main/src/posts/transferHandle.ts index 7a8d7918..3ca671e7 100644 --- a/packages/api-main/src/posts/transferHandle.ts +++ b/packages/api-main/src/posts/transferHandle.ts @@ -5,7 +5,7 @@ import type { DbClient } from '../../drizzle/db'; import { and, eq } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; -import { HandleTable, HandleTransferTable } from '../../drizzle/schema'; +import { AccountTable, HandleTransferTable } from '../../drizzle/schema'; import { lower } from '../utility'; export async function TransferHandle(body: Posts.TransferHandleBody) { @@ -48,9 +48,9 @@ export async function TransferHandle(body: Posts.TransferHandleBody) { } async function isHandleOwner(db: DbClient, handle: string, address: string): Promise { - const count = await db.$count(HandleTable, and( - eq(lower(HandleTable.name), handle.toLowerCase()), - eq(HandleTable.address, address.toLowerCase()), + const count = await db.$count(AccountTable, and( + eq(lower(AccountTable.handle), handle.toLowerCase()), + eq(AccountTable.address, address.toLowerCase()), )); return count !== 0; } From bf956b9424e29abe5a271a16d118689dcb2e2e6f Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:24:22 +0100 Subject: [PATCH 18/40] refactor: update user handle protocol features for account table Handle table was replaced by account table to make it easier for new upcoming user related features to be implemented. --- packages/api-main/drizzle/schema.ts | 1 + packages/api-main/src/posts/acceptHandle.ts | 68 +++++++++++++++---- packages/api-main/src/posts/displayHandle.ts | 35 +++------- packages/api-main/src/posts/registerHandle.ts | 48 ++++++------- packages/api-main/src/posts/transferHandle.ts | 67 +++++++++--------- packages/frontend-main/ARCHITECTURE.md | 4 +- packages/lib-api-types/src/posts/index.ts | 10 +-- .../reader-main/src/messages/acceptHandle.ts | 4 +- .../src/messages/registerHandle.ts | 7 +- .../src/messages/setDisplayHandle.ts | 5 +- .../src/messages/transferHandle.ts | 4 +- 11 files changed, 128 insertions(+), 125 deletions(-) diff --git a/packages/api-main/drizzle/schema.ts b/packages/api-main/drizzle/schema.ts index 179ba560..b9ea039b 100644 --- a/packages/api-main/drizzle/schema.ts +++ b/packages/api-main/drizzle/schema.ts @@ -23,6 +23,7 @@ export const HandleTransferTable = pgTable( name: varchar({ length: 32 }).notNull(), from_address: varchar({ length: 44 }).notNull(), to_address: varchar({ length: 44 }).notNull(), + accepted: boolean().default(false).notNull(), timestamp: timestamp({ withTimezone: true }).notNull(), }, t => [ diff --git a/packages/api-main/src/posts/acceptHandle.ts b/packages/api-main/src/posts/acceptHandle.ts index b258c744..9acb438e 100644 --- a/packages/api-main/src/posts/acceptHandle.ts +++ b/packages/api-main/src/posts/acceptHandle.ts @@ -1,24 +1,65 @@ import type { Posts } from '@atomone/dither-api-types'; -import type { DbClient } from '../../drizzle/db'; - -import { and, eq } from 'drizzle-orm'; +import { and, count, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { AccountTable, HandleTransferTable } from '../../drizzle/schema'; import { lower } from '../utility'; +const transferToAddressExistsStmt = getDatabase() + .select({ count: count() }) + .from(HandleTransferTable) + .where( + and( + eq(lower(HandleTransferTable.name), sql.placeholder('handle')), + eq(HandleTransferTable.to_address, sql.placeholder('address')), + eq(HandleTransferTable.accepted, false), + ), + ) + .prepare('stmt_transfer_to_adress_exists'); + export async function AcceptHandle(body: Posts.AcceptHandleBody) { - const db = getDatabase(); + const address = body.address.toLowerCase(); + const handle = body.handle.toLowerCase(); + try { - if (!await doesTransferToAddressExists(db, body.handle, body.to_address)) { + if (!await doesTransferToAddressExists(handle, address)) { return { status: 400, error: 'handle not found or not transferred to address' }; } - await db - .update(AccountTable) - .set({ address: body.to_address.toLowerCase() }) - .where(eq(lower(AccountTable.handle), body.handle.toLowerCase())); + await getDatabase().transaction(async (tx) => { + // Remove handle from current owner + await tx + .update(AccountTable) + .set({ handle: null }) + .where(eq(lower(AccountTable.handle), handle)); + + // Assign handle to new owner + await tx + .insert(AccountTable) + .values({ + address, + handle: body.handle, + }) + .onConflictDoUpdate({ + target: AccountTable.address, + set: { + handle: body.handle, + }, + }); + + // Mark transfer(s) as finished to disallow potential future usage of this transfer + await tx + .update(HandleTransferTable) + .set({ accepted: true }) + .where( + and( + eq(lower(HandleTransferTable.name), handle), + eq(HandleTransferTable.to_address, address), + eq(HandleTransferTable.accepted, false), + ), + ); + }); return { status: 200 }; } catch (err) { @@ -27,10 +68,7 @@ export async function AcceptHandle(body: Posts.AcceptHandleBody) { } } -async function doesTransferToAddressExists(db: DbClient, handle: string, address: string): Promise { - const count = await db.$count(HandleTransferTable, and( - eq(lower(HandleTransferTable.name), handle.toLowerCase()), - eq(HandleTransferTable.to_address, address.toLowerCase()), - )); - return count !== 0; +async function doesTransferToAddressExists(handle: string, address: string): Promise { + const [result] = await transferToAddressExistsStmt.execute({ address, handle }); + return (result?.count ?? 0) !== 0; } diff --git a/packages/api-main/src/posts/displayHandle.ts b/packages/api-main/src/posts/displayHandle.ts index cc67bc2c..8fe846f5 100644 --- a/packages/api-main/src/posts/displayHandle.ts +++ b/packages/api-main/src/posts/displayHandle.ts @@ -1,17 +1,9 @@ import type { Posts } from '@atomone/dither-api-types'; -import { count, eq, sql } from 'drizzle-orm'; - import { getDatabase } from '../../drizzle/db'; import { AccountTable } from '../../drizzle/schema'; -import { lower } from '../utility'; -import { MAX_DISPLAY_LENGTH } from './registerHandle'; -const handleAddressExistsStmt = getDatabase() - .select({ count: count() }) - .from(AccountTable) - .where(eq(lower(AccountTable.address), sql.placeholder('address'))) - .prepare('stmt_handle_address_exists'); +const MAX_DISPLAY_LENGTH = 128; export async function DisplayHandle(body: Posts.DisplayHandleBody) { const display = (body.display || '').trim(); @@ -19,17 +11,17 @@ export async function DisplayHandle(body: Posts.DisplayHandleBody) { return { status: 400, error: `maximum display length is ${MAX_DISPLAY_LENGTH} characters long` }; } - const db = getDatabase(); try { - const address = body.from.toLowerCase(); - if (!await hasHandle(address)) { - return { status: 400, error: 'account requires a handle to set a display text' }; - } - - await db - .update(AccountTable) - .set({ display }) - .where(eq(AccountTable.address, address)); + await getDatabase() + .insert(AccountTable) + .values({ + address: body.address.toLowerCase(), + display, + }) + .onConflictDoUpdate({ + target: AccountTable.address, + set: { display }, + }); return { status: 200 }; } catch (err) { @@ -37,8 +29,3 @@ export async function DisplayHandle(body: Posts.DisplayHandleBody) { return { status: 400, error: 'failed to update display handle' }; } } - -async function hasHandle(address: string): Promise { - const [result] = await handleAddressExistsStmt.execute({ address }); - return (result?.count ?? 0) !== 0; -} diff --git a/packages/api-main/src/posts/registerHandle.ts b/packages/api-main/src/posts/registerHandle.ts index aab4628c..075b512a 100644 --- a/packages/api-main/src/posts/registerHandle.ts +++ b/packages/api-main/src/posts/registerHandle.ts @@ -9,10 +9,9 @@ import { AccountTable } from '../../drizzle/schema'; import { notify } from '../shared/notify'; import { lower } from '../utility'; -export const MIN_HANDLE_LENGTH = 5; -export const MAX_HANDLE_LENGTH = 32; -export const MAX_DISPLAY_LENGTH = 128; -export const HANDLE_REGEX = /^[a-z]{3}\w*$/i; +const MIN_HANDLE_LENGTH = 5; +const MAX_HANDLE_LENGTH = 32; +const HANDLE_REGEX = /^[a-z]{3}\w*$/i; export async function RegisterHandle(body: Posts.RegisterHandleBody) { if (!HANDLE_REGEX.test(body.handle)) { @@ -26,44 +25,35 @@ export async function RegisterHandle(body: Posts.RegisterHandleBody) { return { status: 400, error: `handle must have between ${MIN_HANDLE_LENGTH} and ${MAX_HANDLE_LENGTH} characters long` }; } - const display = (body.display || '').trim(); - if (display.length > MAX_DISPLAY_LENGTH) { - return { status: 400, error: `maximum display length is ${MAX_DISPLAY_LENGTH} characters long` }; - } - const db = getDatabase(); + const address = body.address.toLowerCase(); + try { if (await doesHandleExists(db, body.handle)) { await notify({ hash: body.hash, type: 'register', - actor: body.from, - owner: body.from, + actor: address, + owner: address, timestamp: new Date(body.timestamp), subcontext: 'Handle is already taken', }); - // Succeed to stop reader from keep trying to register - return { status: 200, error: 'handle is already registered' }; + return { status: 400, error: 'handle is already registered' }; } - const address = body.from.toLowerCase(); - - await db.transaction(async (tx) => { - // Remove current handle if one is registered to address - await tx - .delete(AccountTable) - .where(eq(AccountTable.address, address)); - - // Register a new handle for the address - await tx - .insert(AccountTable) - .values({ + await db + .insert(AccountTable) + .values({ + address, + handle: body.handle, + }) + .onConflictDoUpdate({ + target: AccountTable.address, + set: { handle: body.handle, - address: body.from.toLowerCase(), - display: display || null, - }); - }); + }, + }); return { status: 200 }; } catch (err) { diff --git a/packages/api-main/src/posts/transferHandle.ts b/packages/api-main/src/posts/transferHandle.ts index 3ca671e7..18e4bed7 100644 --- a/packages/api-main/src/posts/transferHandle.ts +++ b/packages/api-main/src/posts/transferHandle.ts @@ -1,44 +1,39 @@ import type { Posts } from '@atomone/dither-api-types'; -import type { DbClient } from '../../drizzle/db'; - -import { and, eq } from 'drizzle-orm'; +import { and, count, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { AccountTable, HandleTransferTable } from '../../drizzle/schema'; import { lower } from '../utility'; +const handleOwnerExistsStmt = getDatabase() + .select({ count: count() }) + .from(AccountTable) + .where( + and( + eq(AccountTable.address, sql.placeholder('address')), + eq(lower(AccountTable.handle), sql.placeholder('handle')), + ), + ) + .prepare('stmt_handle_owner_exists'); + export async function TransferHandle(body: Posts.TransferHandleBody) { - const db = getDatabase(); + const fromAddress = body.from_address.toLowerCase(); + try { - if (!await isHandleOwner(db, body.handle, body.from_address)) { - return { status: 400, error: 'handle not found or not registered for address' }; + if (!await isHandleOwner(fromAddress, body.handle)) { + return { status: 400, error: 'handle not found or not registered to sender address' }; } - const fromAddress = body.from_address.toLowerCase(); - - await db.transaction(async (tx) => { - // If it exists delete previous transfer for the same handle - await tx - .delete(HandleTransferTable) - .where( - and( - eq(lower(HandleTransferTable.name), body.handle.toLowerCase()), - eq(HandleTransferTable.from_address, fromAddress), - ), - ); - - // Add a handle transfer to a new address - await tx - .insert(HandleTransferTable) - .values({ - hash: body.hash.toLowerCase(), - name: body.handle, - from_address: fromAddress, - to_address: body.to_address.toLowerCase(), - timestamp: new Date(body.timestamp), - }); - }); + await getDatabase() + .insert(HandleTransferTable) + .values({ + hash: body.hash.toLowerCase(), + name: body.handle, + from_address: fromAddress, + to_address: body.to_address.toLowerCase(), + timestamp: new Date(body.timestamp), + }); return { status: 200 }; } catch (err) { @@ -47,10 +42,10 @@ export async function TransferHandle(body: Posts.TransferHandleBody) { } } -async function isHandleOwner(db: DbClient, handle: string, address: string): Promise { - const count = await db.$count(AccountTable, and( - eq(lower(AccountTable.handle), handle.toLowerCase()), - eq(AccountTable.address, address.toLowerCase()), - )); - return count !== 0; +async function isHandleOwner(address: string, handle: string): Promise { + const [result] = await handleOwnerExistsStmt.execute({ + address, + handle: handle.toLowerCase(), + }); + return (result?.count ?? 0) !== 0; } diff --git a/packages/frontend-main/ARCHITECTURE.md b/packages/frontend-main/ARCHITECTURE.md index 447e3b23..dba06bcc 100644 --- a/packages/frontend-main/ARCHITECTURE.md +++ b/packages/frontend-main/ARCHITECTURE.md @@ -195,11 +195,11 @@ dither.Unfollow('cosmos1user...'); dither.Dislike('0xjkl012...'); // Username registration and transfer to a new address -dither.RegisterHandle('handle', 'display text'); +dither.RegisterHandle('handle'); dither.TransferHandle('handle', '0xabc123...'); dither.AcceptHandle('handle'); -// Display text change for registered user handle +// Modify user display text dither.SetDisplayHandle('display text'); ``` diff --git a/packages/lib-api-types/src/posts/index.ts b/packages/lib-api-types/src/posts/index.ts index b081fb84..7d652d8c 100644 --- a/packages/lib-api-types/src/posts/index.ts +++ b/packages/lib-api-types/src/posts/index.ts @@ -102,18 +102,16 @@ export type UnfollowBody = Static; export const RegisterHandleBodySchema = t.Object({ hash: t.String(), - from: t.String(), + address: t.String(), handle: t.String(), - display: t.String(), timestamp: t.String(), }); export type RegisterHandleBody = Static; export const DisplayHandleBodySchema = t.Object({ hash: t.String(), - from: t.String(), + address: t.String(), display: t.String(), - timestamp: t.String(), }); export type DisplayHandleBody = Static; @@ -127,10 +125,8 @@ export const TransferHandleBodySchema = t.Object({ export type TransferHandleBody = Static; export const AcceptHandleBodySchema = t.Object({ - hash: t.String(), - from: t.String(), + address: t.String(), handle: t.String(), - timestamp: t.String(), }); export type AcceptHandleBody = Static; diff --git a/packages/reader-main/src/messages/acceptHandle.ts b/packages/reader-main/src/messages/acceptHandle.ts index 8909b021..67e5fd11 100644 --- a/packages/reader-main/src/messages/acceptHandle.ts +++ b/packages/reader-main/src/messages/acceptHandle.ts @@ -24,10 +24,8 @@ export async function AcceptHandle(action: ActionWithData): Promise { try { - const [handle, display] = extractMemoContent(action.memo, 'dither.RegisterHandle'); + const [handle] = extractMemoContent(action.memo, 'dither.RegisterHandle'); const postBody: Posts.RegisterHandleBody = { hash: action.hash, - from: action.sender, + address: action.sender, handle, - display, timestamp: action.timestamp, }; diff --git a/packages/reader-main/src/messages/setDisplayHandle.ts b/packages/reader-main/src/messages/setDisplayHandle.ts index b55c64c5..29fc71b1 100644 --- a/packages/reader-main/src/messages/setDisplayHandle.ts +++ b/packages/reader-main/src/messages/setDisplayHandle.ts @@ -24,10 +24,9 @@ export async function SetDisplayHandle(action: ActionWithData): Promise Date: Thu, 20 Nov 2025 19:45:00 +0100 Subject: [PATCH 19/40] feat: add notification when a handle is transfered to a user --- packages/api-main/drizzle/schema.ts | 1 + packages/api-main/src/posts/registerHandle.ts | 2 +- packages/api-main/src/posts/transferHandle.ts | 14 +++++++++++++- .../components/notifications/NotificationType.vue | 5 +++-- .../notifications/NotificationWrapper.vue | 2 +- .../notifications/TransferHandleNotification.vue | 14 ++++++++++++++ packages/frontend-main/src/localization/index.ts | 1 + .../frontend-main/src/views/NotificationsView.vue | 2 ++ 8 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 packages/frontend-main/src/components/notifications/TransferHandleNotification.vue diff --git a/packages/api-main/drizzle/schema.ts b/packages/api-main/drizzle/schema.ts index b9ea039b..0b99ec0b 100644 --- a/packages/api-main/drizzle/schema.ts +++ b/packages/api-main/drizzle/schema.ts @@ -164,6 +164,7 @@ export const notificationTypeEnum = pgEnum('notification_type', [ 'follow', 'reply', 'registerHandle', + 'transferHandle', ]); export const NotificationTable = pgTable( diff --git a/packages/api-main/src/posts/registerHandle.ts b/packages/api-main/src/posts/registerHandle.ts index 075b512a..0a3c0be9 100644 --- a/packages/api-main/src/posts/registerHandle.ts +++ b/packages/api-main/src/posts/registerHandle.ts @@ -32,7 +32,7 @@ export async function RegisterHandle(body: Posts.RegisterHandleBody) { if (await doesHandleExists(db, body.handle)) { await notify({ hash: body.hash, - type: 'register', + type: 'registerHandle', actor: address, owner: address, timestamp: new Date(body.timestamp), diff --git a/packages/api-main/src/posts/transferHandle.ts b/packages/api-main/src/posts/transferHandle.ts index 18e4bed7..beecb1ed 100644 --- a/packages/api-main/src/posts/transferHandle.ts +++ b/packages/api-main/src/posts/transferHandle.ts @@ -4,6 +4,7 @@ import { and, count, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { AccountTable, HandleTransferTable } from '../../drizzle/schema'; +import { notify } from '../shared/notify'; import { lower } from '../utility'; const handleOwnerExistsStmt = getDatabase() @@ -19,6 +20,8 @@ const handleOwnerExistsStmt = getDatabase() export async function TransferHandle(body: Posts.TransferHandleBody) { const fromAddress = body.from_address.toLowerCase(); + const toAddress = body.to_address.toLowerCase(); + const timestamp = new Date(body.timestamp); try { if (!await isHandleOwner(fromAddress, body.handle)) { @@ -32,9 +35,18 @@ export async function TransferHandle(body: Posts.TransferHandleBody) { name: body.handle, from_address: fromAddress, to_address: body.to_address.toLowerCase(), - timestamp: new Date(body.timestamp), + timestamp, }); + await notify({ + hash: body.hash, + type: 'transferHandle', + actor: fromAddress, + owner: toAddress, + subcontext: `You received @${body.handle} handle`, + timestamp, + }); + return { status: 200 }; } catch (err) { console.error(err); diff --git a/packages/frontend-main/src/components/notifications/NotificationType.vue b/packages/frontend-main/src/components/notifications/NotificationType.vue index ccfeab37..afa3266f 100644 --- a/packages/frontend-main/src/components/notifications/NotificationType.vue +++ b/packages/frontend-main/src/components/notifications/NotificationType.vue @@ -1,7 +1,7 @@ + + diff --git a/packages/frontend-main/src/localization/index.ts b/packages/frontend-main/src/localization/index.ts index 8c931283..1a8039b6 100644 --- a/packages/frontend-main/src/localization/index.ts +++ b/packages/frontend-main/src/localization/index.ts @@ -118,6 +118,7 @@ export const messages = { follow: 'Followed you', reply: 'Replied to your post', registerHandle: 'User handle registration', + transferHandle: 'Sent you a user handle', empty: 'Nothing to show', }, FollowingList: { diff --git a/packages/frontend-main/src/views/NotificationsView.vue b/packages/frontend-main/src/views/NotificationsView.vue index 128d1cbf..f2a13079 100644 --- a/packages/frontend-main/src/views/NotificationsView.vue +++ b/packages/frontend-main/src/views/NotificationsView.vue @@ -8,6 +8,7 @@ import FollowNotification from '@/components/notifications/FollowNotification.vu import LikeNotification from '@/components/notifications/LikeNotification.vue'; import RegisterHandleNotification from '@/components/notifications/RegisterHandleNotification.vue'; import ReplyNotification from '@/components/notifications/ReplyNotification.vue'; +import TransferHandleNotification from '@/components/notifications/TransferHandleNotification.vue'; import Button from '@/components/ui/button/Button.vue'; import { useNotifications } from '@/composables/useNotifications'; import { useWallet } from '@/composables/useWallet'; @@ -40,6 +41,7 @@ const flatNotifications = computed(() => data.value?.pages.flat() ?? []); +
From f576d6bace5cc01cdb117cabf48a1163a19ef971 Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:30:27 +0100 Subject: [PATCH 20/40] fix: change Tiltfile to reload on code changes Previous configuration was reloading but updates were not recognized. --- Tiltfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tiltfile b/Tiltfile index cde1a833..407b65e5 100644 --- a/Tiltfile +++ b/Tiltfile @@ -5,7 +5,7 @@ docker_compose('docker-compose.yml') docker_build('ditherchat/api-main', '.', dockerfile = './packages/api-main/Dockerfile', live_update = [ - sync('./packages/api-main/src', '/app'), + sync('./packages/api-main/src', '/app/packages/api-main/src'), run('bun install', trigger='package.json'), restart_container(), ]) @@ -13,7 +13,7 @@ docker_build('ditherchat/api-main', '.', docker_build('ditherchat/reader-main', '.', dockerfile = './packages/reader-main/Dockerfile', live_update = [ - sync('./packages/reader-main/src', '/app'), + sync('./packages/reader-main/src', '/app/packages/reader-main/src'), run('bun install', trigger='package.json'), restart_container(), ]) From ff9eb48686d9972fd207c954b5a35b7b89b7e5df Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:31:34 +0100 Subject: [PATCH 21/40] chore: change reader docker compose to use testnet API by default --- packages/reader-main/docker-compose.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/reader-main/docker-compose.yml b/packages/reader-main/docker-compose.yml index cb6058b4..c138d342 100644 --- a/packages/reader-main/docker-compose.yml +++ b/packages/reader-main/docker-compose.yml @@ -6,11 +6,12 @@ services: restart: always container_name: chronosync environment: - # Note: It seems all in bits node is not keeping a big history, so REST requests - # fail when trying to get previous TXs, poluting logs. - # API_URLS: 'https://atomone-api.allinbits.com,https://atomone-rest.publicnode.com' + # Mainnet + # API_URLS: 'https://atomone-rest.publicnode.com' + # + # Testnet + API_URLS: 'https://atomone-testnet-1-api.allinbits.services' - API_URLS: 'https://atomone-rest.publicnode.com' START_BLOCK: '2605764' BATCH_SIZE: 50 MEMO_PREFIX: dither. From eb2fef3d899a9d15fe5d3e4303e274b424556b66 Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:53:50 +0100 Subject: [PATCH 22/40] chore: update dockerignore --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index 5779b5a6..b3350cee 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,5 @@ **/node_modules **/dist **/dist/* +.husky/ +.vscode/ From 25e035043c9acc78c9c8a18887b7d6d14663b632 Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:54:34 +0100 Subject: [PATCH 23/40] chore: allow requests to `https://*.allinbits.services` --- packages/frontend-main/public/csp.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/frontend-main/public/csp.json b/packages/frontend-main/public/csp.json index 98044a80..bedd6c64 100644 --- a/packages/frontend-main/public/csp.json +++ b/packages/frontend-main/public/csp.json @@ -26,6 +26,7 @@ "https://cloud.umami.is", "https://api-gateway.umami.dev", "https://*.dither.chat", - "https://*.allinbits.com" + "https://*.allinbits.com", + "https://*.allinbits.services" ] } From 1a9cc2f6df339f78546fd4a0e9b2d87b24284c29 Mon Sep 17 00:00:00 2001 From: jeronimoalbi <894299+jeronimoalbi@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:56:52 +0100 Subject: [PATCH 24/40] feat: change UI to render user handle when available --- packages/api-main/src/gets/feed.ts | 4 ++-- packages/api-main/src/gets/post.ts | 7 ++++--- packages/api-main/src/gets/posts.ts | 11 ++++++++--- packages/api-main/src/gets/search.ts | 4 ++-- packages/api-main/src/types/feed.ts | 5 ++++- .../src/components/popups/FlagPostDialog.vue | 3 ++- .../src/components/popups/ReplyDialog.vue | 3 ++- .../frontend-main/src/components/posts/PostItem.vue | 3 ++- .../src/components/ui/search/SearchInput.vue | 3 ++- packages/frontend-main/src/types/index.ts | 12 ++++++++++++ packages/frontend-main/src/utility/text.ts | 12 ++++++++++++ .../frontend-main/src/views/ManageFollowingView.vue | 2 +- packages/frontend-main/src/views/PostView.vue | 3 ++- 13 files changed, 55 insertions(+), 17 deletions(-) diff --git a/packages/api-main/src/gets/feed.ts b/packages/api-main/src/gets/feed.ts index adc77e56..f842f8b2 100644 --- a/packages/api-main/src/gets/feed.ts +++ b/packages/api-main/src/gets/feed.ts @@ -8,8 +8,8 @@ import { AccountTable, FeedTable } from '../../drizzle/schema'; const statement = getDatabase() .select({ ...getTableColumns(FeedTable), - handle: AccountTable.handle, - display: AccountTable.display, + author_handle: AccountTable.handle, + author_display: AccountTable.display, }) .from(FeedTable) .leftJoin(AccountTable, eq(FeedTable.author, AccountTable.address)) diff --git a/packages/api-main/src/gets/post.ts b/packages/api-main/src/gets/post.ts index 0b09059f..9485f0da 100644 --- a/packages/api-main/src/gets/post.ts +++ b/packages/api-main/src/gets/post.ts @@ -8,8 +8,8 @@ import { AccountTable, FeedTable } from '../../drizzle/schema'; const statementGetPost = getDatabase() .select({ ...getTableColumns(FeedTable), - handle: AccountTable.handle, - display: AccountTable.display, + author_handle: AccountTable.handle, + author_display: AccountTable.display, }) .from(FeedTable) .leftJoin(AccountTable, eq(FeedTable.author, AccountTable.address)) @@ -19,7 +19,8 @@ const statementGetPost = getDatabase() const statementGetReply = getDatabase() .select({ ...getTableColumns(FeedTable), - handle: AccountTable.handle, + author_handle: AccountTable.handle, + author_display: AccountTable.display, }) .from(FeedTable) .leftJoin(AccountTable, eq(FeedTable.author, AccountTable.address)) diff --git a/packages/api-main/src/gets/posts.ts b/packages/api-main/src/gets/posts.ts index 400ee438..0638ed0b 100644 --- a/packages/api-main/src/gets/posts.ts +++ b/packages/api-main/src/gets/posts.ts @@ -8,8 +8,8 @@ import { AccountTable, FeedTable, FollowsTable } from '../../drizzle/schema'; const statement = getDatabase() .select({ ...getTableColumns(FeedTable), - handle: AccountTable.handle, - display: AccountTable.display, + author_handle: AccountTable.handle, + author_display: AccountTable.display, }) .from(FeedTable) .leftJoin(AccountTable, eq(FeedTable.author, AccountTable.address)) @@ -53,8 +53,13 @@ export async function Posts(query: Gets.PostsQuery) { } const followingPostsStatement = getDatabase() - .select() + .select({ + ...getTableColumns(FeedTable), + author_handle: AccountTable.handle, + author_display: AccountTable.display, + }) .from(FeedTable) + .leftJoin(AccountTable, eq(FeedTable.author, AccountTable.address)) .where( and( inArray(FeedTable.author, getDatabase() diff --git a/packages/api-main/src/gets/search.ts b/packages/api-main/src/gets/search.ts index f5ffefa4..d077c9b7 100644 --- a/packages/api-main/src/gets/search.ts +++ b/packages/api-main/src/gets/search.ts @@ -37,8 +37,8 @@ export async function Search(query: Gets.SearchQuery) { const matchedPosts = await getDatabase() .select({ ...getTableColumns(FeedTable), - handle: AccountTable.handle, - display: AccountTable.display, + author_handle: AccountTable.handle, + author_display: AccountTable.display, }) .from(FeedTable) .leftJoin(AccountTable, eq(FeedTable.author, AccountTable.address)) diff --git a/packages/api-main/src/types/feed.ts b/packages/api-main/src/types/feed.ts index 6e7e995c..65766995 100644 --- a/packages/api-main/src/types/feed.ts +++ b/packages/api-main/src/types/feed.ts @@ -5,7 +5,10 @@ import { createSelectSchema } from 'drizzle-typebox'; import { FeedTable } from '../../drizzle/schema'; -export type Post = InferSelectModel; +export type Post = InferSelectModel & { + author_handle?: string; + author_display?: string; +}; export const postSchema = createSelectSchema(FeedTable); export interface ReplyWithParent { diff --git a/packages/frontend-main/src/components/popups/FlagPostDialog.vue b/packages/frontend-main/src/components/popups/FlagPostDialog.vue index 465433e6..f86081d4 100644 --- a/packages/frontend-main/src/components/popups/FlagPostDialog.vue +++ b/packages/frontend-main/src/components/popups/FlagPostDialog.vue @@ -12,6 +12,7 @@ import { useFlagPost } from '@/composables/useFlagPost'; import { useTxDialog } from '@/composables/useTxDialog'; import { useConfigStore } from '@/stores/useConfigStore'; import { fractionalDigits } from '@/utility/atomics'; +import { displayAuthor } from '@/utility/text'; import { showBroadcastingToast } from '@/utility/toast'; import PostMessage from '../posts/PostMessage.vue'; @@ -64,7 +65,7 @@ async function handleSubmit() {
- +
diff --git a/packages/frontend-main/src/components/popups/ReplyDialog.vue b/packages/frontend-main/src/components/popups/ReplyDialog.vue index 45fe967f..cc099281 100644 --- a/packages/frontend-main/src/components/popups/ReplyDialog.vue +++ b/packages/frontend-main/src/components/popups/ReplyDialog.vue @@ -17,6 +17,7 @@ import { useCreateReply } from '@/composables/useCreateReply'; import { useTxDialog } from '@/composables/useTxDialog'; import { useConfigStore } from '@/stores/useConfigStore'; import { fractionalDigits } from '@/utility/atomics'; +import { displayAuthor } from '@/utility/text'; import { showBroadcastingToast } from '@/utility/toast'; const POST_HASH_LEN = 64; @@ -66,7 +67,7 @@ function handleInputValidity(value: boolean) {
- +
diff --git a/packages/frontend-main/src/components/posts/PostItem.vue b/packages/frontend-main/src/components/posts/PostItem.vue index b51d17ee..22825b40 100644 --- a/packages/frontend-main/src/components/posts/PostItem.vue +++ b/packages/frontend-main/src/components/posts/PostItem.vue @@ -5,6 +5,7 @@ import { computed, ref } from 'vue'; import { usePost } from '@/composables/usePost'; import { cn } from '@/utility'; +import { displayAuthor } from '@/utility/text'; import PostActions from '../posts/PostActions.vue'; import PrettyTimestamp from '../posts/PrettyTimestamp.vue'; @@ -40,7 +41,7 @@ const usedPost = computed(() => cachedPost.value || props.post);
- +
diff --git a/packages/frontend-main/src/components/ui/search/SearchInput.vue b/packages/frontend-main/src/components/ui/search/SearchInput.vue index 168ad182..a79b82bd 100644 --- a/packages/frontend-main/src/components/ui/search/SearchInput.vue +++ b/packages/frontend-main/src/components/ui/search/SearchInput.vue @@ -5,6 +5,7 @@ import { RouterLink } from 'vue-router'; import PostMessage from '@/components/posts/PostMessage.vue'; import UserAvatarUsername from '@/components/users/UserAvatarUsername.vue'; import { useSearchPosts } from '@/composables/useSearchPosts'; +import { displayAuthor } from '@/utility/text'; import Input from '../input/Input.vue'; @@ -57,7 +58,7 @@ function clearSearch() { @click="clearSearch" > - +
diff --git a/packages/frontend-main/src/types/index.ts b/packages/frontend-main/src/types/index.ts index 88572f70..71c2063a 100644 --- a/packages/frontend-main/src/types/index.ts +++ b/packages/frontend-main/src/types/index.ts @@ -16,3 +16,15 @@ export interface DitherTypes { // PostHash Flag: [string]; }; + +export interface DisplayableAuthor { + author: string; + author_handle?: string; + author_display?: string; +} + +export interface DisplayableUser { + address: string; + handle?: string; + display?: string; +} diff --git a/packages/frontend-main/src/utility/text.ts b/packages/frontend-main/src/utility/text.ts index f9e1205e..e89f9ef6 100644 --- a/packages/frontend-main/src/utility/text.ts +++ b/packages/frontend-main/src/utility/text.ts @@ -1,3 +1,5 @@ +import type { DisplayableAuthor, DisplayableUser } from '@/types'; + import { Decimal } from '@cosmjs/math'; export function shorten(text: string, start = 8, end = 8) { @@ -29,3 +31,13 @@ export function formatCompactAtomics(amountAtomics: string | bigint | null, frac return '0'; return formatCompactNumber(Decimal.fromAtomics(amountAtomics.toString(), fractionalDigits).toFloatApproximation()); } + +export function displayAuthor(item: DisplayableAuthor): string { + const handle = item.author_handle ? `@${item.author_handle}` : undefined; + return item.author_display || handle || item.author; +} + +export function displayUser(item: DisplayableUser): string { + const handle = item.handle ? `@${item.handle}` : undefined; + return item.display || handle || item.address; +} diff --git a/packages/frontend-main/src/views/ManageFollowingView.vue b/packages/frontend-main/src/views/ManageFollowingView.vue index 2bbe57f5..6af70e91 100644 --- a/packages/frontend-main/src/views/ManageFollowingView.vue +++ b/packages/frontend-main/src/views/ManageFollowingView.vue @@ -80,7 +80,7 @@ async function onClickUnfollow(address: string) { class="flex flex-row items-center justify-between p-4 border-b" > - + + +