Add more COMMENTS

This commit is contained in:
Jesse Wierzbinski 2023-09-28 08:19:21 -10:00
parent 1fb4600445
commit 8a8d15810b
8 changed files with 289 additions and 46 deletions

View file

@ -3,41 +3,57 @@ import { APIApplication } from "~types/entities/application";
import { Token } from "./Token"; import { Token } from "./Token";
/** /**
* Applications from clients * Represents an application that can authenticate with the API.
*/
/**
* Represents an application that can authenticate with the API.
*/ */
@Entity({ @Entity({
name: "applications", name: "applications",
}) })
export class Application extends BaseEntity { export class Application extends BaseEntity {
/** The unique identifier for this application. */
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn("uuid")
id!: string; id!: string;
/** The name of this application. */
@Column("varchar") @Column("varchar")
name!: string; name!: string;
/** The website associated with this application, if any. */
@Column("varchar", { @Column("varchar", {
nullable: true, nullable: true,
}) })
website!: string | null; website!: string | null;
/** The VAPID key associated with this application, if any. */
@Column("varchar", { @Column("varchar", {
nullable: true, nullable: true,
}) })
vapid_key!: string | null; vapid_key!: string | null;
/** The client ID associated with this application. */
@Column("varchar") @Column("varchar")
client_id!: string; client_id!: string;
/** The secret associated with this application. */
@Column("varchar") @Column("varchar")
secret!: string; secret!: string;
/** The scopes associated with this application. */
@Column("varchar") @Column("varchar")
scopes = "read"; scopes = "read";
/** The redirect URIs associated with this application. */
@Column("varchar") @Column("varchar")
redirect_uris = "urn:ietf:wg:oauth:2.0:oob"; redirect_uris = "urn:ietf:wg:oauth:2.0:oob";
static async getFromToken(token: string) { /**
* Retrieves the application associated with the given access token.
* @param token The access token to retrieve the application for.
* @returns The application associated with the given access token, or null if no such application exists.
*/
static async getFromToken(token: string): Promise<Application | null> {
const dbToken = await Token.findOne({ const dbToken = await Token.findOne({
where: { where: {
access_token: token, access_token: token,
@ -48,6 +64,10 @@ export class Application extends BaseEntity {
return dbToken?.application || null; return dbToken?.application || null;
} }
/**
* Converts this application to an API application.
* @returns The API application representation of this application.
*/
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
async toAPI(): Promise<APIApplication> { async toAPI(): Promise<APIApplication> {
return { return {

View file

@ -1,22 +1,41 @@
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm"; import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { APIEmoji } from "~types/entities/emoji"; import { APIEmoji } from "~types/entities/emoji";
/**
* Represents an emoji entity in the database.
*/
@Entity({ @Entity({
name: "emojis", name: "emojis",
}) })
export class Emoji extends BaseEntity { export class Emoji extends BaseEntity {
/**
* The unique identifier for the emoji.
*/
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn("uuid")
id!: string; id!: string;
/**
* The shortcode for the emoji.
*/
@Column("varchar") @Column("varchar")
shortcode!: string; shortcode!: string;
/**
* The URL for the emoji.
*/
@Column("varchar") @Column("varchar")
url!: string; url!: string;
/**
* Whether the emoji is visible in the picker.
*/
@Column("boolean") @Column("boolean")
visible_in_picker!: boolean; visible_in_picker!: boolean;
/**
* Converts the emoji to an APIEmoji object.
* @returns The APIEmoji object.
*/
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
async toAPI(): Promise<APIEmoji> { async toAPI(): Promise<APIEmoji> {
return { return {

View file

@ -8,16 +8,28 @@ import {
import { APIInstance } from "~types/entities/instance"; import { APIInstance } from "~types/entities/instance";
import { User } from "./User"; import { User } from "./User";
/**
* Represents an instance in the database.
*/
@Entity({ @Entity({
name: "instances", name: "instances",
}) })
export class Instance extends BaseEntity { export class Instance extends BaseEntity {
/**
* The unique identifier of the instance.
*/
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn("uuid")
id!: string; id!: string;
/**
* The contact account associated with the instance.
*/
@ManyToOne(() => User, user => user.id) @ManyToOne(() => User, user => user.id)
contact_account!: User; contact_account!: User;
/**
* The configuration of the instance.
*/
@Column("jsonb", { @Column("jsonb", {
default: { default: {
media_attachments: { media_attachments: {
@ -43,6 +55,10 @@ export class Instance extends BaseEntity {
}) })
configuration!: APIInstance["configuration"]; configuration!: APIInstance["configuration"];
/**
* Converts the instance to an API instance.
* @returns The API instance.
*/
async toAPI(): Promise<APIInstance> { async toAPI(): Promise<APIInstance> {
return { return {
uri: "", uri: "",

View file

@ -8,18 +8,29 @@ import { APIAccount } from "~types/entities/account";
import { APIEmoji } from "~types/entities/emoji"; import { APIEmoji } from "~types/entities/emoji";
/** /**
* Stores an ActivityPub object as raw JSON-LD data * Represents a raw ActivityPub object in the database.
*/ */
@Entity({ @Entity({
name: "objects", name: "objects",
}) })
export class RawObject extends BaseEntity { export class RawObject extends BaseEntity {
/**
* The unique identifier of the object.
*/
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn("uuid")
id!: string; id!: string;
/**
* The data associated with the object.
*/
@Column("jsonb") @Column("jsonb")
data!: APObject; data!: APObject;
/**
* Retrieves a RawObject instance by its ID.
* @param id The ID of the RawObject to retrieve.
* @returns A Promise that resolves to the RawObject instance, or undefined if not found.
*/
static async getById(id: string) { static async getById(id: string) {
return await RawObject.createQueryBuilder("object") return await RawObject.createQueryBuilder("object")
.where("object.data->>'id' = :id", { .where("object.data->>'id' = :id", {
@ -28,6 +39,10 @@ export class RawObject extends BaseEntity {
.getOne(); .getOne();
} }
/**
* Parses the emojis associated with the object.
* @returns A Promise that resolves to an array of APIEmoji objects.
*/
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
async parseEmojis() { async parseEmojis() {
const emojis = this.data.tag as { const emojis = this.data.tag as {
@ -51,6 +66,10 @@ export class RawObject extends BaseEntity {
})) as APIEmoji[]; })) as APIEmoji[];
} }
/**
* Converts the RawObject instance to an APIStatus object.
* @returns A Promise that resolves to the APIStatus object.
*/
async toAPI(): Promise<APIStatus> { async toAPI(): Promise<APIStatus> {
const mentions = ( const mentions = (
await Promise.all( await Promise.all(
@ -101,6 +120,10 @@ export class RawObject extends BaseEntity {
}; };
} }
/**
* Determines whether the object is filtered based on the note filters in the configuration.
* @returns A Promise that resolves to a boolean indicating whether the object is filtered.
*/
async isObjectFiltered() { async isObjectFiltered() {
const config = getConfig(); const config = getConfig();
@ -130,6 +153,11 @@ export class RawObject extends BaseEntity {
return filter_result.includes(true); return filter_result.includes(true);
} }
/**
* Determines whether a RawObject instance with the given ID exists in the database.
* @param id The ID of the RawObject to check for existence.
* @returns A Promise that resolves to a boolean indicating whether the RawObject exists.
*/
static async exists(id: string) { static async exists(id: string) {
return !!(await RawObject.getById(id)); return !!(await RawObject.getById(id));
} }

View file

@ -17,61 +17,85 @@ import { APIRelationship } from "~types/entities/relationship";
name: "relationships", name: "relationships",
}) })
export class Relationship extends BaseEntity { export class Relationship extends BaseEntity {
/** The unique identifier for the relationship. */
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn("uuid")
id!: string; id!: string;
/** The user who owns the relationship. */
@ManyToOne(() => User, user => user.relationships) @ManyToOne(() => User, user => user.relationships)
owner!: User; owner!: User;
/** The user who is the subject of the relationship. */
@ManyToOne(() => User) @ManyToOne(() => User)
subject!: User; subject!: User;
/** Whether the owner is following the subject. */
@Column("boolean") @Column("boolean")
following!: boolean; following!: boolean;
/** Whether the owner is showing reblogs from the subject. */
@Column("boolean") @Column("boolean")
showing_reblogs!: boolean; showing_reblogs!: boolean;
/** Whether the owner is receiving notifications from the subject. */
@Column("boolean") @Column("boolean")
notifying!: boolean; notifying!: boolean;
/** Whether the owner is followed by the subject. */
@Column("boolean") @Column("boolean")
followed_by!: boolean; followed_by!: boolean;
/** Whether the owner is blocking the subject. */
@Column("boolean") @Column("boolean")
blocking!: boolean; blocking!: boolean;
/** Whether the owner is blocked by the subject. */
@Column("boolean") @Column("boolean")
blocked_by!: boolean; blocked_by!: boolean;
/** Whether the owner is muting the subject. */
@Column("boolean") @Column("boolean")
muting!: boolean; muting!: boolean;
/** Whether the owner is muting notifications from the subject. */
@Column("boolean") @Column("boolean")
muting_notifications!: boolean; muting_notifications!: boolean;
/** Whether the owner has requested to follow the subject. */
@Column("boolean") @Column("boolean")
requested!: boolean; requested!: boolean;
/** Whether the owner is blocking the subject's domain. */
@Column("boolean") @Column("boolean")
domain_blocking!: boolean; domain_blocking!: boolean;
/** Whether the owner has endorsed the subject. */
@Column("boolean") @Column("boolean")
endorsed!: boolean; endorsed!: boolean;
/** The languages the owner has specified for the subject. */
@Column("jsonb") @Column("jsonb")
languages!: string[]; languages!: string[];
/** A note the owner has added for the subject. */
@Column("varchar") @Column("varchar")
note!: string; note!: string;
/** The date the relationship was created. */
@CreateDateColumn() @CreateDateColumn()
created_at!: Date; created_at!: Date;
/** The date the relationship was last updated. */
@UpdateDateColumn() @UpdateDateColumn()
updated_at!: Date; updated_at!: Date;
static async createNew(owner: User, other: User) { /**
* Creates a new relationship between two users.
* @param owner The user who owns the relationship.
* @param other The user who is the subject of the relationship.
* @returns The newly created relationship.
*/
static async createNew(owner: User, other: User): Promise<Relationship> {
const newRela = new Relationship(); const newRela = new Relationship();
newRela.owner = owner; newRela.owner = owner;
newRela.subject = other; newRela.subject = other;
@ -94,6 +118,10 @@ export class Relationship extends BaseEntity {
return newRela; return newRela;
} }
/**
* Converts the relationship to an API-friendly format.
* @returns The API-friendly relationship.
*/
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
async toAPI(): Promise<APIRelationship> { async toAPI(): Promise<APIRelationship> {
return { return {

View file

@ -22,81 +22,137 @@ import { RawActor } from "./RawActor";
const config = getConfig(); const config = getConfig();
/** /**
* Stores ActivityPub notes * Represents a status (i.e. a post)
*/ */
@Entity({ @Entity({
name: "statuses", name: "statuses",
}) })
export class Status extends BaseEntity { export class Status extends BaseEntity {
/**
* The unique identifier for this status.
*/
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn("uuid")
id!: string; id!: string;
/**
* The user account that created this status.
*/
@ManyToOne(() => User, user => user.id) @ManyToOne(() => User, user => user.id)
account!: User; account!: User;
/**
* The date and time when this status was created.
*/
@CreateDateColumn() @CreateDateColumn()
created_at!: Date; created_at!: Date;
/**
* The date and time when this status was last updated.
*/
@UpdateDateColumn() @UpdateDateColumn()
updated_at!: Date; updated_at!: Date;
/**
* The status that this status is a reblog of, if any.
*/
@ManyToOne(() => Status, status => status.id, { @ManyToOne(() => Status, status => status.id, {
nullable: true, nullable: true,
}) })
reblog?: Status; reblog?: Status;
/**
* The raw object associated with this status.
*/
@ManyToOne(() => RawObject, { @ManyToOne(() => RawObject, {
nullable: true, nullable: true,
onDelete: "SET NULL", onDelete: "SET NULL",
}) })
object!: RawObject; object!: RawObject;
/**
* Whether this status is a reblog.
*/
@Column("boolean") @Column("boolean")
isReblog!: boolean; isReblog!: boolean;
/**
* The content of this status.
*/
@Column("varchar", { @Column("varchar", {
default: "", default: "",
}) })
content!: string; content!: string;
/**
* The visibility of this status.
*/
@Column("varchar") @Column("varchar")
visibility!: APIStatus["visibility"]; visibility!: APIStatus["visibility"];
/**
* The raw object that this status is a reply to, if any.
*/
@ManyToOne(() => RawObject, { @ManyToOne(() => RawObject, {
nullable: true, nullable: true,
}) })
in_reply_to_post!: RawObject | null; in_reply_to_post!: RawObject | null;
/**
* The raw actor that this status is a reply to, if any.
*/
@ManyToOne(() => RawActor, { @ManyToOne(() => RawActor, {
nullable: true, nullable: true,
}) })
in_reply_to_account!: RawActor | null; in_reply_to_account!: RawActor | null;
/**
* Whether this status is sensitive.
*/
@Column("boolean") @Column("boolean")
sensitive!: boolean; sensitive!: boolean;
/**
* The spoiler text for this status.
*/
@Column("varchar", { @Column("varchar", {
default: "", default: "",
}) })
spoiler_text!: string; spoiler_text!: string;
/**
* The application associated with this status, if any.
*/
@ManyToOne(() => Application, app => app.id, { @ManyToOne(() => Application, app => app.id, {
nullable: true, nullable: true,
}) })
application!: Application | null; application!: Application | null;
/**
* The emojis associated with this status.
*/
@ManyToMany(() => Emoji, emoji => emoji.id) @ManyToMany(() => Emoji, emoji => emoji.id)
@JoinTable() @JoinTable()
emojis!: Emoji[]; emojis!: Emoji[];
/**
* The activities that have liked this status.
*/
@ManyToMany(() => RawActivity, activity => activity.id) @ManyToMany(() => RawActivity, activity => activity.id)
@JoinTable() @JoinTable()
likes!: RawActivity[]; likes!: RawActivity[];
/**
* The activities that have announced this status.
*/
@ManyToMany(() => RawActivity, activity => activity.id) @ManyToMany(() => RawActivity, activity => activity.id)
@JoinTable() @JoinTable()
announces!: RawActivity[]; announces!: RawActivity[];
/**
* Removes this status from the database.
* @param options The options for removing this status.
* @returns A promise that resolves when the status has been removed.
*/
async remove(options?: RemoveOptions | undefined) { async remove(options?: RemoveOptions | undefined) {
// Delete object // Delete object
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@ -105,6 +161,11 @@ export class Status extends BaseEntity {
return await super.remove(options); return await super.remove(options);
} }
/**
* Creates a new status and saves it to the database.
* @param data The data for the new status.
* @returns A promise that resolves with the new status.
*/
static async createNew(data: { static async createNew(data: {
account: User; account: User;
application: Application | null; application: Application | null;
@ -233,40 +294,11 @@ export class Status extends BaseEntity {
return newStatus; return newStatus;
} }
/**
* Converts this status to an API status.
* @returns A promise that resolves with the API status.
*/
async toAPI(): Promise<APIStatus> { async toAPI(): Promise<APIStatus> {
return await this.object.toAPI(); return await this.object.toAPI();
/* return {
account: await this.account.toAPI(),
application: (await this.application?.toAPI()) ?? null,
bookmarked: false,
created_at: this.created_at.toISOString(),
emojis: await Promise.all(
this.emojis.map(async emoji => await emoji.toAPI())
),
favourited: false,
favourites_count: this.likes.length,
id: this.object.id,
in_reply_to_account_id: null,
in_reply_to_id: null,
language: null,
media_attachments: [],
mentions: [],
muted: false,
pinned: false,
poll: null,
reblog: this.isReblog ? (await this.reblog?.toAPI()) ?? null : null,
reblogged: false,
reblogs_count: this.announces.length,
replies_count: 0,
sensitive: false,
spoiler_text: "",
tags: [],
card: null,
content: this.content,
uri: `${config.http.base_url}/@${this.account.username}/${this.id}`,
url: `${config.http.base_url}/@${this.account.username}/${this.id}`,
visibility: "public",
quote: null,
}; */
} }
} }

View file

@ -9,35 +9,49 @@ import {
import { User } from "./User"; import { User } from "./User";
import { Application } from "./Application"; import { Application } from "./Application";
export enum TokenType { /**
BEARER = "Bearer", * Represents an access token for a user or application.
} */
@Entity({ @Entity({
name: "tokens", name: "tokens",
}) })
export class Token extends BaseEntity { export class Token extends BaseEntity {
/** The unique identifier for the token. */
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn("uuid")
id!: string; id!: string;
/** The type of token. */
@Column("varchar") @Column("varchar")
token_type: TokenType = TokenType.BEARER; token_type: TokenType = TokenType.BEARER;
/** The scope of the token. */
@Column("varchar") @Column("varchar")
scope!: string; scope!: string;
/** The access token string. */
@Column("varchar") @Column("varchar")
access_token!: string; access_token!: string;
/** The authorization code used to obtain the token. */
@Column("varchar") @Column("varchar")
code!: string; code!: string;
/** The date and time the token was created. */
@CreateDateColumn() @CreateDateColumn()
created_at!: Date; created_at!: Date;
/** The user associated with the token. */
@ManyToOne(() => User, user => user.id) @ManyToOne(() => User, user => user.id)
user!: User; user!: User;
/** The application associated with the token. */
@ManyToOne(() => Application, application => application.id) @ManyToOne(() => Application, application => application.id)
application!: Application; application!: Application;
} }
/**
* The type of token.
*/
enum TokenType {
BEARER = "bearer",
}

View file

@ -24,72 +24,129 @@ import { Relationship } from "./Relationship";
const config = getConfig(); const config = getConfig();
/** /**
* Represents a user in the database.
* Stores local and remote users * Stores local and remote users
*/ */
@Entity({ @Entity({
name: "users", name: "users",
}) })
export class User extends BaseEntity { export class User extends BaseEntity {
/**
* The unique identifier for the user.
*/
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn("uuid")
id!: string; id!: string;
/**
* The username for the user.
*/
@Column("varchar", { @Column("varchar", {
unique: true, unique: true,
}) })
username!: string; username!: string;
/**
* The display name for the user.
*/
@Column("varchar") @Column("varchar")
display_name!: string; display_name!: string;
/**
* The password for the user.
*/
@Column("varchar") @Column("varchar")
password!: string; password!: string;
/**
* The email address for the user.
*/
@Column("varchar", { @Column("varchar", {
unique: true, unique: true,
}) })
email!: string; email!: string;
/**
* The note for the user.
*/
@Column("varchar", { @Column("varchar", {
default: "", default: "",
}) })
note!: string; note!: string;
/**
* Whether the user is an admin or not.
*/
@Column("boolean", { @Column("boolean", {
default: false, default: false,
}) })
is_admin!: boolean; is_admin!: boolean;
/**
* The source for the user.
*/
@Column("jsonb") @Column("jsonb")
source!: APISource; source!: APISource;
/**
* The avatar for the user.
*/
@Column("varchar") @Column("varchar")
avatar!: string; avatar!: string;
/**
* The header for the user.
*/
@Column("varchar") @Column("varchar")
header!: string; header!: string;
/**
* The date the user was created.
*/
@CreateDateColumn() @CreateDateColumn()
created_at!: Date; created_at!: Date;
/**
* The date the user was last updated.
*/
@UpdateDateColumn() @UpdateDateColumn()
updated_at!: Date; updated_at!: Date;
/**
* The public key for the user.
*/
@Column("varchar") @Column("varchar")
public_key!: string; public_key!: string;
/**
* The private key for the user.
*/
@Column("varchar") @Column("varchar")
private_key!: string; private_key!: string;
/**
* The relationships for the user.
*/
@OneToMany(() => Relationship, relationship => relationship.owner) @OneToMany(() => Relationship, relationship => relationship.owner)
relationships!: Relationship[]; relationships!: Relationship[];
/**
* The actor for the user.
*/
@ManyToOne(() => RawActor, actor => actor.id) @ManyToOne(() => RawActor, actor => actor.id)
actor!: RawActor; actor!: RawActor;
/**
* The pinned notes for the user.
*/
@ManyToMany(() => RawObject, object => object.id) @ManyToMany(() => RawObject, object => object.id)
@JoinTable() @JoinTable()
pinned_notes!: RawObject[]; pinned_notes!: RawObject[];
/**
* Gets a user by actor ID.
* @param id The actor ID to search for.
* @returns The user with the given actor ID.
*/
static async getByActorId(id: string) { static async getByActorId(id: string) {
return await User.createQueryBuilder("user") return await User.createQueryBuilder("user")
// Objects is a many-to-many relationship // Objects is a many-to-many relationship
@ -103,6 +160,11 @@ export class User extends BaseEntity {
.getOne(); .getOne();
} }
/**
* Creates a new user.
* @param data The data for the new user.
* @returns The newly created user.
*/
static async createNew(data: { static async createNew(data: {
username: string; username: string;
display_name?: string; display_name?: string;
@ -140,6 +202,11 @@ export class User extends BaseEntity {
return user; return user;
} }
/**
* Retrieves a user from a token.
* @param access_token The access token to retrieve the user from.
* @returns The user associated with the given access token.
*/
static async retrieveFromToken(access_token: string) { static async retrieveFromToken(access_token: string) {
if (!access_token) return null; if (!access_token) return null;
@ -160,6 +227,11 @@ export class User extends BaseEntity {
return token.user; return token.user;
} }
/**
* Gets the relationship to another user.
* @param other The other user to get the relationship to.
* @returns The relationship to the other user.
*/
async getRelationshipToOtherUser(other: User) { async getRelationshipToOtherUser(other: User) {
const relationship = await Relationship.findOne({ const relationship = await Relationship.findOne({
where: { where: {
@ -176,6 +248,11 @@ export class User extends BaseEntity {
return relationship; return relationship;
} }
/**
* Removes the user.
* @param options The options for removing the user.
* @returns The removed user.
*/
async remove(options?: RemoveOptions | undefined) { async remove(options?: RemoveOptions | undefined) {
// Clean up tokens // Clean up tokens
const tokens = await Token.findBy({ const tokens = await Token.findBy({
@ -210,6 +287,10 @@ export class User extends BaseEntity {
return await super.remove(options); return await super.remove(options);
} }
/**
* Gets the relationships for the user.
* @returns The relationships for the user.
*/
async getRelationships() { async getRelationships() {
const relationships = await Relationship.find({ const relationships = await Relationship.find({
where: { where: {
@ -223,12 +304,14 @@ export class User extends BaseEntity {
return relationships; return relationships;
} }
/**
* Updates the actor for the user.
* @returns The updated actor.
*/
async updateActor() { async updateActor() {
const config = getConfig();
// Check if actor exists // Check if actor exists
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
// Check is actor already exists
const actorExists = await RawActor.getByActorId( const actorExists = await RawActor.getByActorId(
`${config.http.base_url}/@${this.username}` `${config.http.base_url}/@${this.username}`
); );
@ -278,6 +361,9 @@ export class User extends BaseEntity {
return actor; return actor;
} }
/**
* Generates keys for the user.
*/
async generateKeys(): Promise<void> { async generateKeys(): Promise<void> {
// openssl genrsa -out private.pem 2048 // openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -outform PEM -pubout -out public.pem // openssl rsa -in private.pem -outform PEM -pubout -out public.pem