const z = @import( "std" ); const db = @import( "db.zig" ); const zap = @import( "zap" ); const req = @import( "req.zig" ); const net = @import( "net-util.zig" ); const model = @import( "model.zig" ); const u = struct { usingnamespace @import( "util.zig" ); usingnamespace @import( "userdefs.zig" ); usingnamespace @import( "user.zig" ); }; const alloc = u.alloc; const ArenaAllocator = z.heap.ArenaAllocator; const ArrayList = z.ArrayList; const UserEntry = u.UserEntry; const ErrorRes = u.ErrorRes; const OkRes = net.OkResponse; const bufprint = z.fmt.bufPrint; const memeql = z.mem.eql; pub const routes = .{ .@"settings" = settings, .@"get-notes" = getNotes, .@"getalldata" = getAllData, .@"delete-note" = deleteNote, .@"delete-notes" = deleteNotes, .@"update-settings" = updateSettingsRoute }; pub const ChatEntry = struct { id: []const u8, name: []const u8, }; pub const ChatFiles = struct { files: ?[]ChatEntry, }; const PrefsDbEntry = struct { uuid: []const u8, nickname: []const u8, prompt_data: []const u8, site_prefs: []const u8, chat_files: []const u8 }; const PrefsPromptData = struct { system: ?[]const u8 = "" }; const PrefsSiteData = struct { font: ?[]const u8 = "Terminal", model: ?[]const u8 = null }; pub const Prefs = struct { uuid: []const u8, nickname: []const u8, prompt_data: PrefsPromptData = .{}, site_prefs: PrefsSiteData = .{}, chat_files: ?ChatFiles = null, db_entry: ?db.Row(PrefsDbEntry) = null, pub const @"getty.sb" = struct { pub const attributes = .{ .db_entry = .{ .skip = true }, }; }; }; const PrefsResponse = struct { uuid: []const u8, nickname: []const u8, prompt_data: PrefsPromptData = .{}, site_prefs: PrefsSiteData = .{}, chat_files: ?ChatFiles = null, plan: u.SubData, pub fn fromPrefs( prefs: *const Prefs, user: *const u.UserEntry ) PrefsResponse { return PrefsResponse { .uuid = prefs.uuid, .nickname = prefs.nickname, .prompt_data = prefs.prompt_data, .site_prefs = prefs.site_prefs, .chat_files = prefs.chat_files, .plan = user.subscription_data }; } }; const NoteEntry = struct { content: []const u8, id: []const u8 }; const NoteTargetReq = struct { noteId: []const u8, pub const @"getty.db" = u.@"json.ignore.unknown"; }; /// creates directories for notes and chats pub fn createDirectories() void { var pathbuf: [256]u8 = undefined; const notes_path = bufprint( &pathbuf, "../data/notes", .{} ) catch unreachable; var notes_dir = z.fs.cwd().openDir( notes_path, .{} ) catch r: { z.fs.cwd().makeDir( notes_path ) catch {}; break :r null; }; if( notes_dir ) |*c| c.close(); const chats_path = bufprint( &pathbuf, "../data/chats", .{} ) catch unreachable; var chats_dir = z.fs.cwd().openDir( chats_path, .{} ) catch r: { z.fs.cwd().makeDir( chats_path ) catch {}; break :r null; }; if( chats_dir ) |*c| c.close(); } pub const UpdateReqParams = struct { pub const @"getty.db" = u.@"json.ignore.unknown"; prefs: struct { prompt_data: ?PrefsPromptData = null, site_prefs: ?PrefsSiteData = null, nickname: ?[]const u8 = null, pub const @"getty.db" = u.@"json.ignore.unknown"; } = .{}, pub fn isValid( self: @This(), user: *const UserEntry ) !bool { if( self.prefs.prompt_data ) |p| { if( p.system ) |s| { if( !u.Validator.isValidStrSystem( s ) ) return error.InvalidSystemPrompt; } } if( self.prefs.nickname ) |name| { if( !u.Validator.isValidStrNickname( name ) ) return error.InvalidNickname; } if( self.prefs.site_prefs ) |p| { if( p.font ) |f| if( !u.Validator.isValidStrFont( f ) ) return error.InvalidSiteFont; if( p.model ) |m| { if( !u.Validator.isValidStrFont( m ) ) return error.InvalidModel; // todo later: check if model exists within modelmap const models = model.loadModels() catch return error.ModelMapError; for( models.v ) |mod| { if( memeql( u8, m, mod.name ) ) { return model.canBeUsedByUser( mod, user ); } } return error.InvalidModel; } } return true; } }; fn dbToPrefs( row: *db.Row(PrefsDbEntry) ) !Prefs { const a = row.alloc.allocator(); const prompt = try u.jsonParseAlloc( PrefsPromptData, row.v.prompt_data, a ); const site = try u.jsonParseAlloc( PrefsSiteData, row.v.site_prefs, a ); const chat = try u.jsonParseAlloc( ChatFiles, row.v.chat_files, a ); return Prefs{ .uuid = row.v.uuid, .nickname = row.v.nickname, .prompt_data = prompt.v, .site_prefs = site.v, .chat_files = chat.v, .db_entry = row.* }; } fn prefsToDb( prefs: Prefs, arena: *ArenaAllocator ) !PrefsDbEntry { const a = arena.allocator(); const prompt = try u.jsonStringifyAlloc( prefs.prompt_data, a ); const site = try u.jsonStringifyAlloc( prefs.site_prefs, a ); const chat = try u.jsonStringifyAlloc( prefs.chat_files, a ); return PrefsDbEntry { .uuid = prefs.uuid, .nickname = prefs.nickname, .prompt_data = prompt, .site_prefs = site, .chat_files = chat }; } pub fn getSettings( uuid: []const u8 ) !Prefs { const query = "SELECT * FROM user_data WHERE uuid = ?"; var res = try u.dbi.oneAlloc( PrefsDbEntry, query, .{ .uuid = uuid } ); return dbToPrefs( &res ); } fn getSettingsDbEntry( uuid: []const u8 ) !db.Row(PrefsDbEntry) { const query = "SELECT * FROM user_data WHERE uuid = ?"; const res = try u.dbi.oneAlloc( PrefsDbEntry, query, .{ .uuid = uuid } ); return res; } fn createNewSettings( name: []const u8, uuid: []const u8 ) !Prefs { var arena = z.heap.ArenaAllocator.init( alloc ); const a = arena.allocator(); defer arena.deinit(); const models = model.loadModels() catch null; var modelname: []const u8 = "qwen2.5:1.5b"; if( models ) |mods| { for( mods.v ) |mod| { if( mod.free == 1 ) { modelname = a.dupe( u8, mod.name ) catch ""; break; } } } const prefs = Prefs { .uuid = uuid, .nickname = name, .site_prefs = .{ .font = "Terminal", .model = modelname }, .prompt_data = .{ .system = "" }, .chat_files = .{ .files = &[_]ChatEntry{} } }; const converted = try prefsToDb( prefs, &arena ); const query = "INSERT INTO user_data (uuid, nickname, site_prefs, prompt_data, chat_files) VALUES (?, ?, ?, ?, ?)"; try u.dbi.put( query, .{ .uuid = converted.uuid, .nickname = converted.nickname, .site_prefs = converted.site_prefs, .prompt_data = converted.prompt_data, .chat_files = converted.chat_files } ); // return the actual entry - if it fails then we panic return getSettings( uuid ); } fn updateSettingsFromParams( oldprefs: *Prefs, newprefs: *UpdateReqParams, user: *const UserEntry ) !void { _ = try newprefs.isValid( user ); if( newprefs.prefs.site_prefs ) |site| { if( site.model ) |m| oldprefs.site_prefs.model = m; if( site.font ) |f| oldprefs.site_prefs.font = f; } if( newprefs.prefs.nickname ) |nickname| { oldprefs.nickname = nickname; } if( newprefs.prefs.prompt_data ) |prompt_data| { oldprefs.prompt_data = prompt_data; } var arena = z.heap.ArenaAllocator.init( alloc ); defer arena.deinit(); const converted = prefsToDb( oldprefs.*, &arena ) catch return error.JsonParseError; const query = "UPDATE user_data SET site_prefs = ?, nickname = ?, prompt_data = ? WHERE uuid = ?"; return u.dbi.put( query, .{ .site_prefs = converted.site_prefs, .nickname = converted.nickname, .prompt_data = converted.prompt_data, .uuid = converted.uuid, } ); } pub fn hasChat( prefs: *Prefs, id: []const u8 ) bool { if( prefs.chat_files == null ) return false; if( prefs.chat_files.?.files == null ) return false; const files = prefs.chat_files.?.files.?; for( files ) |file| { if( memeql( u8, file.id, id ) ) return true; } return false; } pub fn updateSettings( newprefs: *Prefs ) !void { try u.checkDbOnThread(); var arena = ArenaAllocator.init( alloc ); const a = arena.allocator(); defer arena.deinit(); const prefs = u.jsonStringifyAlloc( newprefs.site_prefs, a ) catch return error.InvalidSitePrefs; const prompt_data = u.jsonStringifyAlloc( newprefs.prompt_data, a ) catch return error.InvalidPromptData; const chat_files = u.jsonStringifyAlloc( newprefs.chat_files, a ) catch return error.InvalidChatFiles; const nickname = newprefs.nickname; const uuid = newprefs.uuid; const query = "UPDATE user_data SET site_prefs = ?, nickname = ?, prompt_data = ?, chat_files = ? WHERE uuid = ?"; return u.dbi.put( query, .{ .site_prefs = prefs, .nickname = nickname, .prompt_data = prompt_data, .chat_files = chat_files, .uuid = uuid, } ); } ///route @/settings fn settings( r: zap.Request ) void { u.checkDbOnThread() catch { return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ); }; if( net.handleInvalidPostReq( r ) ) return; var decoded = u.apiTokenFromPostReq( r ) catch return; defer decoded.deinit(); var user = u.getEntry( decoded.claims.uuid ) catch |e| { switch( e ) { error.QueryError => return net.sendJson( r, .not_found, ErrorRes{ .msg = "user not found" } ), else => return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal_server_error" } ) } }; defer user.db_entry.?.deinit(); var entry = getSettings( decoded.claims.uuid ) catch |e| { switch( e ) { error.QueryError => return net.sendJson( r, .not_found, ErrorRes{ .status = "nodata", .msg = "no user data found. is this account not activated?" } ), else => { z.debug.print( "error getting settings: {s} {any} {any}\n", .{ decoded.claims.uuid, e, @errorReturnTrace() } ); return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ); } } }; defer entry.db_entry.?.deinit(); return net.sendJson( r, .ok, OkRes( .{ .userprefs = PrefsResponse.fromPrefs( &entry, &user ) } ) ); } ///route @/update-settings fn updateSettingsRoute( r: zap.Request ) void { u.checkDbOnThread() catch { return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ); }; if( net.handleInvalidPostReq( r ) ) return; var decoded = u.tokenFromPostReq( r ) catch return; defer decoded.deinit(); var user = u.getEntry( decoded.claims.uuid ) catch { return net.sendJson( r, .not_found, ErrorRes{ .msg = "user not found" } ); }; defer user.db_entry.?.deinit(); var entry = getSettings( user.uuid ) catch |e| r: { switch( e ) { error.QueryError => break :r createNewSettings( "user", user.uuid ) catch { z.debug.print( "error querying settings: {s} {any}", .{ user.uuid, @errorReturnTrace() } ); return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ); }, else => return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ) } }; defer { if( entry.db_entry ) |e| e.deinit(); } var params = u.jsonParse( UpdateReqParams, r.body.? ) catch { return net.sendJson( r, .bad_request, ErrorRes{ .msg = "invalid request body" } ); }; defer params.deinit(); updateSettingsFromParams( &entry, ¶ms.v, &user ) catch |e| { switch( e ) { error.JsonParseError => return net.sendJson( r, .bad_request, ErrorRes{ .msg = "invalid request body" } ), error.InvalidNickname => return net.sendJson( r, .bad_request, ErrorRes{ .msg = "invalid field: nickname" } ), error.InvalidSiteFont => return net.sendJson( r, .bad_request, ErrorRes{ .msg = "invalid field: site_font" } ), error.InvalidSystemPrompt => return net.sendJson( r, .bad_request, ErrorRes{ .msg = "invalid field: system_prompt" } ), error.InvalidModel => return net.sendJson( r, .bad_request, ErrorRes{ .msg = "invalid field: model" } ), else => { z.debug.print( "error updating settings: {s} {any}", .{ entry.uuid, @errorReturnTrace() } ); return net.sendJson( r, .internal_server_error, ErrorRes{.msg = "internal server error" } ); } } }; return net.sendJson( r, .ok, OkRes( .{ .msg = "settings updated successfully", .userprefs = PrefsResponse.fromPrefs( &entry, &user ) } ) ); } fn readNotesFile( uuid: []const u8 ) !u.JsonResponse([]NoteEntry) { var buf: [256]u8 = undefined; const path = z.fmt.bufPrint( &buf, "../data/notes/{s}-notes.json", .{ uuid } ) catch ""; const contents = u.readFileCrypto( path ) catch { return error.IOError; }; defer alloc.free( contents ); const json = u.jsonParse( []NoteEntry, contents ) catch { return error.ParseError; }; return json; } fn saveNotesFile( uuid: []const u8, notes: []NoteEntry ) !void { var buf: [256]u8 = undefined; const path = z.fmt.bufPrint( &buf, "../data/notes/{s}-notes.json", .{ uuid } ) catch ""; const contents = u.jsonStringify( notes ) catch { return error.EncodeError; }; defer alloc.free( contents ); const stdout = u.writeFileCrypto( path, contents ) catch |e| { z.debug.print( "Failed to write file: {any}", .{ e } ); return error.IOError; }; alloc.free( stdout ); } ///route @/get-notes fn getNotes( r: zap.Request ) void { u.checkDbOnThread() catch { return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ); }; if( net.handleInvalidPostReq( r ) ) return; const uuid = u.uuidFromApiOrAuthToken( r ) catch return; defer alloc.free( uuid ); const notes = readNotesFile( uuid ) catch |e| { switch( e ) { error.IOError => return net.sendJson( r, .ok, OkRes( .{ .notes = [_]NoteEntry{} } ) ), error.ParseError => return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "error parsing notes file" } ) } }; defer notes.deinit(); return net.sendJson( r, .ok, OkRes( .{ .notes = notes.v } ) ); } ///route @/delete-note fn deleteNote( r: zap.Request ) void { u.checkDbOnThread() catch { return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ); }; if( net.handleInvalidPostReq( r ) ) return; const uuid = u.uuidFromApiOrAuthToken( r ) catch return; defer alloc.free( uuid ); const params = u.jsonParse( NoteTargetReq, r.body.? ) catch { z.debug.print( "{s}\n", .{ r.body.? } ); return net.sendJson( r, .bad_request, ErrorRes{ .msg = "invalid request format" } ); }; defer params.deinit(); const notes = readNotesFile( uuid ) catch |e| { switch( e ) { error.IOError => return net.sendJson( r, .not_found, ErrorRes{ .msg = "note not found" } ), error.ParseError => return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "error parsing notes file" } ) } }; defer notes.deinit(); var list = ArrayList( NoteEntry ).init( alloc ); defer list.deinit(); for( notes.v ) |note| { if( !memeql( u8, note.id, params.v.noteId ) ) list.append( note ) catch {}; } saveNotesFile( uuid, list.items ) catch { return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "error saving notes file" } ); }; return net.sendJson( r, .ok, OkRes( .{ .msg = "note deleted" } ) ); } ///route @/delete-notes fn deleteNotes( r: zap.Request ) void { u.checkDbOnThread() catch { return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ); }; if( net.handleInvalidPostReq( r ) ) return; const uuid = u.uuidFromApiOrAuthToken( r ) catch return; defer alloc.free( uuid ); var buf: [256]u8 = undefined; const path = z.fmt.bufPrint( &buf, "../data/notes/{s}-notes.json", .{ uuid } ) catch ""; z.fs.cwd().deleteFile( path ) catch |e| { switch ( e ) { error.FileNotFound => return net.sendJson( r, .not_found, ErrorRes{ .msg = "no notes found" } ), else => return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ), } }; net.sendJson( r, .ok, OkRes( .{ .msg = "notes deleted" } ) ); } fn aggregateUserData( uuid: []const u8 ) ![]const u8 { const userdata = try getSettings( uuid ); defer userdata.db_entry.?.deinit(); const json = try u.jsonStringify( userdata ); defer alloc.free( json ); const wrapped_json = try u.jsonStringify( json ); defer alloc.free( wrapped_json ); const proc = try z.process.Child.run( .{ .allocator = alloc, .argv = &.{ "node", "../data-aggregate.cjs", wrapped_json } } ); defer alloc.free( proc.stdout ); defer alloc.free( proc.stderr ); while( true ) { if( proc.term == .Exited ) break; z.Thread.yield() catch {}; } if( proc.stderr.len > 0 ) z.debug.print( "{s}\n", .{ proc.stderr } ); if( proc.stdout.len > 0 and proc.stdout[0] == '0' ) { z.debug.print( "{s}\n", .{ proc.stdout } ); return error.ReadError; } const copy = alloc.dupe( u8, proc.stdout ); return copy; } ///route @/getalldata fn getAllData( r: zap.Request ) void { if( net.handleInvalidPostReq( r ) ) return; u.checkDbOnThread() catch return; var token = u.tokenFromPostReq( r ) catch return; defer token.deinit(); const user = u.getEntry( token.claims.uuid ) catch { return net.sendJson( r, .unauthorized, ErrorRes{ .msg = "user not found" } ); }; defer user.db_entry.?.deinit(); const data = aggregateUserData( user.uuid ) catch |e| { z.debug.print( "error aggregating data: {any} {any}", .{ e, @errorReturnTrace() } ); return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ); }; defer alloc.free( data ); var path_buf: [256]u8 = undefined; const path = z.fmt.bufPrint( &path_buf, "/tmp/{s}.zip", .{ user.uuid } ) catch { return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ); }; u.writeFile( path, data ) catch |e| { z.debug.print( "error writing file: {any} {any}", .{ e, @errorReturnTrace() } ); return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ); }; r.setHeader( "Content-Type", "application/zip" ) catch {}; r.setHeader( "Content-Disposition", "attachment; filename=userdata.zip" ) catch {}; r.sendFile( path ) catch |e| { z.debug.print( "error sending file: {any} {any}", .{ e, @errorReturnTrace() } ); }; }