-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow quick adding card via Telegram messenger input
- Loading branch information
Showing
13 changed files
with
332 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { EnvSafe } from "../../env/env-schema.ts"; | ||
import { getDatabase } from "../get-database.ts"; | ||
import { DatabaseException } from "../database-exception.ts"; | ||
import { | ||
deckSchema, | ||
DeckWithoutCardsDbType, | ||
} from "./decks-with-cards-schema.ts"; | ||
import { z } from "zod"; | ||
|
||
export const getDecksCreatedByMe = async ( | ||
env: EnvSafe, | ||
userId: number, | ||
): Promise<DeckWithoutCardsDbType[]> => { | ||
const db = getDatabase(env); | ||
|
||
const result = await db.rpc("get_active_decks_by_author", { | ||
user_id: userId, | ||
}); | ||
|
||
if (result.error) { | ||
throw new DatabaseException(result.error); | ||
} | ||
|
||
return z.array(deckSchema).parse(result.data); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { EnvSafe } from "../../env/env-schema.ts"; | ||
import { getDatabase } from "../get-database.ts"; | ||
import { DatabaseException } from "../database-exception.ts"; | ||
|
||
export type ServerBotState = | ||
| null | ||
| { type: "cardAdded"; cardFront: string; cardBack: string } | ||
| { | ||
type: "deckSelected"; | ||
cardFront: string; | ||
cardBack: string; | ||
deckId: number; | ||
editingField?: "cardFront" | "cardBack"; | ||
}; | ||
|
||
export const userSetServerBotState = async ( | ||
envSafe: EnvSafe, | ||
userId: number, | ||
state: ServerBotState, | ||
) => { | ||
const db = getDatabase(envSafe); | ||
const { error } = await db | ||
.from("user") | ||
.update({ server_bot_state: state }) | ||
.eq("id", userId); | ||
|
||
if (error) { | ||
throw new DatabaseException(error); | ||
} | ||
}; | ||
|
||
export const userGetServerBotState = async ( | ||
envSafe: EnvSafe, | ||
userId: number, | ||
): Promise<ServerBotState> => { | ||
const db = getDatabase(envSafe); | ||
const result = await db | ||
.from("user") | ||
.select("server_bot_state") | ||
.eq("id", userId) | ||
.single(); | ||
|
||
if (result.error) { | ||
throw new DatabaseException(result.error); | ||
} | ||
|
||
if (!result.data) { | ||
return null; | ||
} | ||
|
||
return result.data.server_bot_state as ServerBotState; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export enum CallbackQueryType { | ||
Deck = "deck", | ||
ConfirmCreateCard = "confirm", | ||
EditFront = "edit-front", | ||
EditBack = "edit-back", | ||
Cancel = "cancel", | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { EnvSafe } from "../env/env-schema.ts"; | ||
import { Context } from "grammy"; | ||
import { assert } from "../lib/typescript/assert.ts"; | ||
import { getDatabase } from "../db/get-database.ts"; | ||
import { CallbackQueryType } from "./callback-query-type.ts"; | ||
import { | ||
userGetServerBotState, | ||
userSetServerBotState, | ||
} from "../db/user/user-set-server-bot-state.ts"; | ||
import { sendCardCreateConfirmMessage } from "./send-card-create-confirm-message.ts"; | ||
import { DatabaseException } from "../db/database-exception.ts"; | ||
|
||
export const onCallbackQuery = (envSafe: EnvSafe) => async (ctx: Context) => { | ||
assert(ctx.callbackQuery); | ||
assert(ctx.from); | ||
|
||
const data = ctx.callbackQuery.data; | ||
const db = getDatabase(envSafe); | ||
if (!data) { | ||
await ctx.answerCallbackQuery(); | ||
return; | ||
} | ||
|
||
if (data.startsWith(CallbackQueryType.Deck)) { | ||
const deckId = Number(data.split(":")[1]); | ||
if (!deckId) { | ||
throw new Error(`Deck id ${deckId} is not valid`); | ||
} | ||
const state = await userGetServerBotState(envSafe, ctx.from.id); | ||
assert(state?.type === "cardAdded", "State is not cardAdded"); | ||
await userSetServerBotState(envSafe, ctx.from.id, { | ||
type: "deckSelected", | ||
cardBack: state.cardBack, | ||
cardFront: state.cardFront, | ||
deckId, | ||
}); | ||
|
||
await sendCardCreateConfirmMessage(envSafe, ctx); | ||
await ctx.answerCallbackQuery(); | ||
return; | ||
} | ||
|
||
if ( | ||
data === CallbackQueryType.EditFront || | ||
data === CallbackQueryType.EditBack | ||
) { | ||
const isFront = data === CallbackQueryType.EditFront; | ||
const state = await userGetServerBotState(envSafe, ctx.from.id); | ||
assert(state?.type === "deckSelected", "State is not deckSelected"); | ||
await userSetServerBotState(envSafe, ctx.from.id, { | ||
...state, | ||
editingField: isFront ? "cardFront" : "cardBack", | ||
}); | ||
await ctx.deleteMessage(); | ||
await ctx.reply( | ||
`Send a message with the new ${isFront ? "front" : "back"}:`, | ||
); | ||
return; | ||
} | ||
|
||
if (data === CallbackQueryType.Cancel) { | ||
await ctx.answerCallbackQuery("Cancelled"); | ||
await ctx.deleteMessage(); | ||
await userSetServerBotState(envSafe, ctx.from.id, null); | ||
return; | ||
} | ||
|
||
if (data === CallbackQueryType.ConfirmCreateCard) { | ||
const state = await userGetServerBotState(envSafe, ctx.from.id); | ||
assert(state?.type === "deckSelected", "State is not deckSelected"); | ||
|
||
const createCardsResult = await db.from("deck_card").insert({ | ||
deck_id: state.deckId, | ||
front: state.cardFront, | ||
back: state.cardBack, | ||
}); | ||
|
||
if (createCardsResult.error) { | ||
throw new DatabaseException(createCardsResult.error); | ||
} | ||
|
||
await ctx.reply('Card has been created. Click "MemoCard" to review it 👇'); | ||
await ctx.deleteMessage(); | ||
await userSetServerBotState(envSafe, ctx.from.id, null); | ||
return; | ||
} | ||
|
||
console.log("Unknown button event with payload", data); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { EnvSafe } from "../env/env-schema.ts"; | ||
import { Context, InlineKeyboard } from "grammy"; | ||
import { assert } from "../lib/typescript/assert.ts"; | ||
import { | ||
userGetServerBotState, | ||
userSetServerBotState, | ||
} from "../db/user/user-set-server-bot-state.ts"; | ||
import { sendCardCreateConfirmMessage } from "./send-card-create-confirm-message.ts"; | ||
import { parseDeckFromText } from "./parse-deck-from-text.ts"; | ||
import { getDecksCreatedByMe } from "../db/deck/get-decks-created-by-me.ts"; | ||
import { CallbackQueryType } from "./callback-query-type.ts"; | ||
|
||
export const onMessage = (envSafe: EnvSafe) => async (ctx: Context) => { | ||
if (!ctx.message?.text) { | ||
return; | ||
} | ||
assert(ctx.from); | ||
|
||
const userState = await userGetServerBotState(envSafe, ctx.from.id); | ||
if (userState?.type === "deckSelected" && userState.editingField) { | ||
await userSetServerBotState(envSafe, ctx.from.id, { | ||
...userState, | ||
[userState.editingField]: ctx.message.text, | ||
editingField: undefined, | ||
}); | ||
|
||
await sendCardCreateConfirmMessage(envSafe, ctx); | ||
|
||
return; | ||
} | ||
|
||
const cardAsText = parseDeckFromText(ctx.message.text); | ||
if (!cardAsText) { | ||
await ctx.reply( | ||
"Please send a message in the format: `front \\- back`\n\n*Example:*\nMe gusta \\- I like it", | ||
{ | ||
parse_mode: "MarkdownV2", | ||
}, | ||
); | ||
return; | ||
} | ||
const [decks] = await Promise.all([ | ||
getDecksCreatedByMe(envSafe, ctx.from.id), | ||
userSetServerBotState(envSafe, ctx.from.id, { | ||
type: "cardAdded", | ||
cardFront: cardAsText.front, | ||
cardBack: cardAsText.back, | ||
}), | ||
]); | ||
|
||
await ctx.reply("To create a card from it, select a deck: ", { | ||
reply_markup: InlineKeyboard.from( | ||
decks | ||
.map((deck) => [ | ||
InlineKeyboard.text( | ||
deck.name, | ||
`${CallbackQueryType.Deck}:${deck.id}`, | ||
), | ||
]) | ||
.concat([[InlineKeyboard.text("❌ Cancel", CallbackQueryType.Cancel)]]), | ||
), | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { Context } from "grammy"; | ||
|
||
export const onStart = (ctx: Context) => { | ||
return ctx.reply( | ||
`Improve your memory with spaced repetition. Learn languages, history or other subjects with the proven flashcard method. Click "MemoCard" 👇`, | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export const parseDeckFromText = ( | ||
text: string, | ||
): { | ||
front: string; | ||
back: string; | ||
} | null => { | ||
const [front, back] = text.split(" - "); | ||
if (!front || !back) { | ||
return null; | ||
} | ||
return { front, back }; | ||
}; |
Oops, something went wrong.