Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature: Following #78

Merged
merged 1 commit into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Command from "#utils/Command.js";
import { ButtonStyles, Client, ComponentTypes, InteractionTypes } from "oceanic.js";
import "./express-server.js";
import database from "#utils/mongodb-database.js";
import interactWithPost from "#utils/interact-with-post.js";
import interactWithPost from "#utils/interact-with-bluesky.js";
import { existsSync, mkdirSync, rmSync } from "fs";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
Expand Down Expand Up @@ -149,14 +149,14 @@ client.on("messageCreate", async (message) => {
for (const sessionData of sessionDataList) {

// Check if the user added a Bluesky post.
const matchingRegex = /https?:\/\/bsky.app\/profile\/(?<postCreatorHandle>\S+)\/post\/(?<rkey>\S+)/gm;
const matchingRegex = /https?:\/\/bsky.app\/profile\/(?<targetHandle>\S+)\/post\/(?<rkey>\S+)/gm;
const matches = [...message.content.matchAll(matchingRegex)];
for (const match of matches) {

if (match.groups) {

const {rkey, postCreatorHandle} = match.groups;
await interactWithPost({rkey, postCreatorHandle, actorDID: sessionData.sub, guildID}, "repost");
const {rkey, targetHandle} = match.groups;
await interactWithPost({rkey, targetHandle, actorDID: sessionData.sub, guildID}, "repost");
await message.createReaction("♻️");

}
Expand Down Expand Up @@ -187,14 +187,14 @@ client.on("messageReactionRemove", async (uncachedMessage, reactor, reaction) =>
for (const sessionData of sessionDataList) {

// Check if the user added a Bluesky post.
const matchingRegex = /https?:\/\/bsky.app\/profile\/(?<postCreatorHandle>\S+)\/post\/(?<rkey>\S+)/gm;
const matchingRegex = /https?:\/\/bsky.app\/profile\/(?<targetHandle>\S+)\/post\/(?<rkey>\S+)/gm;
const matches = [...message.content.matchAll(matchingRegex)];
for (const match of matches) {

if (match.groups) {

const {rkey, postCreatorHandle} = match.groups;
await interactWithPost({rkey, postCreatorHandle, actorDID: sessionData.sub, guildID}, "deleteRepost");
const {rkey, targetHandle} = match.groups;
await interactWithPost({rkey, targetHandle, actorDID: sessionData.sub, guildID}, "deleteRepost");

}

Expand Down
23 changes: 23 additions & 0 deletions src/commands/follow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Command from "#utils/Command.js"
import { ApplicationCommandOptionTypes } from "oceanic.js";
import interactWithBlueskyNow from "#utils/interact-with-bluesky-now.js";

const unlikeNowCommand = new Command({
name: "follow",
description: "Follow an account on Bluesky.",
options: [
{
type: ApplicationCommandOptionTypes.STRING,
name: "handle",
description: "What's the name of the account?",
required: true
}
],
async action(interaction) {

return await interactWithBlueskyNow(interaction, "follow", "follow");

}
});

export default unlikeNowCommand;
4 changes: 2 additions & 2 deletions src/commands/like/now.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Command from "#utils/Command.js"
import { ApplicationCommandOptionTypes } from "oceanic.js";
import interactWithPostNow from "#utils/interact-with-post-now.js";
import interactWithBlueskyNow from "#utils/interact-with-bluesky-now.js";

const likeNowSubCommand = new Command({
name: "now",
Expand All @@ -15,7 +15,7 @@ const likeNowSubCommand = new Command({
],
async action(interaction) {

return await interactWithPostNow(interaction, "like/now", "like");
return await interactWithBlueskyNow(interaction, "like/now", "like");

}
});
Expand Down
4 changes: 2 additions & 2 deletions src/commands/repost/now.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Command from "#utils/Command.js"
import { ApplicationCommandOptionTypes } from "oceanic.js";
import interactWithPostNow from "#utils/interact-with-post-now.js";
import interactWithBlueskyNow from "#utils/interact-with-bluesky-now.js";

const repostNowSubCommand = new Command({
name: "now",
Expand All @@ -15,7 +15,7 @@ const repostNowSubCommand = new Command({
],
async action(interaction) {

return await interactWithPostNow(interaction, "repost/now", "repost");
return await interactWithBlueskyNow(interaction, "repost/now", "repost");

}
});
Expand Down
4 changes: 2 additions & 2 deletions src/commands/unlike.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Command from "#utils/Command.js"
import { ApplicationCommandOptionTypes } from "oceanic.js";
import interactWithPostNow from "#utils/interact-with-post-now.js";
import interactWithBlueskyNow from "#utils/interact-with-bluesky-now.js";

const unlikeNowCommand = new Command({
name: "unlike",
Expand All @@ -15,7 +15,7 @@ const unlikeNowCommand = new Command({
],
async action(interaction) {

return await interactWithPostNow(interaction, "unlike", "deleteLike");
return await interactWithBlueskyNow(interaction, "unlike", "deleteLike");

}
});
Expand Down
4 changes: 2 additions & 2 deletions src/commands/unrepost.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Command from "#utils/Command.js"
import { ApplicationCommandOptionTypes } from "oceanic.js";
import interactWithPostNow from "#utils/interact-with-post-now.js";
import interactWithBlueskyNow from "#utils/interact-with-bluesky-now.js";

const unrepostNowCommand = new Command({
name: "unrepost",
Expand All @@ -15,7 +15,7 @@ const unrepostNowCommand = new Command({
],
async action(interaction) {

return await interactWithPostNow(interaction, "unrepost", "deleteRepost");
return await interactWithBlueskyNow(interaction, "unrepost", "deleteRepost");

}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ButtonStyles, CommandInteraction, ComponentInteraction, ComponentTypes, ModalSubmitInteraction, StringSelectMenu } from "oceanic.js";
import interactWithPost from "./interact-with-post.js";
import interactWithBluesky from "./interact-with-bluesky.js";
import database from "./mongodb-database.js";
import createAccountSelector from "./create-account-selector.js";
import getGuildIDFromInteraction from "./get-guild-id-from-interaction.js";
Expand All @@ -11,21 +11,22 @@ import { authenticator } from "otplib";
import IncorrectDecryptionKeyError from "./errors/IncorrectDecryptionKeyError.js";
import MFAIncorrectCodeError from "./errors/MFAIncorrectCodeError.js";

async function interactWithPostNow(interaction: CommandInteraction | ComponentInteraction | ModalSubmitInteraction, customIDPrefix: string, action: "deleteRepost" | "like" | "deleteLike" | "repost") {
async function interactWithBlueskyNow(interaction: CommandInteraction | ComponentInteraction | ModalSubmitInteraction, customIDPrefix: string, action: "follow" | "deleteRepost" | "like" | "deleteLike" | "repost") {

const guildID = getGuildIDFromInteraction(interaction);

async function confirmAction(options: {interaction: ModalSubmitInteraction | ComponentInteraction, guildID: string, actorDID: string, decryptionKey?: string}) {

// Repost the post.
await interactWithPost(options, action);
await interactWithBluesky(options, action);

// Let the user know that we liked the post.
const responses = {
repost: "♻️",
like: "💖",
deleteLike: "💔",
deleteRepost: "🗑️"
deleteRepost: "🗑️",
follow: "➕"
};
await interaction.editOriginal({
content: responses[action],
Expand All @@ -40,7 +41,8 @@ async function interactWithPostNow(interaction: CommandInteraction | ComponentIn
// Ask the user which user they want to post as.
await interaction.defer(64);
const postLink = interaction.data.options.getString("link");
if (!postLink) throw new Error();
const handle = interaction.data.options.getString("handle");
if (!postLink && !handle) throw new Error();

const defaultSession = await database.collection("sessions").findOne({guildID, isDefault: true});
const accountSelector = await createAccountSelector(guildID, customIDPrefix, (did) => did === defaultSession?.sub);
Expand All @@ -50,7 +52,7 @@ async function interactWithPostNow(interaction: CommandInteraction | ComponentIn
embeds: [
{
footer: {
text: postLink.split("?")[0]
text: handle! ?? postLink!.split("?")[0]
}
}
],
Expand Down Expand Up @@ -186,4 +188,4 @@ async function interactWithPostNow(interaction: CommandInteraction | ComponentIn

}

export default interactWithPostNow;
export default interactWithBlueskyNow;
78 changes: 78 additions & 0 deletions src/utils/interact-with-bluesky.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ComponentInteraction, ModalSubmitInteraction } from "oceanic.js";
import blueskyClient from "./bluesky-client.js";
import { Agent } from "@atproto/api";
import { isThreadViewPost } from "@atproto/api/dist/client/types/app/bsky/feed/defs.js";

async function interactWithBluesky(source: {interaction?: ModalSubmitInteraction | ComponentInteraction, rkey?: string, targetHandle?: string, actorDID?: string, guildID: string, decryptionKey?: string}, action: "follow" | "deletePost" | "deleteLike" | "like" | "deleteRepost" | "repost") {

let {interaction, rkey, targetHandle, actorDID} = source;

if (interaction && !targetHandle) {

// Get the rkey of the post.
const originalMessage = await interaction.getOriginal();
const originalEmbed = originalMessage.embeds[0];
const value = originalEmbed?.footer?.text;

if (action === "follow") {

targetHandle = value;

} else {

const postSplit = value?.split("/");
rkey = postSplit?.pop();
if (!postSplit || !rkey) throw new Error();

targetHandle = postSplit[4];

}

}

if (!targetHandle || !actorDID || (action !== "follow" && !rkey)) throw new Error();

// Get the CID of the post if necessary.
const session = await blueskyClient.restore(actorDID, "auto", {guildID: source.guildID, decryptionKey: source.decryptionKey});
const agent = new Agent(session);
const targetDID = targetHandle.includes("did:") ? targetHandle : await blueskyClient.handleResolver.resolve(targetHandle);
let cid;
let uri;
if (!targetDID) throw new Error();
if (action !== "follow") {

if (!rkey) throw new Error();

const recordResponse = await agent.com.atproto.repo.getRecord({
collection: "app.bsky.feed.post",
repo: targetDID,
rkey
});

cid = recordResponse.data.cid;

if (!cid) throw new Error();

// Get the URI we need.
uri = `at://${targetDID}/app.bsky.feed.post/${rkey}`;
if (action === "deleteLike" || action === "deleteRepost") {

const response = await agent.getPostThread({uri});
if (isThreadViewPost(response.data.thread)) {

const possibleURI = response.data.thread.post.viewer?.[action === "deleteLike" ? "like" : "repost"];
if (!possibleURI) return;
uri = possibleURI;

}

}

}

// Interact with Bluesky.
await agent[action](uri ?? targetDID, cid as string);

}

export default interactWithBluesky;
60 changes: 0 additions & 60 deletions src/utils/interact-with-post.ts

This file was deleted.

Loading